diff --git a/modules/setting/repository.go b/modules/setting/repository.go index a722ea2a0..b6b0b5050 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -7,6 +7,7 @@ import ( "os/exec" "path" "path/filepath" + "slices" "strings" "code.gitea.io/gitea/modules/log" @@ -19,6 +20,8 @@ const ( RepoCreatingPublic = "public" ) +var RecognisedRepositoryDownloadOrCloneMethods = []string{"download-zip", "download-targz", "download-bundle", "vscode-clone", "vscodium-clone", "cite"} + // ItemsPerPage maximum items per page in forks, watchers and stars of a repo const ItemsPerPage = 40 @@ -43,6 +46,7 @@ var ( DisabledRepoUnits []string DefaultRepoUnits []string DefaultForkRepoUnits []string + DownloadOrCloneMethods []string PrefixArchiveFiles bool DisableMigrations bool DisableStars bool `ini:"DISABLE_STARS"` @@ -161,6 +165,7 @@ var ( DisabledRepoUnits: []string{}, DefaultRepoUnits: []string{}, DefaultForkRepoUnits: []string{}, + DownloadOrCloneMethods: []string{"download-zip", "download-targz", "download-bundle", "vscode-clone"}, PrefixArchiveFiles: true, DisableMigrations: false, DisableStars: false, @@ -361,4 +366,10 @@ func loadRepositoryFrom(rootCfg ConfigProvider) { if err := loadRepoArchiveFrom(rootCfg); err != nil { log.Fatal("loadRepoArchiveFrom: %v", err) } + + for _, method := range Repository.DownloadOrCloneMethods { + if !slices.Contains(RecognisedRepositoryDownloadOrCloneMethods, method) { + log.Error("Unrecognised repository download or clone method: %s", method) + } + } } diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 08d83f94b..c1d1af852 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -53,6 +53,7 @@ func CommonTemplateContextData() ContextData { "ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, "ShowFooterVersion": setting.Other.ShowFooterVersion, "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, + "DownloadOrCloneMethods": setting.Repository.DownloadOrCloneMethods, "EnableSwagger": setting.API.EnableSwagger, "EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl index 0797b400d..9ff826bc9 100644 --- a/templates/repo/clone_script.tmpl +++ b/templates/repo/clone_script.tmpl @@ -38,5 +38,8 @@ for (const el of document.getElementsByClassName('js-clone-url-vsc')) { el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link); } + for (const el of document.getElementsByClassName('js-clone-url-vscodium')) { + el['href'] = 'vscodium://vscode.git/clone?url=' + encodeURIComponent(link); + } })(); diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index d91dc4394..eb6d96f28 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -131,15 +131,32 @@ {{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}} diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index ed5466bc6..bb341b2ce 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -43,6 +43,84 @@ func TestViewRepo(t *testing.T) { session.MakeRequest(t, req, http.StatusNotFound) } +func TestViewRepoCloneMethods(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + getCloneMethods := func() []string { + req := NewRequest(t, "GET", "/user2/repo1") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + cloneMoreMethodsHTML := htmlDoc.doc.Find("#more-btn div a") + + var methods []string + cloneMoreMethodsHTML.Each(func(i int, s *goquery.Selection) { + a, _ := s.Attr("href") + methods = append(methods, a) + }) + + return methods + } + + testCloneMethods := func(expected []string) { + methods := getCloneMethods() + + assert.Len(t, methods, len(expected)) + for i, expectedMethod := range expected { + assert.Contains(t, methods[i], expectedMethod) + } + } + + t.Run("Defaults", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + testCloneMethods([]string{"/master.zip", "/master.tar.gz", "/master.bundle", "vscode://"}) + }) + + t.Run("Customized methods", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, []string{"vscodium-clone", "download-targz"})() + + testCloneMethods([]string{"vscodium://", "/master.tar.gz"}) + }) + + t.Run("Individual methods", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + singleMethodTest := func(method, expectedURLPart string) { + t.Run(method, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, []string{method})() + + testCloneMethods([]string{expectedURLPart}) + }) + } + + cases := map[string]string{ + "download-zip": "/master.zip", + "download-targz": "/master.tar.gz", + "download-bundle": "/master.bundle", + "vscode-clone": "vscode://", + "vscodium-clone": "vscodium://", + } + for method, expectedURLPart := range cases { + singleMethodTest(method, expectedURLPart) + } + }) + + t.Run("All methods", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, setting.RecognisedRepositoryDownloadOrCloneMethods)() + + methods := getCloneMethods() + // We compare against + // len(setting.RecognisedRepositoryDownloadOrCloneMethods) - 1, because + // the test environment does not currently set things up for the cite + // method to display. + assert.GreaterOrEqual(t, len(methods), len(setting.RecognisedRepositoryDownloadOrCloneMethods)-1) + }) +} + func testViewRepo(t *testing.T) { defer tests.PrepareTestEnv(t)()