From f52e31f5ce88796f7a75c92dd721de4020784e7c Mon Sep 17 00:00:00 2001 From: Quentin Date: Thu, 27 Feb 2025 19:18:02 +0100 Subject: [PATCH] Clone repository with Tea CLI (#33725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds "Tea CLI" as a clone method. Capture d’écran 2025-02-25 à 23 38 47 --------- Signed-off-by: Quentin Guidée Co-authored-by: wxiaoguang --- models/repo/repo.go | 18 +++++++--- templates/repo/clone_panel.tmpl | 1 + tests/integration/repo_test.go | 10 ++++++ web_src/js/features/repo-common.ts | 57 +++++++++++++++++++++++------- 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index 4e27dbaf14..d42792faa2 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -646,13 +646,15 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { type CloneLink struct { SSH string HTTPS string + Tea string } -// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name. +// ComposeHTTPSCloneURL returns HTTPS clone URL based on the given owner and repository name. func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string { return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo)) } +// ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name. func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { sshUser := setting.SSH.User sshDomain := setting.SSH.Domain @@ -686,11 +688,17 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) } +// ComposeTeaCloneCommand returns Tea CLI clone command based on the given owner and repository name. +func ComposeTeaCloneCommand(ctx context.Context, owner, repo string) string { + return fmt.Sprintf("tea clone %s/%s", url.PathEscape(owner), url.PathEscape(repo)) +} + func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink { - cl := new(CloneLink) - cl.SSH = ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName) - cl.HTTPS = ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName) - return cl + return &CloneLink{ + SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName), + HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName), + Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName), + } } // CloneLink returns clone URLs of repository. diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl index b813860150..2ed8f52fbe 100644 --- a/templates/repo/clone_panel.tmpl +++ b/templates/repo/clone_panel.tmpl @@ -14,6 +14,7 @@ {{if $.CloneButtonShowSSH}} {{end}} +
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index 8c568a1272..38d0e7fe1f 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -130,8 +130,13 @@ func TestViewRepo1CloneLinkAnonymous(t *testing.T) { link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") assert.True(t, exists, "The template has changed") assert.Equal(t, setting.AppURL+"user2/repo1.git", link) + _, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") assert.False(t, exists) + + link, exists = htmlDoc.doc.Find(".repo-clone-tea").Attr("data-link") + assert.True(t, exists, "The template has changed") + assert.Equal(t, "tea clone user2/repo1", link) } func TestViewRepo1CloneLinkAuthorized(t *testing.T) { @@ -146,10 +151,15 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) { link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") assert.True(t, exists, "The template has changed") assert.Equal(t, setting.AppURL+"user2/repo1.git", link) + link, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") assert.True(t, exists, "The template has changed") sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.User, setting.SSH.Domain, setting.SSH.Port) assert.Equal(t, sshURL, link) + + link, exists = htmlDoc.doc.Find(".repo-clone-tea").Attr("data-link") + assert.True(t, exists, "The template has changed") + assert.Equal(t, "tea clone user2/repo1", link) } func TestViewRepoWithSymlinks(t *testing.T) { diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index 444cb163bf..4362a2c713 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -53,20 +53,49 @@ export function substituteRepoOpenWithUrl(tmpl: string, url: string): string { function initCloneSchemeUrlSelection(parent: Element) { const elCloneUrlInput = parent.querySelector('.repo-clone-url'); - const tabSsh = parent.querySelector('.repo-clone-ssh'); const tabHttps = parent.querySelector('.repo-clone-https'); + const tabSsh = parent.querySelector('.repo-clone-ssh'); + const tabTea = parent.querySelector('.repo-clone-tea'); const updateClonePanelUi = function() { - const scheme = localStorage.getItem('repo-clone-protocol') || 'https'; - const isSSH = scheme === 'ssh' && Boolean(tabSsh) || scheme !== 'ssh' && !tabHttps; - if (tabHttps) { - tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS" - tabHttps.classList.toggle('active', !isSSH); - } - if (tabSsh) { - tabSsh.classList.toggle('active', isSSH); + let scheme = localStorage.getItem('repo-clone-protocol'); + if (!['https', 'ssh', 'tea'].includes(scheme)) { + scheme = 'https'; + } + + // Fallbacks if the scheme preference is not available in the tabs, for example: empty repo page, there are only HTTPS and SSH + if (scheme === 'tea' && !tabTea) { + scheme = 'https'; + } + if (scheme === 'https' && !tabHttps) { + scheme = 'ssh'; + } else if (scheme === 'ssh' && !tabSsh) { + scheme = 'https'; + } + + const isHttps = scheme === 'https'; + const isSsh = scheme === 'ssh'; + const isTea = scheme === 'tea'; + + if (tabHttps) { + tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS" + tabHttps.classList.toggle('active', isHttps); + } + if (tabSsh) { + tabSsh.classList.toggle('active', isSsh); + } + if (tabTea) { + tabTea.classList.toggle('active', isTea); + } + + let tab: Element; + if (isHttps) { + tab = tabHttps; + } else if (isSsh) { + tab = tabSsh; + } else if (isTea) { + tab = tabTea; } - const tab = isSSH ? tabSsh : tabHttps; if (!tab) return; const link = toOriginUrl(tab.getAttribute('data-link')); @@ -84,12 +113,16 @@ function initCloneSchemeUrlSelection(parent: Element) { updateClonePanelUi(); // tabSsh or tabHttps might not both exist, eg: guest view, or one is disabled by the server + tabHttps?.addEventListener('click', () => { + localStorage.setItem('repo-clone-protocol', 'https'); + updateClonePanelUi(); + }); tabSsh?.addEventListener('click', () => { localStorage.setItem('repo-clone-protocol', 'ssh'); updateClonePanelUi(); }); - tabHttps?.addEventListener('click', () => { - localStorage.setItem('repo-clone-protocol', 'https'); + tabTea?.addEventListener('click', () => { + localStorage.setItem('repo-clone-protocol', 'tea'); updateClonePanelUi(); }); elCloneUrlInput.addEventListener('focus', () => {