Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clone repository with Tea CLI #33725

Merged
merged 11 commits into from
Feb 27, 2025
22 changes: 15 additions & 7 deletions models/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,14 +646,16 @@ 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.
func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string {
// ComposeHTTPSCloneLink returns HTTPS clone URL based on the given owner and repository name.
func ComposeHTTPSCloneLink(ctx context.Context, owner, repo string) string {
return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo))
}

func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
// ComposeSSHCloneLink returns SSH clone URL based on the given owner and repository name.
func ComposeSSHCloneLink(doer *user_model.User, ownerName, repoName string) string {
sshUser := setting.SSH.User
sshDomain := setting.SSH.Domain

Expand Down Expand Up @@ -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))
}

// ComposeTeaCloneLink returns Tea CLI clone command based on the given owner and repository name.
func ComposeTeaCloneLink(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: ComposeSSHCloneLink(doer, repo.OwnerName, repoPathName),
HTTPS: ComposeHTTPSCloneLink(ctx, repo.OwnerName, repoPathName),
Tea: ComposeTeaCloneLink(ctx, repo.OwnerName, repoPathName),
}
}

// CloneLink returns clone URLs of repository.
Expand Down
16 changes: 8 additions & 8 deletions models/repo/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,30 +183,30 @@ func TestComposeSSHCloneURL(t *testing.T) {
setting.SSH.Domain = "domain"
setting.SSH.Port = 22
setting.Repository.UseCompatSSHURI = false
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneLink(&user_model.User{Name: "doer"}, "user", "repo"))
setting.Repository.UseCompatSSHURI = true
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneLink(&user_model.User{Name: "doer"}, "user", "repo"))
// test SSH_DOMAIN while use non-standard SSH port
setting.SSH.Port = 123
setting.Repository.UseCompatSSHURI = false
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneLink(nil, "user", "repo"))
setting.Repository.UseCompatSSHURI = true
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneLink(nil, "user", "repo"))

// test IPv6 SSH_DOMAIN
setting.Repository.UseCompatSSHURI = false
setting.SSH.Domain = "::1"
setting.SSH.Port = 22
assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneLink(nil, "user", "repo"))
setting.SSH.Port = 123
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneLink(nil, "user", "repo"))

setting.SSH.User = "(DOER_USERNAME)"
setting.SSH.Domain = "domain"
setting.SSH.Port = 22
assert.Equal(t, "doer@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
assert.Equal(t, "doer@domain:user/repo.git", ComposeSSHCloneLink(&user_model.User{Name: "doer"}, "user", "repo"))
setting.SSH.Port = 123
assert.Equal(t, "ssh://doer@domain:123/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
assert.Equal(t, "ssh://doer@domain:123/user/repo.git", ComposeSSHCloneLink(&user_model.User{Name: "doer"}, "user", "repo"))
}

func TestIsUsableRepoName(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions routers/web/goget.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ func goGet(ctx *context.Context) {

var cloneURL string
if setting.Repository.GoGetCloneURLProtocol == "ssh" {
cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName)
cloneURL = repo_model.ComposeSSHCloneLink(ctx.Doer, ownerName, repoName)
} else {
cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName)
cloneURL = repo_model.ComposeHTTPSCloneLink(ctx, ownerName, repoName)
}
goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/)
goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/)
Expand Down
4 changes: 2 additions & 2 deletions services/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,9 @@ func EarlyResponseForGoGetMeta(ctx *Context) {

var cloneURL string
if setting.Repository.GoGetCloneURLProtocol == "ssh" {
cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame)
cloneURL = repo_model.ComposeSSHCloneLink(ctx.Doer, username, reponame)
} else {
cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame)
cloneURL = repo_model.ComposeHTTPSCloneLink(ctx, username, reponame)
}
goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL)
htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent))
Expand Down
1 change: 1 addition & 0 deletions templates/repo/clone_panel.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
{{if $.CloneButtonShowSSH}}
<button class="item repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">SSH</button>
{{end}}
<button class="item repo-clone-tea" data-link="{{$.CloneButtonOriginLink.Tea}}">Tea CLI</button>
</div>
<div class="divider"></div>

Expand Down
10 changes: 10 additions & 0 deletions tests/integration/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
44 changes: 31 additions & 13 deletions web_src/js/features/repo-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,34 @@ export function substituteRepoOpenWithUrl(tmpl: string, url: string): string {
function initCloneSchemeUrlSelection(parent: Element) {
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 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);
const isHTTPS = scheme === 'https' && Boolean(tabHTTPS) || scheme !== 'https' && !tabHTTPS;
const isSSH = scheme === 'ssh' && Boolean(tabSSH) || scheme !== 'ssh' && !tabSSH;
const isTea = scheme === 'tea' && Boolean(tabTea) || scheme !== 'tea' && !tabTea;
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 (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'));

Expand All @@ -83,13 +97,17 @@ function initCloneSchemeUrlSelection(parent: Element) {
};

updateClonePanelUi();
// tabSsh or tabHttps might not both exist, eg: guest view, or one is disabled by the server
tabSsh?.addEventListener('click', () => {
// 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', () => {
Expand Down
Loading