Configurable clone methods

Adds `[repository].DOWNLOAD_OR_CLONE_METHODS` (defaulting to
"download-zip,download-targz,download-bundle,vscode-clone"), which lets
an instance administrator override the additional clone methods
displayed on the repository home view.

This is purely display-only, the clone methods not listed here are still
available, unless disabled elsewhere. They're just not displayed.

Fixes #710.

Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
This commit is contained in:
Gergely Nagy 2023-12-31 16:24:05 +01:00
parent 98b5a08f06
commit 2aadcf4946
No known key found for this signature in database
6 changed files with 119 additions and 7 deletions

View file

@ -7,6 +7,7 @@ import (
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -19,6 +20,8 @@ const (
RepoCreatingPublic = "public" 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 // ItemsPerPage maximum items per page in forks, watchers and stars of a repo
const ItemsPerPage = 40 const ItemsPerPage = 40
@ -43,6 +46,7 @@ var (
DisabledRepoUnits []string DisabledRepoUnits []string
DefaultRepoUnits []string DefaultRepoUnits []string
DefaultForkRepoUnits []string DefaultForkRepoUnits []string
DownloadOrCloneMethods []string
PrefixArchiveFiles bool PrefixArchiveFiles bool
DisableMigrations bool DisableMigrations bool
DisableStars bool `ini:"DISABLE_STARS"` DisableStars bool `ini:"DISABLE_STARS"`
@ -160,6 +164,7 @@ var (
DisabledRepoUnits: []string{}, DisabledRepoUnits: []string{},
DefaultRepoUnits: []string{}, DefaultRepoUnits: []string{},
DefaultForkRepoUnits: []string{}, DefaultForkRepoUnits: []string{},
DownloadOrCloneMethods: []string{"download-zip", "download-targz", "download-bundle", "vscode-clone"},
PrefixArchiveFiles: true, PrefixArchiveFiles: true,
DisableMigrations: false, DisableMigrations: false,
DisableStars: false, DisableStars: false,
@ -358,4 +363,10 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
if err := loadRepoArchiveFrom(rootCfg); err != nil { if err := loadRepoArchiveFrom(rootCfg); err != nil {
log.Fatal("loadRepoArchiveFrom: %v", err) 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)
}
}
} }

View file

@ -53,6 +53,7 @@ func CommonTemplateContextData() ContextData {
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, "ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
"ShowFooterVersion": setting.Other.ShowFooterVersion, "ShowFooterVersion": setting.Other.ShowFooterVersion,
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,
"DownloadOrCloneMethods": setting.Repository.DownloadOrCloneMethods,
"EnableSwagger": setting.API.EnableSwagger, "EnableSwagger": setting.API.EnableSwagger,
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, "EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,

View file

@ -961,6 +961,7 @@ all_branches = All branches
fork_no_valid_owners = This repository can not be forked because there are no valid owners. fork_no_valid_owners = This repository can not be forked because there are no valid owners.
use_template = Use this template use_template = Use this template
clone_in_vsc = Clone in VS Code clone_in_vsc = Clone in VS Code
clone_in_vscodium = Clone in VS Codium
download_zip = Download ZIP download_zip = Download ZIP
download_tar = Download TAR.GZ download_tar = Download TAR.GZ
download_bundle = Download BUNDLE download_bundle = Download BUNDLE

View file

@ -38,5 +38,8 @@
for (const el of document.getElementsByClassName('js-clone-url-vsc')) { for (const el of document.getElementsByClassName('js-clone-url-vsc')) {
el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link); 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);
}
})(); })();
</script> </script>

View file

@ -131,15 +131,32 @@
<button id="more-btn" class="ui basic small compact jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}"> <button id="more-btn" class="ui basic small compact jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
{{svg "octicon-kebab-horizontal"}} {{svg "octicon-kebab-horizontal"}}
<div class="menu"> <div class="menu">
{{$citation := .CitationExist}}
{{$originLink := .CloneButtonOriginLink}}
{{range $.DownloadOrCloneMethods}}
{{if not $.DisableDownloadSourceArchives}} {{if not $.DisableDownloadSourceArchives}}
{{if eq . "download-zip"}}
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_zip"}}</a> <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
{{end}}
{{if eq . "download-targz"}}
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_tar"}}</a> <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
{{end}}
{{if eq . "download-bundle"}}
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a> <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
{{if .CitiationExist}} {{end}}
{{if $citation}}
{{if eq . "cite"}}
<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a> <a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
{{end}} {{end}}
{{end}} {{end}}
<a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{.CloneButtonOriginLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vsc"}}</a> {{end}}
{{if eq . "vscode-clone"}}
<a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{$originLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vsc"}}</a>
{{end}}
{{if eq . "vscodium-clone"}}
<a class="item js-clone-url-vscodium" href="vscodium://vscode.git/clone?url={{$originLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vscodium"}}</a>
{{end}}
{{end}}
</div> </div>
</button> </button>
{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}} {{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}

View file

@ -12,6 +12,7 @@ import (
"time" "time"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
@ -42,6 +43,84 @@ func TestViewRepo(t *testing.T) {
session.MakeRequest(t, req, http.StatusNotFound) 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) { func testViewRepo(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()