Compare commits

...
2 Commits
Author SHA1 Message Date
2c8e99bbf7 fix: make commit message merge correctly (#38490)
fix #38487

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
2026-07-17 07:44:05 +00:00
8a3daef525 fix(repo): stop advertising HTTP clone URLs when DISABLE_HTTP_GIT is set (#38378)
Fixes #38339

---------

Signed-off-by: TowyTowy <towy@airreps.link>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-17 05:17:28 +00:00
17 changed files with 216 additions and 149 deletions
+10 -3
View File
@@ -647,6 +647,10 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
// CloneLink represents different types of clone URLs of repository.
type CloneLink struct {
IsWikiRepo bool
SupportSSH bool
SupportHTTPS bool
SSH string
HTTPS string
Tea string
@@ -698,9 +702,12 @@ func ComposeTeaCloneCommand(ctx context.Context, owner, repo string) string {
func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink {
return &CloneLink{
SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName),
HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName),
Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName),
IsWikiRepo: strings.HasSuffix(repoPathName, ".wiki"),
SupportHTTPS: !setting.Repository.DisableHTTPGit,
SupportSSH: !setting.SSH.Disabled && (doer != nil || setting.SSH.ExposeAnonymous),
SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName),
HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName),
Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName),
}
}
+41 -2
View File
@@ -77,8 +77,12 @@ func (c *CommitMessage) MessageTrailer() CommitMessageTrailerValues {
}
var commitMessageTrailerSplit = sync.OnceValue(func() *regexp.Regexp {
// the sep is either something like "\n---\n" or "\n\n" in the body, or at the start of the body like "---\n"
return regexp.MustCompile(`(?s)^(?P<content>.*?)(?P<sep>^|^\n|^-{3,}\n+|\n-{3,}\n+|\n\n)(?P<trailer>(?:[A-Za-z0-9][-A-Za-z0-9]*:[^\n]*\n?)*\n*)$`)
// ref: https://git-scm.com/docs/git-interpret-trailers
// TODO: the regexp is not able to perfectly parse the all kinds of trailers
// It was just copied from legacy code, it is not exactly the same as how Git parses the trailer and not quite right in some cases.
// For the key characters: it follows RFC 822 field name syntax (or RFC 2822/RFC 5322): printable ASCII characters between 33 and 126 except the colon (:),
// but maybe we don't want to make it that complicated, so here we only support some common "symbol-like" characters.
return regexp.MustCompile(`(?s)^(?P<content>.*?)(?P<sep>^|^\n|^-{3,}\n+|\n+-{3,}\n+|\n{2,})(?P<trailer>(?:[A-Za-z0-9][-\w]*:[^\n]*(\n\s+[^\n]*)*\n?)*\n*)$`)
})
// CommitMessageSplitTrailer tries to split the message by the trailer separator
@@ -93,6 +97,41 @@ func CommitMessageSplitTrailer(s string) (content, sep, trailer string) {
return v[re.SubexpIndex("content")], v[re.SubexpIndex("sep")], v[re.SubexpIndex("trailer")]
}
// CommitMessageMerge merges two commit messages with their trailers
func CommitMessageMerge(m1, m2 string) string {
c1, s1, t1 := CommitMessageSplitTrailer(m1)
c2, s2, t2 := CommitMessageSplitTrailer(m2)
c1, t1 = strings.TrimSpace(c1), strings.TrimSpace(t1)
c2, t2 = strings.TrimSpace(c2), strings.TrimSpace(t2)
out := strings.Builder{}
if c1 != "" && c2 != "" {
out.WriteString(c1)
out.WriteString("\n\n")
out.WriteString(c2)
} else if c1 != "" {
out.WriteString(c1)
} else if c2 != "" {
out.WriteString(c2)
}
if t1 != "" || t2 != "" {
sep := util.Iif(t1 == "", s2, s1)
sep = util.IfZero(sep, "\n\n")
if c1 != "" || c2 != "" {
out.WriteString(sep)
}
if t1 != "" {
out.WriteString(t1)
}
if t1 != "" && t2 != "" {
out.WriteString("\n")
}
if t2 != "" {
out.WriteString(t2)
}
}
return out.String()
}
func CommitMessageParseTrailer(s string) CommitMessageTrailerValues {
ret := CommitMessageTrailerValues{}
for line := range strings.SplitSeq(util.NormalizeStringEOL(s), "\n") {
+32 -2
View File
@@ -26,10 +26,12 @@ func TestCommitMessageTrailer(t *testing.T) {
{"a", "a", "", ""},
{"a\n\nk", "a\n\nk", "", ""},
{"a\n\nk:v", "a", "\n\n", "k:v"},
{"a\n\nk:v\n next-line", "a", "\n\n", "k:v\n next-line"},
{"a\n\nk:v\n next-line\nother: v", "a", "\n\n", "k:v\n next-line\nother: v"},
{"a\n\nk:v\n\n", "a", "\n\n", "k:v\n\n"},
{"a\n--\nk:v", "a\n--\nk:v", "", ""},
{"a\n---\nk:v", "a", "\n---\n", "k:v"},
{"a\n\n---\n\nk:v", "a\n", "\n---\n\n", "k:v"},
{"a\n---\nk:v", "a", "\n---\n", "k:v"}, // TODO: should we support such case? No empty line between "---" and the trailer
{"a\n\n---\n\nk:v", "a", "\n\n---\n\n", "k:v"},
{"k: v", "", "", "k: v"},
{"\nk:v", "", "\n", "k:v"},
@@ -127,3 +129,31 @@ func TestCommitMessageParticipants(t *testing.T) {
}
})
}
func TestCommitMessageMerge(t *testing.T) {
cases := []struct {
m1, m2 string
out string
}{
{"", "", ""},
{"msg1", "", "msg1"},
{"", "msg2", "msg2"},
{"msg1", "msg2", "msg1\n\nmsg2"},
{"k1: a", "", "k1: a"},
{"", "k2: b", "k2: b"},
{"k1: a", "k2: b", "k1: a\nk2: b"},
{"msg1", "k2: b", "msg1\n\nk2: b"},
{"k1: a", "msg2", "msg2\n\nk1: a"},
{"msg1\n\nk1: a", "msg2", "msg1\n\nmsg2\n\nk1: a"},
{"msg1\n----\nk1: a", "msg2", "msg1\n\nmsg2\n----\nk1: a"},
{"msg1\n\n----\n\nk1: a", "msg2", "msg1\n\nmsg2\n\n----\n\nk1: a"},
{"msg1", "msg2\n----\nk2: b", "msg1\n\nmsg2\n----\nk2: b"},
{"msg1", "msg2\n\nk2: b", "msg1\n\nmsg2\n\nk2: b"},
{"msg1\n\nk1: a", "msg2\n\nk2: b", "msg1\n\nmsg2\n\nk1: a\nk2: b"},
}
for i, c := range cases {
out := CommitMessageMerge(c.m1, c.m2)
assert.Equal(t, c.out, out, "idx=%d, m1=%q m2=%q", i, c.m1, c.m2)
}
}
+18 -14
View File
@@ -10,6 +10,8 @@ import (
pull_model "gitea.dev/models/pull"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
"gitea.dev/modules/svg"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
@@ -62,20 +64,23 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
hasPendingPullRequestMergeTip = ctx.Locale.Tr("repo.pulls.auto_merge_has_pending_schedule", pendingPullRequestMerge.Doer.Name, createdPRMergeStr)
}
defaultMergeTitle, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetDefaultMergeMessage", err)
return
}
defaultSquashMergeTitle, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetDefaultSquashMergeMessage", err)
return
}
var defaultMergeTitle, defaultMergeBody string
var defaultSquashMergeTitle, defaultSquashMergeBody string
var defaultSquashMergeCommitMessages string
if !prInfo.IsPullRequestBroken {
defaultSquashMergeCommitMessages = pull_service.GetSquashMergeCommitMessages(ctx, pull)
var err error
defaultMergeTitle, defaultMergeBody, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
if err != nil && !errors.Is(err, util.ErrNotExist) {
log.Error("GetDefaultMergeMessage for style %s failed, error: %v", mergeStyle, err)
}
defaultSquashMergeTitle, defaultSquashMergeBody, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
if err != nil && !errors.Is(err, util.ErrNotExist) {
log.Error("GetDefaultMergeMessage for squash failed, error: %v", err)
}
defaultSquashMergeCommitMessages, err = pull_service.GetSquashMergeCommitMessages(ctx, pull)
if err != nil && !errors.Is(err, util.ErrNotExist) {
log.Error("GetSquashMergeCommitMessages failed, error: %v", err)
}
}
allOverridableChecksOk := !prInfo.MergeBoxData.hasOverridableBlockers
@@ -106,7 +111,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
// if this pr can be merged now, then hide the auto merge
generalHideAutoMerge := prInfo.MergeBoxData.canMergeNow && allOverridableChecksOk
var mergeStyles []any
if pull.IsStatusMergeable() {
mergeStyles = []any{
@@ -138,7 +142,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
"allowed": prConfig.AllowSquash,
"textDoMerge": ctx.Locale.Tr("repo.pulls.squash_merge_pull_request"),
"mergeTitleFieldText": defaultSquashMergeTitle,
"mergeMessageFieldText": defaultSquashMergeCommitMessages + defaultSquashMergeBody,
"mergeMessageFieldText": git.CommitMessageMerge(defaultSquashMergeCommitMessages, defaultSquashMergeBody),
"hideAutoMerge": generalHideAutoMerge,
},
map[string]any{
+8 -2
View File
@@ -67,7 +67,7 @@ func prepareHomeSidebarRepoTopics(ctx *context.Context) {
ctx.Data["Topics"] = topics
}
func prepareOpenWithEditorApps(ctx *context.Context) {
func prepareClonePanel(ctx *context.Context) {
var tmplApps []map[string]any
apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
for _, app := range apps {
@@ -93,6 +93,12 @@ func prepareOpenWithEditorApps(ctx *context.Context) {
})
}
ctx.Data["OpenWithEditorApps"] = tmplApps
if !setting.Repository.DisableDownloadSourceArchives {
// FIXME: here it only uses the shortname in the ref to build the link, it can't distinguish the branch/tag/commit with the same name
// in the future, it's better to use something like "/archive/branch/the-name.zip", "/archive/tag/the-name.zip" */}}
ctx.Data["DownloadArchiveLinkPrefix"] = ctx.Repo.RepoLink + "/archive/" + util.PathEscapeSegments(ctx.Repo.RefFullName.ShortName())
}
}
func prepareHomeSidebarCitationFile(entry *git.TreeEntry) func(ctx *context.Context) {
@@ -439,7 +445,7 @@ func Home(ctx *context.Context) {
isTreePathRoot := ctx.Repo.TreePath == ""
prepareFuncs := []func(*context.Context){
prepareOpenWithEditorApps,
prepareClonePanel,
prepareHomeSidebarRepoTopics,
checkOutdatedBranch,
prepareToRenderDirOrFile(entry),
+1 -23
View File
@@ -589,6 +589,7 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
ctx.Repo.RepoLink = repo.Link()
ctx.Data["RepoLink"] = ctx.Repo.RepoLink
ctx.Data["FeedURL"] = ctx.Repo.RepoLink
ctx.Data["CloneButtonOriginLink"] = repo.CloneLink(ctx, ctx.Doer) // CloneButtonOriginLink may be rewritten to the WikiCloneLink by the router middleware
unit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeExternalTracker)
if err == nil {
@@ -643,18 +644,6 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
// If multiple forks are available or if the user can fork to another account, but there is already a fork: open selection dialog
ctx.Data["ShowForkModal"] = len(userAndOrgForks) > 1 || (canSignedUserFork && len(userAndOrgForks) > 0)
ctx.Data["RepoCloneLink"] = repo.CloneLink(ctx, ctx.Doer)
cloneButtonShowHTTPS := !setting.Repository.DisableHTTPGit
cloneButtonShowSSH := !setting.SSH.Disabled && (ctx.IsSigned || setting.SSH.ExposeAnonymous)
if !cloneButtonShowHTTPS && !cloneButtonShowSSH {
// We have to show at least one link, so we just show the HTTPS
cloneButtonShowHTTPS = true
}
ctx.Data["CloneButtonShowHTTPS"] = cloneButtonShowHTTPS
ctx.Data["CloneButtonShowSSH"] = cloneButtonShowSSH
ctx.Data["CloneButtonOriginLink"] = ctx.Data["RepoCloneLink"] // it may be rewritten to the WikiCloneLink by the router middleware
ctx.Data["RepoSearchEnabled"] = setting.Indexer.RepoIndexerEnabled
if setting.Indexer.RepoIndexerEnabled {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
@@ -778,16 +767,6 @@ func repoAssignmentPrepareRepoTransfer(ctx *Context, data *repoAssignmentPrepare
}
}
func repoAssignmentHandleGoGet(ctx *Context, data *repoAssignmentPrepareDataStruct) {
repo := data.repo
if ctx.FormString("go-get") == "1" {
ctx.Data["GoGetImport"] = ComposeGoGetImport(ctx, repo.Owner.Name, repo.Name)
fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}"
ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}"
}
}
// RepoAssignment returns a middleware to handle repository assignment
func RepoAssignment(ctx *Context) {
repoAssignmentPreCheck(ctx)
@@ -804,7 +783,6 @@ func RepoAssignment(ctx *Context) {
repoAssignmentPrepareRepoTransfer,
repoAssignmentPrepareBranches,
repoAssignmentPreparePullRequests,
repoAssignmentHandleGoGet,
}
for _, f := range funcs {
f(ctx, prepareData)
+8 -15
View File
@@ -777,30 +777,25 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
}
// GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string {
func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) (_ string, err error) {
if err := pr.LoadIssue(ctx); err != nil {
log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
return ""
return "", err
}
if err := pr.Issue.LoadPoster(ctx); err != nil {
log.Error("Cannot load poster %d for pr id %d, index %d Error: %v", pr.Issue.PosterID, pr.ID, pr.Index, err)
return ""
return "", err
}
if pr.HeadRepo == nil {
var err error
pr.HeadRepo, err = repo_model.GetRepositoryByID(ctx, pr.HeadRepoID)
if err != nil {
log.Error("GetRepositoryByIdCtx[%d]: %v", pr.HeadRepoID, err)
return ""
return "", err
}
}
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo)
if err != nil {
log.Error("Unable to open head repository: Error: %v", err)
return ""
return "", err
}
defer closer.Close()
@@ -810,8 +805,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
} else {
pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitHeadRefName())
if err != nil {
log.Error("Unable to get head commit: %s Error: %v", pr.GetGitHeadRefName(), err)
return ""
return "", err
}
headCommitRef = git.RefNameFromCommit(pr.HeadCommitID)
}
@@ -822,8 +816,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
limitedCommits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, limit)
if err != nil {
log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
return ""
return "", err
}
mergeMessage := strings.TrimSpace(pr.Issue.Content) // use PR's title and description as squash commit message
@@ -831,7 +824,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
mergeMessage = formatSquashMergeCommitMessages(limitedCommits) // use PR's commit messages as squash commit message
}
coAuthors := collectSquashMergeCommitCoAuthors(ctx, gitRepo, pr, headCommitRef, mergeBaseRef, limit, limitedCommits)
return buildSquashMergeCommitMessages(mergeMessage, coAuthors)
return buildSquashMergeCommitMessages(mergeMessage, coAuthors), nil
}
func buildSquashMergeCommitMessages(mergeMessage string, coAuthors []string) string {
+1 -1
View File
@@ -84,7 +84,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir
return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err)
}
cloneLink := repo.CloneLink(ctx, nil /* no doer so do not generate user-related SSH link */)
cloneLink := repo.CloneLinkGeneral(ctx)
match := map[string]string{
"Name": repo.Name,
"Description": util.NormalizeStringEOL(repo.Description),
-4
View File
@@ -9,10 +9,6 @@
<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
<meta name="keywords" content="{{MetaKeywords}}">
<meta name="referrer" content="same-origin">{{/* required by: 1. "redirect_to" cookie; 2. cross-origin protection */}}
{{if .GoGetImport}}
<meta name="go-import" content="{{.GoGetImport}} git {{.RepoCloneLink.HTTPS}}">
<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}">
{{end}}
{{if and .EnableFeed .FeedURL}}
<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom">
<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss">
+10 -7
View File
@@ -1,13 +1,16 @@
<!-- there is always at least one button (guaranteed by context/repo.go) -->
<div class="ui action small input clone-buttons-combo">
{{if $.CloneButtonShowHTTPS}}
<button class="ui small button repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">HTTPS</button>
<!-- render the clone combo only when a git protocol is available; the URL input would otherwise be empty -->
{{$cloneLink := $.CloneButtonOriginLink}}
{{if or $cloneLink.SupportHTTPS $cloneLink.SupportSSH}}
<div class="ui action small input clone-buttons-combo" data-global-init="initRepoCloneButtonsCombo">
{{if $cloneLink.SupportHTTPS}}
<button class="ui small button repo-clone-https" data-link="{{$cloneLink.HTTPS}}">HTTPS</button>
{{end}}
{{if $.CloneButtonShowSSH}}
<button class="ui small button repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">SSH</button>
{{if $cloneLink.SupportSSH}}
<button class="ui small button repo-clone-ssh" data-link="{{$cloneLink.SSH}}">SSH</button>
{{end}}
<input size="10" class="repo-clone-url js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
<input size="10" class="repo-clone-url js-clone-url" value="{{Iif $cloneLink.SupportHTTPS $cloneLink.HTTPS $cloneLink.SSH}}" readonly>
<button class="ui small icon button" data-clipboard-target=".repo-clone-url" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">
{{svg "octicon-copy" 14}}
</button>
</div>
{{end}}
+44 -36
View File
@@ -1,48 +1,56 @@
<button class="ui compact primary button js-btn-clone-panel">
{{$cloneLink := $.CloneButtonOriginLink}}
{{$downloadArchiveLinkPrefix := $.DownloadArchiveLinkPrefix}}
{{$openWithEditorApps := $.OpenWithEditorApps}}
{{$showCloneLinks := or $cloneLink.SupportHTTPS $cloneLink.SupportSSH}}
{{$showOpenWithEditorApps := and $showCloneLinks $openWithEditorApps}}{{/* the editor apps need the clone link */}}
{{if or $showCloneLinks $showOpenWithEditorApps $downloadArchiveLinkPrefix}}
<button class="ui compact primary button" data-global-init="initRepoClonePanel">
{{svg "octicon-code" 16}}
<span>{{ctx.Locale.Tr "repo.code"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button>
<div class="clone-panel-popup tippy-target">
<div class="flex-text-block clone-panel-field">{{svg "octicon-terminal"}} Clone</div>
<div class="clone-panel-tab">
<!-- there is always at least one button (guaranteed by context/repo.go) -->
{{if $.CloneButtonShowHTTPS}}
<button class="item repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">HTTPS</button>
{{end}}
{{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>
<div class="clone-panel-field">
<div class="ui input tiny action">
<input size="30" class="repo-clone-url js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
<div class="ui small compact icon button" data-clipboard-target=".js-clone-url" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">
{{svg "octicon-copy" 14}}
</div>
</div>
</div>
{{if not .PageIsWiki}}
<div class="flex-items-block clone-panel-list">
{{range .OpenWithEditorApps}}
<a class="item muted js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
{{if $showCloneLinks}}
<div class="flex-text-block clone-panel-field">{{svg "octicon-terminal"}} Clone</div>
<div class="clone-panel-tab">
<!-- tea clone also uses the git HTTPS/SSH transports, so the whole clone section is hidden when both are disabled -->
{{if $cloneLink.SupportHTTPS}}
<button class="item repo-clone-https" data-link="{{$cloneLink.HTTPS}}">HTTPS</button>
{{end}}
{{if $cloneLink.SupportSSH}}
<button class="item repo-clone-ssh" data-link="{{$cloneLink.SSH}}">SSH</button>
{{end}}
{{if not $cloneLink.IsWikiRepo}}
<button class="item repo-clone-tea" data-link="{{$cloneLink.Tea}}">Tea CLI</button>
{{end}}
</div>
{{if and (not $.DisableDownloadSourceArchives) $.RefFullName}}
<div class="divider"></div>
<div class="clone-panel-field">
<div class="ui input tiny action">
<input size="30" class="repo-clone-url js-clone-url" value="{{Iif $cloneLink.SupportHTTPS $cloneLink.HTTPS $cloneLink.SSH}}" readonly>
<div class="ui small compact icon button" data-clipboard-target=".js-clone-url" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">
{{svg "octicon-copy" 14}}
</div>
</div>
</div>
{{end}}
{{if $showOpenWithEditorApps}}
<div class="flex-items-block clone-panel-list repo-clone-with-apps">
{{range $app := $openWithEditorApps}}
<a class="item muted js-clone-url-editor" data-href-template="{{$app.OpenURL}}">{{$app.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" $app.DisplayName}}</a>
{{end}}
</div>
{{end}}
{{if $downloadArchiveLinkPrefix}}
{{if $showOpenWithEditorApps}}<div class="divider"></div>{{end}}
<div class="flex-items-block clone-panel-list">
{{/* FIXME: here it only uses the shortname in the ref to build the link, it can't distinguish the branch/tag/commit with the same name
in the future, it's better to use something like "/archive/branch/the-name.zip", "/archive/tag/the-name.zip" */}}
<a class="item muted archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefFullName.ShortName}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} {{ctx.Locale.Tr "repo.download_zip"}}</a>
<a class="item muted archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefFullName.ShortName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} {{ctx.Locale.Tr "repo.download_tar"}}</a>
<a class="item muted archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefFullName.ShortName}}.bundle" rel="nofollow">{{svg "octicon-package"}} {{ctx.Locale.Tr "repo.download_bundle"}}</a>
<a class="item muted archive-link" href="{{$downloadArchiveLinkPrefix}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} {{ctx.Locale.Tr "repo.download_zip"}}</a>
<a class="item muted archive-link" href="{{$downloadArchiveLinkPrefix}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} {{ctx.Locale.Tr "repo.download_tar"}}</a>
<a class="item muted archive-link" href="{{$downloadArchiveLinkPrefix}}.bundle" rel="nofollow">{{svg "octicon-package"}} {{ctx.Locale.Tr "repo.download_bundle"}}</a>
</div>
{{end}}
{{end}}
</div>
{{end}}
+11 -8
View File
@@ -21,7 +21,7 @@
<div class="ui segment center">{{ctx.Locale.Tr "repo.no_branch"}}</div>
{{else if .CanWriteCode}}
<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.quick_guide"}}</h4>
<div class="ui attached guide table segment empty-repo-guide">
<div class="ui attached segment empty-repo-guide flex-relaxed-list">
<div class="item">
<h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository"}}</small></h3>
@@ -36,12 +36,15 @@
</a>
{{end}}
{{end}}
{{template "repo/clone_buttons" .}}
{{template "repo/clone_buttons" dict "CloneButtonOriginLink" $.CloneButtonOriginLink}}
</div>
</div>
{{if not .Repository.IsArchived}}
<div class="divider tw-my-0"></div>
{{$cloneLink := $.CloneButtonOriginLink}}
{{$showGitClientCommands := and (or $cloneLink.SupportHTTPS $cloneLink.SupportSSH) (not .Repository.IsArchived)}}
{{/* TODO: when both HTTPS and SSH are disabled, the UI is not that good */}}
{{if $showGitClientCommands}}
<div class="divider"></div>
<div class="item">
<h3>{{ctx.Locale.Tr "repo.create_new_repo_command"}}</h3>
@@ -52,19 +55,19 @@ git init{{if ne .Repository.ObjectFormatName "sha1"}} --object-format={{.Reposit
{{if ne .Repository.DefaultBranch "master"}}git checkout -b {{.Repository.DefaultBranch}}{{end}}
git add README.md
git commit -m "first commit"
git remote add {{$gitRemoteName}} <span class="js-clone-url">{{$.CloneButtonOriginLink.HTTPS}}</span>
git remote add {{$gitRemoteName}} <span class="js-clone-url">{{Iif $cloneLink.SupportHTTPS $cloneLink.HTTPS $cloneLink.SSH}}</span>
git push -u {{$gitRemoteName}} {{.Repository.DefaultBranch}}</code></pre>
</div>
</div>
<div class="divider"></div>
<div class="item">
<h3>{{ctx.Locale.Tr "repo.push_exist_repo"}}</h3>
<div class="markup">
<pre><code>git remote add {{$gitRemoteName}} <span class="js-clone-url">{{$.CloneButtonOriginLink.HTTPS}}</span>
<pre><code>git remote add {{$gitRemoteName}} <span class="js-clone-url">{{Iif $cloneLink.SupportHTTPS $cloneLink.HTTPS $cloneLink.SSH}}</span>
git push -u {{$gitRemoteName}} {{.Repository.DefaultBranch}}</code></pre>
</div>
</div>
{{else}}
<div class="item">HTTPS and SSH clones are disabled, you can only modify the repository via Gitea's web UI.</div>
{{end}}
</div>
{{else}}
+5 -1
View File
@@ -112,7 +112,11 @@
{{end}}
<!-- Only show clone panel in repository home page -->
{{if $isTreePathRoot}}
{{template "repo/clone_panel" .}}
{{template "repo/clone_panel" (dict
"CloneButtonOriginLink" $.CloneButtonOriginLink
"OpenWithEditorApps" $.OpenWithEditorApps
"DownloadArchiveLinkPrefix" $.DownloadArchiveLinkPrefix
)}}
{{end}}
{{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
<a class="ui compact button" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
+1 -1
View File
@@ -15,7 +15,7 @@
</div>
</div>
<div class="flex-text-block">
{{template "repo/clone_panel" .}}
{{template "repo/clone_panel" dict "CloneButtonOriginLink" $.CloneButtonOriginLink}}
</div>
</div>
<h2 class="ui top header">{{ctx.Locale.Tr "repo.wiki.wiki_page_revisions"}}</h2>
+1 -1
View File
@@ -28,7 +28,7 @@
</div>
</div>
</div>
{{template "repo/clone_panel" .}}
{{template "repo/clone_panel" dict "CloneButtonOriginLink" $.CloneButtonOriginLink}}
</div>
<div class="ui dividing header">
<div class="flex-text-block tw-flex-wrap tw-justify-end">
+2 -1
View File
@@ -1321,7 +1321,8 @@ Co-authored-by: user4 <user4@example.com>
pullIndex, err := strconv.ParseInt(elems[4], 10, 64)
assert.NoError(t, err)
pullRequest := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, Index: pullIndex})
squashMergeCommitMessage := pull_service.GetSquashMergeCommitMessages(t.Context(), pullRequest)
squashMergeCommitMessage, err := pull_service.GetSquashMergeCommitMessages(t.Context(), pullRequest)
assert.NoError(t, err)
assert.Equal(t, tc.expectedMessage, squashMergeCommitMessage)
})
}
+23 -28
View File
@@ -1,4 +1,4 @@
import {queryElems} from '../utils/dom.ts';
import {queryElems, toggleElem} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
@@ -7,6 +7,7 @@ import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue';
import {createApp} from 'vue';
import {createTippy} from '../modules/tippy.ts';
import {localUserSettings} from '../modules/user-settings.ts';
import {registerGlobalInitFunc} from '../modules/observer.ts';
async function onDownloadArchive(e: Event) {
e.preventDefault();
@@ -51,32 +52,33 @@ export function substituteRepoOpenWithUrl(tmpl: string, url: string): string {
return tmpl.replace('{url}', needEncode ? encodeURIComponent(url) : url);
}
function initCloneSchemeUrlSelection(parent: Element) {
const elCloneUrlInput = parent.querySelector<HTMLInputElement>('.repo-clone-url')!;
function initRepoCloneButtonsCombo(parent: Element) {
// the clone section is not rendered at all when no git transport (HTTPS/SSH) is available
const elCloneUrlInput = parent.querySelector<HTMLInputElement>('.repo-clone-url');
if (!elCloneUrlInput) return;
const tabHttps = parent.querySelector('.repo-clone-https');
const tabSsh = parent.querySelector('.repo-clone-ssh');
const tabTea = parent.querySelector('.repo-clone-tea');
const listOpenWithEditorApps = parent.querySelector('.repo-clone-with-apps');
// not every tab exists in every panel, eg: the admin may disable HTTP/SSH, and the empty repo page has no Tea CLI tab
const tabByScheme: Record<string, Element | null> = {https: tabHttps, ssh: tabSsh, tea: tabTea};
const updateClonePanelUi = function() {
let scheme = localUserSettings.getString('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';
// fall back to the first available tab when the preferred scheme's tab is absent (unset preference, or disabled protocol)
if (!tabByScheme[scheme]) {
scheme = ['https', 'ssh', 'tea'].find((s) => tabByScheme[s]) ?? '';
}
const isHttps = scheme === 'https';
const isSsh = scheme === 'ssh';
const isTea = scheme === 'tea';
if (listOpenWithEditorApps) {
toggleElem(listOpenWithEditorApps, !isTea); // don't show the "Open with editor apps" list when "Tea" clone is selected
}
if (tabHttps) {
const link = tabHttps.getAttribute('data-link')!;
tabHttps.textContent = link.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS"
@@ -89,16 +91,9 @@ function initCloneSchemeUrlSelection(parent: Element) {
tabTea.classList.toggle('active', isTea);
}
let tab: Element | null = null;
if (isHttps) {
tab = tabHttps;
} else if (isSsh) {
tab = tabSsh;
} else if (isTea) {
tab = tabTea;
}
const tab = tabByScheme[scheme];
if (!tab) return; // no protocol available at all, leave the (hidden) input untouched
if (!tab) return;
const link = tab.getAttribute('data-link')!;
for (const el of document.querySelectorAll('.js-clone-url')) {
@@ -132,10 +127,10 @@ function initCloneSchemeUrlSelection(parent: Element) {
});
}
function initClonePanelButton(btn: HTMLButtonElement) {
function initRepoClonePanel(btn: HTMLButtonElement) {
const elPanel = btn.nextElementSibling!;
// "init" must be before the "createTippy" otherwise the "tippy-target" will be removed from the document
initCloneSchemeUrlSelection(elPanel);
initRepoCloneButtonsCombo(elPanel);
createTippy(btn, {
content: elPanel,
trigger: 'click',
@@ -147,8 +142,8 @@ function initClonePanelButton(btn: HTMLButtonElement) {
}
export function initRepoCloneButtons() {
queryElems(document, '.js-btn-clone-panel', initClonePanelButton);
queryElems(document, '.clone-buttons-combo', initCloneSchemeUrlSelection);
registerGlobalInitFunc('initRepoClonePanel', initRepoClonePanel);
registerGlobalInitFunc('initRepoCloneButtonsCombo', initRepoCloneButtonsCombo);
}
export async function updateIssuesMeta(url: string, action: string, issue_ids: string, id: string) {