diff --git a/.github/workflows/cron-renovate.yml b/.github/workflows/cron-renovate.yml index 4db83a336d..843527ba20 100644 --- a/.github/workflows/cron-renovate.yml +++ b/.github/workflows/cron-renovate.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: renovatebot/github-action@693b9ef15eec82123529a37c782242f091365961 # v46.1.14 + - uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15 with: renovate-version: ${{ env.RENOVATE_VERSION }} configurationFile: renovate.json5 diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 4cc8d25bbb..a35937f266 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -131,7 +131,7 @@ jobs: ports: - "7700:7700" redis: - image: redis:latest@sha256:e74c9b933d78e2829583d88f92793f4524752a15ac59c8baff2dd5ed000b7432 + image: redis:latest@sha256:a505f8b9d8ac3ff7b0848055b4abf1901d6d77606774aa1e38bd37f1197ed2b5 options: >- # wait until redis has started --health-cmd "redis-cli ping" --health-interval 5s diff --git a/Makefile b/Makefile index 939264233f..86d21dcf44 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-che GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.0 # renovate: datasource=go +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.1 # renovate: datasource=go XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go diff --git a/go.mod b/go.mod index a5f42c6caf..9d23a99410 100644 --- a/go.mod +++ b/go.mod @@ -105,6 +105,7 @@ require ( go.yaml.in/yaml/v4 v4.0.0-rc.5 golang.org/x/crypto v0.53.0 golang.org/x/image v0.42.0 + golang.org/x/mod v0.37.0 golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.21.0 @@ -267,7 +268,6 @@ require ( go.uber.org/zap/exp v0.3.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect - golang.org/x/mod v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect diff --git a/modules/git/commit_message.go b/modules/git/commit_message.go index 8fd3601f0d..0729609d87 100644 --- a/modules/git/commit_message.go +++ b/modules/git/commit_message.go @@ -70,9 +70,11 @@ 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.*?)(?P^|^\n|^-{3,}\n|\n-{3,}\n|\n\n)(?P(?:[A-Za-z0-9][-A-Za-z0-9]*:[^\n]*\n?)*)$`) + return regexp.MustCompile(`(?s)^(?P.*?)(?P^|^\n|^-{3,}\n+|\n-{3,}\n+|\n\n)(?P(?:[A-Za-z0-9][-A-Za-z0-9]*:[^\n]*\n?)*\n*)$`) }) +// CommitMessageSplitTrailer tries to split the message by the trailer separator +// content + sep + trailer will reconstruct the original message func CommitMessageSplitTrailer(s string) (content, sep, trailer string) { s = util.NormalizeStringEOL(s) re := commitMessageTrailerSplit() diff --git a/modules/git/commit_message_test.go b/modules/git/commit_message_test.go index 049f1c03f7..1d13f81803 100644 --- a/modules/git/commit_message_test.go +++ b/modules/git/commit_message_test.go @@ -26,8 +26,10 @@ 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\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"}, {"k: v", "", "", "k: v"}, {"\nk:v", "", "\n", "k:v"}, diff --git a/modules/packages/goproxy/metadata.go b/modules/packages/goproxy/metadata.go index b19738db56..db50ac7faa 100644 --- a/modules/packages/goproxy/metadata.go +++ b/modules/packages/goproxy/metadata.go @@ -10,6 +10,8 @@ import ( "strings" "gitea.dev/modules/util" + + "golang.org/x/mod/semver" ) const ( @@ -20,6 +22,7 @@ const ( var ( ErrInvalidStructure = util.NewInvalidArgumentErrorf("package has invalid structure") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") ErrGoModFileTooLarge = util.NewInvalidArgumentErrorf("go.mod file is too large") ) @@ -54,6 +57,13 @@ func ParsePackage(r io.ReaderAt, size int64) (*Package, error) { Name: strings.TrimSuffix(nameAndVersion, "@"+parts[1]), Version: versionParts[0], } + + // the version is taken verbatim from the zip path and later written + // one per line into the @v/list proxy response, so it has to be a + // valid module version (no newlines or other stray characters) + if !semver.IsValid(p.Version) { + return nil, ErrInvalidVersion + } } if len(versionParts) > 1 { diff --git a/modules/packages/goproxy/metadata_test.go b/modules/packages/goproxy/metadata_test.go index 4e7f394f8b..24db359651 100644 --- a/modules/packages/goproxy/metadata_test.go +++ b/modules/packages/goproxy/metadata_test.go @@ -59,6 +59,16 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, "module gitea.com/go-gitea/gitea", p.GoMod) }) + t.Run("InvalidVersion", func(t *testing.T) { + data := createArchive(map[string][]byte{ + packageName + "@v1.0.0\nv99.0.0/go.mod": []byte("module " + packageName), + }) + + p, err := ParsePackage(data, int64(data.Len())) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + }) + t.Run("Valid", func(t *testing.T) { data := createArchive(map[string][]byte{ packageName + "@" + packageVersion + "/subdir/go.mod": []byte("invalid"), diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 9bff2c24be..c1e11779d1 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -2205,10 +2205,10 @@ "repo.settings.trust_model.collaborator.desc": "Déanfar sínithe bailí ó chomhoibritheoirí an stórais seo a mharcáil mar \"iontaofa\", cibé acu a mheaitseálann siad an tiomnóir nó nach meaitseálann. Seachas sin, déanfar sínithe bailí a mharcáil mar \"neamhiontaofa\" má mheaitseálann an síniú an tiomnóir agus \"gan mheaitseáil\" mura bhfuil.", "repo.settings.trust_model.committer": "Coimisitheoir", "repo.settings.trust_model.committer.long": "Tiomnaithe: Sínithe muiníne a mheaitseálann tiomnóirí. Meaitseálann sé seo iompar GitHub agus cuirfidh sé iallach ar thiomnóirí atá sínithe ag Gitea Gitea a bheith mar an tiomnóir.", - "repo.settings.trust_model.committer.desc": "Ní mharcálfar sínithe bailí mar \"iontaofa\" ach amháin má mheaitseálann siad an tiomnaí, nó marcálfar iad mar \"gan mheaitseáil\". Cuireann sé seo iallach ar Gitea a bheith ina tiomnaí ar thiomnuithe sínithe, agus an tiomnaí iarbhír marcáilte mar Chomhúdaraithe ag: agus Co-thiomnaithe ag: leantóir sa tiomnú. Caithfidh eochair réamhshocraithe Gitea a bheith ag teacht le húsáideoir sa bhunachar sonraí.", + "repo.settings.trust_model.committer.desc": "Ní mharcálfar sínithe bailí mar \"iontaofa\" ach amháin má mheaitseálann siad an tiomnóir, nó marcálfar iad mar \"gan mheaitseáil\". Cuireann sé seo iallach ar Gitea a bheith ina thiomnóir ar thiomnuithe sínithe, agus an tiomnóir iarbhír marcáilte mar leantóir Co-authored-by: sa thiomnú. Caithfidh eochair réamhshocraithe Gitea a bheith ag teacht le húsáideoir sa bhunachar sonraí.", "repo.settings.trust_model.collaboratorcommitter": "Comhoibritheo+Coimiteoir", "repo.settings.trust_model.collaboratorcommitter.long": "Comhoibrí+Coiste: sínithe muiníne ó chomhoibrithe a mheaitseálann an tiomnóir", - "repo.settings.trust_model.collaboratorcommitter.desc": "Marcálfar sínithe bailí ó chomhoibritheoirí an stórais seo mar \"iontaofa\" má mheaitseálann siad an tiomnaí. Seachas sin, marcálfar sínithe bailí mar \"neamhiontaofa\" má mheaitseálann an síniú an tiomnaí agus \"gan mheaitseáil\" murach sin. Cuirfidh sé seo iallach ar Gitea a bheith marcáilte mar an tiomnaí ar thiomnuithe sínithe, agus an tiomnaí iarbhír marcáilte mar Chomhúdaraithe ag: agus Co-Tiomnaithe ag: leantóir sa tiomnú. Ní mór don eochair réamhshocraithe Gitea a bheith ag teacht le húsáideoir sa bhunachar sonraí.", + "repo.settings.trust_model.collaboratorcommitter.desc": "Marcálfar sínithe bailí ó chomhoibritheoirí an stórais seo mar \"iontaofa\" má mheaitseálann siad an tiomnóir. Seachas sin, marcálfar sínithe bailí mar \"neamhiontaofa\" má mheaitseálann an síniú an tiomnóir agus \"gan mheaitseáil\" murach sin. Cuirfidh sé seo iallach ar Gitea a bheith marcáilte mar an tiomnóir ar thiomnuithe sínithe, agus an tiomnóir iarbhír marcáilte mar leantóir Co-Authored-By: sa tiomnú. Ní mór don eochair réamhshocraithe Gitea a bheith ag teacht le húsáideoir sa bhunachar sonraí.", "repo.settings.wiki_delete": "Scrios Sonraí Vicí", "repo.settings.wiki_delete_desc": "Tá sonraí wiki stóras a scriosadh buan agus ní féidir iad a chur ar ais.", "repo.settings.wiki_delete_notices_1": "- Scriosfaidh agus díchumasóidh sé seo an stóras vicí do %s go buan.", @@ -2599,6 +2599,9 @@ "repo.diff.review.reject": "Iarr athruithe", "repo.diff.review.self_approve": "Ní féidir le húdair iarratais tarraing a n-iarratas tarraingthe féin a chead", "repo.diff.committed_by": "tiomanta ag", + "repo.diff.coauthored_by": "comhúdaraithe ag", + "repo.commits.avatar_stack_and": "agus", + "repo.commits.avatar_stack_people": "%d duine", "repo.diff.protected": "Cosanta", "repo.diff.image.side_by_side": "Taobh le Taobh", "repo.diff.image.swipe": "Scaoil", @@ -2862,6 +2865,14 @@ "org.teams.all_repositories_read_permission_desc": "Tugann an fhoireann seo rochtain do Léamh ar gach stórais: is féidir le baill amharc ar stórais agus iad a chlónáil.", "org.teams.all_repositories_write_permission_desc": "Tugann an fhoireann seo rochtain do Scríobh ar gach stórais: is féidir le baill léamh ó stórais agus iad a bhrú chucu.", "org.teams.all_repositories_admin_permission_desc": "Tugann an fhoireann seo rochtain Riarthóra ar gach stóras: is féidir le comhaltaí léamh, brú a dhéanamh agus comhoibritheoirí a chur le stórtha.", + "org.teams.visibility": "Infheictheacht", + "org.teams.visibility_private": "Príobháideach", + "org.teams.visibility_private_helper": "Le feiceáil ag baill foirne agus úinéirí eagraíochta amháin.", + "org.teams.visibility_limited": "Teoranta", + "org.teams.visibility_limited_helper": "Infheicthe ag gach ball den eagraíocht seo.", + "org.teams.visibility_public": "Poiblí", + "org.teams.visibility_public_helper": "Infheicthe ag aon úsáideoir atá sínithe isteach.", + "org.teams.owners_visibility_fixed": "Ní féidir infheictheacht fhoireann na nÚinéirí a athrú.", "org.teams.invite.title": "Tugadh cuireadh duit dul isteach i bhfoireann %s san eagraíocht %s.", "org.teams.invite.by": "Ar cuireadh ó %s", "org.teams.invite.description": "Cliceáil ar an gcnaipe thíos le do thoil chun dul isteach san fhoireann.", @@ -3774,6 +3785,7 @@ "actions.runs.no_matching_online_runner_helper": "Gan aon reathaí ar líne a mheaitseáil le lipéad: %s", "actions.runs.no_job_without_needs": "Caithfidh post amháin ar a laghad a bheith sa sreabhadh oibre gan spleáchas.", "actions.runs.no_job": "Caithfidh post amháin ar a laghad a bheith sa sreabhadh oibre", + "actions.runs.invalid_reusable_workflow_uses": "Sreabhadh oibre in-athúsáidte neamhbhailí \"úsáidí\": %s", "actions.runs.actor": "Aisteoir", "actions.runs.status": "Stádas", "actions.runs.actors_no_select": "Gach aisteoir", @@ -3794,13 +3806,17 @@ "actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre", "actions.runs.summary": "Achoimre", "actions.runs.all_jobs": "Gach post", + "actions.runs.job_summaries": "Achoimrí poist", "actions.runs.expand_caller_jobs": "Taispeáin poist an ghlaoiteora sreabha oibre in-athúsáidte seo", "actions.runs.collapse_caller_jobs": "Folaigh poist an ghlaoiteora sreabha oibre in-athúsáidte seo", "actions.runs.attempt": "Iarracht", "actions.runs.latest": "Is déanaí", "actions.runs.latest_attempt": "An iarracht is déanaí", "actions.runs.triggered_via": "Spreagtha trí %s", - "actions.runs.total_duration": "Fad iomlán:", + "actions.runs.rerun_triggered": "Athrith spreagtha", + "actions.runs.back_to_pull_request": "Ar ais chuig an iarratas tarraingthe", + "actions.runs.back_to_workflow": "Ar ais chuig an sreabhadh oibre", + "actions.runs.total_duration": "Fad iomlán", "actions.runs.workflow_dependencies": "Spleáchais ar Shreabhadh Oibre", "actions.runs.graph_jobs_count_1": "%d post", "actions.runs.graph_jobs_count_n": "%d poist", diff --git a/package.json b/package.json index d6fc24a21f..ff7f8f1be0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "packageManager": "pnpm@11.5.2", + "packageManager": "pnpm@11.5.3", "engines": { "node": ">= 22.18.0", "pnpm": ">= 11.0.0" diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 081ea5e91b..ddd5a9bb21 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -11,6 +11,7 @@ import ( api "gitea.dev/modules/structs" "gitea.dev/services/context" "gitea.dev/services/convert" + git_service "gitea.dev/services/git" ) // CompareDiff compare two branches or commits @@ -18,8 +19,12 @@ func CompareDiff(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} repository repoCompareDiff // --- // summary: Get commit comparison information + // description: | + // By default returns JSON commit comparison information. The raw diff or patch can be + // requested with the `output` query parameter set to `diff` or `patch` respectively. // produces: // - application/json + // - text/plain // parameters: // - name: owner // in: path @@ -33,9 +38,16 @@ func CompareDiff(ctx *context.APIContext) { // required: true // - name: basehead // in: path - // description: compare two branches or commits + // description: compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs, including branch names that contain slashes. // type: string // required: true + // - name: output + // in: query + // description: return the raw comparison as `diff` or `patch` instead of JSON + // type: string + // enum: + // - diff + // - patch // responses: // "200": // "$ref": "#/responses/Compare" @@ -57,6 +69,16 @@ func CompareDiff(ctx *context.APIContext) { } defer closer() + // ?output=diff|patch returns the raw output, otherwise the JSON comparison is returned. + switch ctx.FormString("output") { + case "diff": + downloadCompareDiffOrPatch(ctx, compareInfo, false) + return + case "patch": + downloadCompareDiffOrPatch(ctx, compareInfo, true) + return + } + verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") files := ctx.FormString("files") == "" || ctx.FormBool("files") @@ -88,3 +110,20 @@ func CompareDiff(ctx *context.APIContext) { Commits: apiCommits, }) } + +// downloadCompareDiffOrPatch writes a comparison's raw diff or patch to the response. +func downloadCompareDiffOrPatch(ctx *context.APIContext, compareInfo *git_service.CompareInfo, patch bool) { + ctx.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8") + compareArg := compareInfo.BaseCommitID + compareInfo.CompareSeparator + compareInfo.HeadCommitID + + var err error + if patch { + err = compareInfo.HeadGitRepo.GetPatch(compareArg, ctx.Resp) + } else { + err = compareInfo.HeadGitRepo.GetDiff(compareArg, ctx.Resp) + } + if err != nil { + ctx.APIErrorInternal(err) + return + } +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 45735fc8fe..5a9fa1c1d1 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -201,12 +201,12 @@ func newComparePageInfo() *comparePageInfoType { } // parseCompareInfo parse compare info between two commit for preparing comparing references -func (cpi *comparePageInfoType) parseCompareInfo(ctx *context.Context) error { +func (cpi *comparePageInfoType) parseCompareInfo(ctx *context.Context, compareParam string) error { baseRepo := ctx.Repo.Repository fileOnly := ctx.FormBool("file-only") // 1 Parse compare router param - compareReq := common.ParseCompareRouterParam(ctx.PathParam("*")) + compareReq := common.ParseCompareRouterParam(compareParam) // remove the check when we support compare with carets if compareReq.BaseOriRefSuffix != "" { @@ -545,7 +545,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor // CompareDiff show different from one commit to another commit func CompareDiff(ctx *context.Context) { comparePageInfo := newComparePageInfo() - err := comparePageInfo.parseCompareInfo(ctx) + err := comparePageInfo.parseCompareInfo(ctx, ctx.PathParam("*")) if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { ctx.NotFound(nil) return @@ -605,6 +605,45 @@ func CompareDiff(ctx *context.Context) { ctx.HTML(http.StatusOK, tplCompare) } +// DownloadCompareDiff render a comparison's raw unified diff +func DownloadCompareDiff(ctx *context.Context) { + downloadCompareDiffOrPatch(ctx, false) +} + +// DownloadComparePatch render a comparison as a git format-patch +func DownloadComparePatch(ctx *context.Context) { + downloadCompareDiffOrPatch(ctx, true) +} + +func downloadCompareDiffOrPatch(ctx *context.Context, patch bool) { + // The route captures `basehead` separately so the `.diff`/`.patch` suffix is + // stripped from the catch-all `*` param parseCompareInfo would otherwise read. + cpi := newComparePageInfo() + if err := cpi.parseCompareInfo(ctx, ctx.PathParam("basehead")); err != nil { + if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { + ctx.NotFound(nil) + } else { + ctx.ServerError("ParseCompareInfo", err) + } + return + } + ci := cpi.compareInfo + + ctx.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8") + compareArg := ci.BaseCommitID + ci.CompareSeparator + ci.HeadCommitID + + var err error + if patch { + err = ci.HeadGitRepo.GetPatch(compareArg, ctx.Resp) + } else { + err = ci.HeadGitRepo.GetDiff(compareArg, ctx.Resp) + } + if err != nil { + ctx.ServerError("DownloadCompareDiffOrPatch", err) + return + } +} + func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Context) { ci := cpi.compareInfo if cpi.allowCreatePull { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 650f94110f..81283a2538 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1310,7 +1310,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateIssueForm) repo := ctx.Repo.Repository comparePageInfo := newComparePageInfo() - err := comparePageInfo.parseCompareInfo(ctx) + err := comparePageInfo.parseCompareInfo(ctx, ctx.PathParam("*")) if errors.Is(err, util.ErrNotExist) { ctx.JSONErrorNotFound() return diff --git a/routers/web/web.go b/routers/web/web.go index ee30b614c8..bde886177c 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1269,9 +1269,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeViewNodes) }) m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff) - m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists). - Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). - Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) + m.PathGroup("/compare/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/.diff", repo.MustBeNotEmpty, repo.DownloadCompareDiff) + g.MatchPath("GET", "/.patch", repo.MustBeNotEmpty, repo.DownloadComparePatch) + g.MatchPath("GET", "/<*:*>", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff) + g.MatchPath("POST", "/<*:*>", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) + }) m.Get("/pulls/new/*", repo.PullsNewRedirect) }, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{reponame}": repo code: find, compare, list diff --git a/services/pull/pull.go b/services/pull/pull.go index 4491406b33..13f8ffa8df 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -4,14 +4,15 @@ package pull import ( + "bytes" "context" "errors" "fmt" "io" - "regexp" "slices" "strings" "time" + "unicode" "unicode/utf8" "gitea.dev/models/db" @@ -767,8 +768,6 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re return errors.Join(errs...) } -var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`) - // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one) func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string { if err := pr.LoadIssue(ctx); err != nil { @@ -819,54 +818,44 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ return "" } - posterSig := pr.Issue.Poster.NewGitSig().String() + mergeMessage := strings.TrimSpace(pr.Issue.Content) // use PR's title and description as squash commit message + if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages { + 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) +} - uniqueAuthors := make(container.Set[string]) - authors := make([]string, 0, len(limitedCommits)) - stringBuilder := strings.Builder{} - - if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages { - // use PR's title and description as squash commit message - message := strings.TrimSpace(pr.Issue.Content) - stringBuilder.WriteString(message) - if stringBuilder.Len() > 0 { - stringBuilder.WriteRune('\n') - if !commitMessageTrailersPattern.MatchString(message) { - // TODO: this trailer check doesn't work with the separator line added below for the co-authors - stringBuilder.WriteRune('\n') - } - } - } else { - // use PR's commit messages as squash commit message - // commits list is in reverse chronological order - maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize - for _, commit := range slices.Backward(limitedCommits) { - msg := strings.TrimSpace(commit.MessageUTF8()) - if msg == "" { - continue - } - - // This format follows GitHub's squash commit message style, - // even if there are other "* " in the commit message body, they are written as-is. - // Maybe, ideally, we should indent those lines too. - _, _ = fmt.Fprintf(&stringBuilder, "* %s\n\n", msg) - if maxMsgSize > 0 && stringBuilder.Len() >= maxMsgSize { - tmp := stringBuilder.String() - wasValidUtf8 := utf8.ValidString(tmp) - tmp = tmp[:maxMsgSize] + "..." - if wasValidUtf8 { - // If the message was valid UTF-8 before truncation, ensure it remains valid after truncation - // For non-utf8 messages, we can't do much about it, end users should use utf-8 as much as possible - tmp = strings.ToValidUTF8(tmp, "") - } - stringBuilder.Reset() - stringBuilder.WriteString(tmp) - break - } - } +func buildSquashMergeCommitMessages(mergeMessage string, coAuthors []string) string { + if len(coAuthors) == 0 { + return mergeMessage } - // collect co-authors + msgContent, msgSep, msgTrailer := git.CommitMessageSplitTrailer(mergeMessage) + if (msgSep == "" || msgSep == "\n\n") && msgTrailer == "" { + msgContent = strings.TrimRightFunc(msgContent, unicode.IsSpace) + msgSep = "\n\n---------\n\n" + } + var sb strings.Builder + sb.WriteString(msgContent) + sb.WriteString(msgSep) + if msgTrailer = strings.TrimSpace(msgTrailer); msgTrailer != "" { + sb.WriteString(msgTrailer) + sb.WriteRune('\n') + } + for _, author := range coAuthors { + sb.WriteString(git.CoAuthoredByTrailer + ": ") + sb.WriteString(author) + sb.WriteRune('\n') + } + return sb.String() +} + +func collectSquashMergeCommitCoAuthors(ctx context.Context, gitRepo *git.Repository, pr *issues_model.PullRequest, headCommitRef, mergeBaseRef git.RefName, limitFirst int, limitedCommits []*git.Commit) []string { + posterSig := pr.Issue.Poster.NewGitSig().String() + uniqueAuthors := make(container.Set[string]) + authors := make([]string, 0, len(limitedCommits)) + for _, commit := range limitedCommits { authorString := commit.Author.String() if uniqueAuthors.Add(authorString) && authorString != posterSig { @@ -880,14 +869,14 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ } // collect the remaining authors - if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors { - skip := limit - limit = 30 + if limitFirst >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors { + skip := limitFirst + batchLimit := 30 for { - commits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, limit, skip) + commits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, batchLimit, skip) if err != nil { log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err) - return "" + return authors } if len(commits) == 0 { break @@ -901,22 +890,46 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ } } } - skip += limit + skip += batchLimit + } + } + return authors +} + +func formatSquashMergeCommitMessages(commits []*git.Commit) string { + maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize + sb := &bytes.Buffer{} + // commits list is in reverse chronological order + for _, commit := range slices.Backward(commits) { + msg := strings.TrimSpace(commit.MessageUTF8()) + if msg == "" { + continue + } + + // This format follows GitHub's squash commit message style, + // even if there are other "* " in the commit message body, they are written as-is. + // Maybe, ideally, we should indent those lines too. + _, _ = fmt.Fprintf(sb, "* %s\n\n", msg) + if maxMsgSize > 0 && sb.Len() >= maxMsgSize { + break } } - if stringBuilder.Len() > 0 && len(authors) > 0 { - // TODO: this separator line doesn't work with the trailer check (commitMessageTrailersPattern) above - stringBuilder.WriteString("---------\n\n") + buf := bytes.TrimSpace(sb.Bytes()) + if maxMsgSize > 0 && len(buf) > maxMsgSize { + buf = buf[:maxMsgSize] + for { + r, sz := utf8.DecodeLastRune(buf) + if r == utf8.RuneError && sz == 1 { + buf = buf[:len(buf)-1] + continue + } + break + } + buf = append(buf, '.', '.', '.') } - - for _, author := range authors { - stringBuilder.WriteString(git.CoAuthoredByTrailer + ": ") - stringBuilder.WriteString(author) - stringBuilder.WriteRune('\n') - } - - return stringBuilder.String() + buf = append(buf, '\n', '\n') + return util.UnsafeBytesToString(buf) } // GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go index 0648cd383d..554c9ed577 100644 --- a/services/pull/pull_test.go +++ b/services/pull/pull_test.go @@ -11,28 +11,33 @@ import ( repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" "gitea.dev/models/unittest" + "gitea.dev/modules/git" "gitea.dev/modules/gitrepo" + "gitea.dev/modules/setting" + "gitea.dev/modules/test" "github.com/stretchr/testify/assert" ) // TODO TestPullRequest_PushToBaseRepo -func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) { - // Not a valid trailer section - assert.False(t, commitMessageTrailersPattern.MatchString("")) - assert.False(t, commitMessageTrailersPattern.MatchString("No trailer.")) - assert.False(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nNot a trailer due to following text.")) - assert.False(t, commitMessageTrailersPattern.MatchString("Message body not correctly separated from trailer section by empty line.\nSigned-off-by: Bob ")) - // Valid trailer section - assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob ")) - assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nOther-Trailer: Value")) - assert.True(t, commitMessageTrailersPattern.MatchString("Message body correctly separated from trailer section by empty line.\n\nSigned-off-by: Bob ")) - assert.True(t, commitMessageTrailersPattern.MatchString("Multiple trailers.\n\nSigned-off-by: Bob \nOther-Trailer: Value")) - assert.True(t, commitMessageTrailersPattern.MatchString("Newline after trailer section.\n\nSigned-off-by: Bob \n")) - assert.True(t, commitMessageTrailersPattern.MatchString("No space after colon is accepted.\n\nSigned-off-by:Bob ")) - assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob ")) - assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value")) +func TestPullRequest_FormatSquashMergeCommitMessages(t *testing.T) { + oldest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 1"}} + newest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 2\n\nCommit description."}} + + defer test.MockVariableValue(&setting.Repository.PullRequest.DefaultMergeMessageSize, 0)() + + assert.Equal(t, "* commit msg 1\n\n* commit msg 2\n\nCommit description.\n\n", formatSquashMergeCommitMessages([]*git.Commit{newest, oldest})) + + utf8Msg := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "🌞"}} + setting.Repository.PullRequest.DefaultMergeMessageSize = 3 + assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg})) + setting.Repository.PullRequest.DefaultMergeMessageSize = 4 + assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg})) + setting.Repository.PullRequest.DefaultMergeMessageSize = 5 + assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg})) + setting.Repository.PullRequest.DefaultMergeMessageSize = 6 + assert.Equal(t, "* 🌞\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg})) } func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) { @@ -88,3 +93,27 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage) } + +func TestBuildSquashMergeCommitMessages(t *testing.T) { + cases := []struct { + msg string + coAuthors []string + expected string + }{ + {"title", nil, "title"}, + {"title", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"}, + {"title\n\n", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"}, + {"title\n\nKey: val", []string{"the-user"}, "title\n\nKey: val\nCo-authored-by: the-user\n"}, + {"title\n\n----\nKey: val", []string{"the-user"}, "title\n\n----\nKey: val\nCo-authored-by: the-user\n"}, + {"title\n\n----\nKey: val\n\n", []string{"the-user"}, "title\n\n----\nKey: val\nCo-authored-by: the-user\n"}, + + {"title\n\nbody", nil, "title\n\nbody"}, + {"title\n\nbody", []string{"the-user"}, "title\n\nbody\n\n---------\n\nCo-authored-by: the-user\n"}, + {"title\n\nbody\n\nKey: val", []string{"the-user"}, "title\n\nbody\n\nKey: val\nCo-authored-by: the-user\n"}, + {"title\n\nbody\n\n----\nKey: val", []string{"the-user"}, "title\n\nbody\n\n----\nKey: val\nCo-authored-by: the-user\n"}, + } + for _, c := range cases { + msg := buildSquashMergeCommitMessages(c.msg, c.coAuthors) + assert.Equal(t, c.expected, msg, "msg: %s", c.msg) + } +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index fa41917aab..37a358a26b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -8108,8 +8108,10 @@ }, "/repos/{owner}/{repo}/compare/{basehead}": { "get": { + "description": "By default returns JSON commit comparison information. The raw diff or patch can be\nrequested with the `output` query parameter set to `diff` or `patch` respectively.\n", "produces": [ - "application/json" + "application/json", + "text/plain" ], "tags": [ "repository" @@ -8133,10 +8135,20 @@ }, { "type": "string", - "description": "compare two branches or commits", + "description": "compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs, including branch names that contain slashes.", "name": "basehead", "in": "path", "required": true + }, + { + "enum": [ + "diff", + "patch" + ], + "type": "string", + "description": "return the raw comparison as `diff` or `patch` instead of JSON", + "name": "output", + "in": "query" } ], "responses": { diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 68a495d546..7829544526 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -19468,6 +19468,7 @@ }, "/repos/{owner}/{repo}/compare/{basehead}": { "get": { + "description": "By default returns JSON commit comparison information. The raw diff or patch can be\nrequested with the `output` query parameter set to `diff` or `patch` respectively.\n", "operationId": "repoCompareDiff", "parameters": [ { @@ -19489,13 +19490,25 @@ } }, { - "description": "compare two branches or commits", + "description": "compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs, including branch names that contain slashes.", "in": "path", "name": "basehead", "required": true, "schema": { "type": "string" } + }, + { + "description": "return the raw comparison as `diff` or `patch` instead of JSON", + "in": "query", + "name": "output", + "schema": { + "enum": [ + "diff", + "patch" + ], + "type": "string" + } } ], "responses": { diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go index 0d8cec7776..5bb8cf6bde 100644 --- a/tests/integration/api_repo_compare_test.go +++ b/tests/integration/api_repo_compare_test.go @@ -6,6 +6,7 @@ package integration import ( "net/http" "net/url" + "strings" "testing" auth_model "gitea.dev/models/auth" @@ -62,3 +63,113 @@ func TestAPICompareBranches(t *testing.T) { }) }) } + +func TestAPIDownloadCompareDiffOrPatch(t *testing.T) { + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + + t.Run("BranchToBranchDiff", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + body := resp.Body.String() + assert.Contains(t, body, "diff --git ") + }) + + t.Run("BranchToBranchPatch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b?output=patch").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + body := resp.Body.String() + assert.True(t, strings.HasPrefix(body, "From "), "patch output should start with a format-patch header, got: %q", body[:min(40, len(body))]) + }) + + t.Run("CommitToCommitDiff", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("BranchToCommitDiff", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // 8babce96... is the head of remove-files-b; pairing it with add-csv guarantees a non-empty diff. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...8babce967f21b9dfa6987f943b91093dac58a4f0?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("TwoDotSeparator", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv..remove-files-b?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("SlashedBranchName", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // user2/repo1's `feature/1` branch contains a slash; the route must match it + // without URL-encoding. master and feature/1 happen to share a SHA in the fixture, + // so we only assert the route resolves (200 OK) rather than checking diff content. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/compare/master...feature/1?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + }) + + t.Run("UnknownOutputReturnsJSON", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Only "diff"/"patch" switch to raw output; any other value falls through to JSON. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b?output=foo").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + apiResp := DecodeJSON(t, resp, &api.Compare{}) + assert.Equal(t, 2, apiResp.TotalCommits) + }) + + t.Run("SingleRefImplicitBase", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // No `...`/`..` separator: parseCompareInfo defaults the base to the + // repo's PR target branch (master for repo20) and compares it against + // the given head. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("PrivateRepoAnonymous", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // repo16 is private; an unauthenticated request must not leak its existence. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo16/compare/master...good-sign?output=diff") + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("CrossRepoFork", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13}) + repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11}) + user13Sess := loginUser(t, "user13") + user13Token := getTokenForLoggedInUser(t, user13Sess, auth_model.AccessTokenScopeWriteRepository) + + _, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "cross-repo-diff"}, map[string]string{"hello.txt": "hi\n"}) + require.NoError(t, err) + + req := NewRequest(t, "GET", "/api/v1/repos/user12/repo10/compare/master...user13:cross-repo-diff?output=diff").AddTokenAuth(user13Token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + }) +} diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index ac2e014d92..d5f5bc2cc2 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -167,6 +167,53 @@ Hello from 2 assert.Equal(t, 0, htmlDoc.doc.Find(".pullrequest-form").Length()) } +func TestCompareDownloadDiffOrPatch(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + + t.Run("BranchToBranchDiff", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv...remove-files-b.diff") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("BranchToBranchPatch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv...remove-files-b.patch") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.True(t, strings.HasPrefix(resp.Body.String(), "From "), "patch output should start with a format-patch header") + }) + + t.Run("SingleRefImplicitBase", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv.diff") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("InvalidBaseRef", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo20/compare/does-not-exist...remove-files-b.diff") + session.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("PrivateRepoAnonymous", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // repo16 is private; an unauthenticated request must not leak its existence. + req := NewRequest(t, "GET", "/user2/repo16/compare/master...good-sign.diff") + MakeRequest(t, req, http.StatusNotFound) + }) +} + func TestCompareCodeExpand(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 8469057620..ae1f8a3490 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -1272,7 +1272,7 @@ Commit description. commitMessage: `loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message`, }, }, - expectedMessage: `* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...`, + expectedMessage: "* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...\n\n", }, { name: "Test Co-authored-by",