mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Clone repository with Tea CLI (#33725)
This PR adds "Tea CLI" as a clone method. <img width="350" alt="Capture d’écran 2025-02-25 à 23 38 47" src="https://github.com/user-attachments/assets/8e86e54a-998b-45d1-9f20-167b449e79b6" /> --------- Signed-off-by: Quentin Guidée <quentin.guidee@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -646,13 +646,15 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { | |||||||
| type CloneLink struct { | type CloneLink struct { | ||||||
| 	SSH   string | 	SSH   string | ||||||
| 	HTTPS 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 { | 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)) | 	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 { | func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { | ||||||
| 	sshUser := setting.SSH.User | 	sshUser := setting.SSH.User | ||||||
| 	sshDomain := setting.SSH.Domain | 	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)) | 	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 { | func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink { | ||||||
| 	cl := new(CloneLink) | 	return &CloneLink{ | ||||||
| 	cl.SSH = ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName) | 		SSH:   ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName), | ||||||
| 	cl.HTTPS = ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName) | 		HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName), | ||||||
| 	return cl | 		Tea:   ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName), | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // CloneLink returns clone URLs of repository. | // CloneLink returns clone URLs of repository. | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ | |||||||
| 		{{if $.CloneButtonShowSSH}} | 		{{if $.CloneButtonShowSSH}} | ||||||
| 			<button class="item repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">SSH</button> | 			<button class="item repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">SSH</button> | ||||||
| 		{{end}} | 		{{end}} | ||||||
|  | 		<button class="item repo-clone-tea" data-link="{{$.CloneButtonOriginLink.Tea}}">Tea CLI</button> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="divider"></div> | 	<div class="divider"></div> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -130,8 +130,13 @@ func TestViewRepo1CloneLinkAnonymous(t *testing.T) { | |||||||
| 	link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") | 	link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") | ||||||
| 	assert.True(t, exists, "The template has changed") | 	assert.True(t, exists, "The template has changed") | ||||||
| 	assert.Equal(t, setting.AppURL+"user2/repo1.git", link) | 	assert.Equal(t, setting.AppURL+"user2/repo1.git", link) | ||||||
|  |  | ||||||
| 	_, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") | 	_, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") | ||||||
| 	assert.False(t, exists) | 	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) { | 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") | 	link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") | ||||||
| 	assert.True(t, exists, "The template has changed") | 	assert.True(t, exists, "The template has changed") | ||||||
| 	assert.Equal(t, setting.AppURL+"user2/repo1.git", link) | 	assert.Equal(t, setting.AppURL+"user2/repo1.git", link) | ||||||
|  |  | ||||||
| 	link, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") | 	link, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") | ||||||
| 	assert.True(t, exists, "The template has changed") | 	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) | 	sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.User, setting.SSH.Domain, setting.SSH.Port) | ||||||
| 	assert.Equal(t, sshURL, link) | 	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) { | func TestViewRepoWithSymlinks(t *testing.T) { | ||||||
|   | |||||||
| @@ -53,20 +53,49 @@ export function substituteRepoOpenWithUrl(tmpl: string, url: string): string { | |||||||
| function initCloneSchemeUrlSelection(parent: Element) { | function initCloneSchemeUrlSelection(parent: Element) { | ||||||
|   const elCloneUrlInput = parent.querySelector<HTMLInputElement>('.repo-clone-url'); |   const elCloneUrlInput = parent.querySelector<HTMLInputElement>('.repo-clone-url'); | ||||||
|  |  | ||||||
|   const tabSsh = parent.querySelector('.repo-clone-ssh'); |  | ||||||
|   const tabHttps = parent.querySelector('.repo-clone-https'); |   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 updateClonePanelUi = function() { | ||||||
|     const scheme = localStorage.getItem('repo-clone-protocol') || 'https'; |     let scheme = localStorage.getItem('repo-clone-protocol'); | ||||||
|     const isSSH = scheme === 'ssh' && Boolean(tabSsh) || scheme !== 'ssh' && !tabHttps; |     if (!['https', 'ssh', 'tea'].includes(scheme)) { | ||||||
|     if (tabHttps) { |       scheme = 'https'; | ||||||
|       tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS" |     } | ||||||
|       tabHttps.classList.toggle('active', !isSSH); |  | ||||||
|     } |     // Fallbacks if the scheme preference is not available in the tabs, for example: empty repo page, there are only HTTPS and SSH | ||||||
|     if (tabSsh) { |     if (scheme === 'tea' && !tabTea) { | ||||||
|       tabSsh.classList.toggle('active', isSSH); |       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; |     if (!tab) return; | ||||||
|     const link = toOriginUrl(tab.getAttribute('data-link')); |     const link = toOriginUrl(tab.getAttribute('data-link')); | ||||||
|  |  | ||||||
| @@ -84,12 +113,16 @@ function initCloneSchemeUrlSelection(parent: Element) { | |||||||
|  |  | ||||||
|   updateClonePanelUi(); |   updateClonePanelUi(); | ||||||
|   // tabSsh or tabHttps might not both exist, eg: guest view, or one is disabled by the server |   // 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', () => { |   tabSsh?.addEventListener('click', () => { | ||||||
|     localStorage.setItem('repo-clone-protocol', 'ssh'); |     localStorage.setItem('repo-clone-protocol', 'ssh'); | ||||||
|     updateClonePanelUi(); |     updateClonePanelUi(); | ||||||
|   }); |   }); | ||||||
|   tabHttps?.addEventListener('click', () => { |   tabTea?.addEventListener('click', () => { | ||||||
|     localStorage.setItem('repo-clone-protocol', 'https'); |     localStorage.setItem('repo-clone-protocol', 'tea'); | ||||||
|     updateClonePanelUi(); |     updateClonePanelUi(); | ||||||
|   }); |   }); | ||||||
|   elCloneUrlInput.addEventListener('focus', () => { |   elCloneUrlInput.addEventListener('focus', () => { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Quentin
					Quentin