Merge branch 'main' into puni9869/ISSUE-36299

This commit is contained in:
puni9869 authored and GitHub committed 2026-06-16 09:15:06 +05:30
commit 689d4b2c7c
21 files changed
+447 -101

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: renovatebot/github-action@693b9ef15eec82123529a37c782242f091365961 # v46.1.14 - uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15
with: with:
renovate-version: ${{ env.RENOVATE_VERSION }} renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5 configurationFile: renovate.json5
+1 -1
View File
@@ -131,7 +131,7 @@ jobs:
ports: ports:
- "7700:7700" - "7700:7700"
redis: redis:
image: redis:latest@sha256:e74c9b933d78e2829583d88f92793f4524752a15ac59c8baff2dd5ed000b7432 image: redis:latest@sha256:a505f8b9d8ac3ff7b0848055b4abf1901d6d77606774aa1e38bd37f1197ed2b5
options: >- # wait until redis has started options: >- # wait until redis has started
--health-cmd "redis-cli ping" --health-cmd "redis-cli ping"
--health-interval 5s --health-interval 5s
+1 -1
View File
@@ -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 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 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 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 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 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 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
+1 -1
View File
@@ -105,6 +105,7 @@ require (
go.yaml.in/yaml/v4 v4.0.0-rc.5 go.yaml.in/yaml/v4 v4.0.0-rc.5
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.53.0
golang.org/x/image v0.42.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/net v0.56.0
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.21.0 golang.org/x/sync v0.21.0
@@ -267,7 +268,6 @@ require (
go.uber.org/zap/exp v0.3.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // 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/time v0.15.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/tools v0.45.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
+3 -1
View File
@@ -70,9 +70,11 @@ func (c *CommitMessage) MessageTrailer() CommitMessageTrailerValues {
var commitMessageTrailerSplit = sync.OnceValue(func() *regexp.Regexp { 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" // 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?)*)$`) 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*)$`)
}) })
// 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) { func CommitMessageSplitTrailer(s string) (content, sep, trailer string) {
s = util.NormalizeStringEOL(s) s = util.NormalizeStringEOL(s)
re := commitMessageTrailerSplit() re := commitMessageTrailerSplit()
+2
View File
@@ -26,8 +26,10 @@ func TestCommitMessageTrailer(t *testing.T) {
{"a", "a", "", ""}, {"a", "a", "", ""},
{"a\n\nk", "a\n\nk", "", ""}, {"a\n\nk", "a\n\nk", "", ""},
{"a\n\nk:v", "a", "\n\n", "k:v"}, {"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--\nk:v", "", ""},
{"a\n---\nk:v", "a", "\n---\n", "k: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"}, {"k: v", "", "", "k: v"},
{"\nk:v", "", "\n", "k:v"}, {"\nk:v", "", "\n", "k:v"},
+10
View File
@@ -10,6 +10,8 @@ import (
"strings" "strings"
"gitea.dev/modules/util" "gitea.dev/modules/util"
"golang.org/x/mod/semver"
) )
const ( const (
@@ -20,6 +22,7 @@ const (
var ( var (
ErrInvalidStructure = util.NewInvalidArgumentErrorf("package has invalid structure") ErrInvalidStructure = util.NewInvalidArgumentErrorf("package has invalid structure")
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
ErrGoModFileTooLarge = util.NewInvalidArgumentErrorf("go.mod file is too large") 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]), Name: strings.TrimSuffix(nameAndVersion, "@"+parts[1]),
Version: versionParts[0], 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 { if len(versionParts) > 1 {
+10
View File
@@ -59,6 +59,16 @@ func TestParsePackage(t *testing.T) {
assert.Equal(t, "module gitea.com/go-gitea/gitea", p.GoMod) 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) { t.Run("Valid", func(t *testing.T) {
data := createArchive(map[string][]byte{ data := createArchive(map[string][]byte{
packageName + "@" + packageVersion + "/subdir/go.mod": []byte("invalid"), packageName + "@" + packageVersion + "/subdir/go.mod": []byte("invalid"),
+19 -3
View File
@@ -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.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": "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.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": "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.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": "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_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.", "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.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.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.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.protected": "Cosanta",
"repo.diff.image.side_by_side": "Taobh le Taobh", "repo.diff.image.side_by_side": "Taobh le Taobh",
"repo.diff.image.swipe": "Scaoil", "repo.diff.image.swipe": "Scaoil",
@@ -2862,6 +2865,14 @@
"org.teams.all_repositories_read_permission_desc": "Tugann an fhoireann seo rochtain do <strong>Léamh</strong> ar <strong>gach stórais</strong>: is féidir le baill amharc ar stórais agus iad a chlónáil.", "org.teams.all_repositories_read_permission_desc": "Tugann an fhoireann seo rochtain do <strong>Léamh</strong> ar <strong>gach stórais</strong>: 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 <strong>Scríobh</strong> ar <strong>gach stórais</strong>: is féidir le baill léamh ó stórais agus iad a bhrú chucu.", "org.teams.all_repositories_write_permission_desc": "Tugann an fhoireann seo rochtain do <strong>Scríobh</strong> ar <strong>gach stórais</strong>: 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 <strong>Riarthóra</strong> ar <strong>gach stóras</strong>: is féidir le comhaltaí léamh, brú a dhéanamh agus comhoibritheoirí a chur le stórtha.", "org.teams.all_repositories_admin_permission_desc": "Tugann an fhoireann seo rochtain <strong>Riarthóra</strong> ar <strong>gach stóras</strong>: 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 <strong>%s</strong> san eagraíocht <strong>%s</strong>.", "org.teams.invite.title": "Tugadh cuireadh duit dul isteach i bhfoireann <strong>%s</strong> san eagraíocht <strong>%s</strong>.",
"org.teams.invite.by": "Ar cuireadh ó %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.", "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_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_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.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.actor": "Aisteoir",
"actions.runs.status": "Stádas", "actions.runs.status": "Stádas",
"actions.runs.actors_no_select": "Gach aisteoir", "actions.runs.actors_no_select": "Gach aisteoir",
@@ -3794,13 +3806,17 @@
"actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre", "actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre",
"actions.runs.summary": "Achoimre", "actions.runs.summary": "Achoimre",
"actions.runs.all_jobs": "Gach post", "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.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.collapse_caller_jobs": "Folaigh poist an ghlaoiteora sreabha oibre in-athúsáidte seo",
"actions.runs.attempt": "Iarracht", "actions.runs.attempt": "Iarracht",
"actions.runs.latest": "Is déanaí", "actions.runs.latest": "Is déanaí",
"actions.runs.latest_attempt": "An iarracht is déanaí", "actions.runs.latest_attempt": "An iarracht is déanaí",
"actions.runs.triggered_via": "Spreagtha trí %s", "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.workflow_dependencies": "Spleáchais ar Shreabhadh Oibre",
"actions.runs.graph_jobs_count_1": "%d post", "actions.runs.graph_jobs_count_1": "%d post",
"actions.runs.graph_jobs_count_n": "%d poist", "actions.runs.graph_jobs_count_n": "%d poist",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"type": "module", "type": "module",
"packageManager": "pnpm@11.5.2", "packageManager": "pnpm@11.5.3",
"engines": { "engines": {
"node": ">= 22.18.0", "node": ">= 22.18.0",
"pnpm": ">= 11.0.0" "pnpm": ">= 11.0.0"
+40 -1
View File
@@ -11,6 +11,7 @@ import (
api "gitea.dev/modules/structs" api "gitea.dev/modules/structs"
"gitea.dev/services/context" "gitea.dev/services/context"
"gitea.dev/services/convert" "gitea.dev/services/convert"
git_service "gitea.dev/services/git"
) )
// CompareDiff compare two branches or commits // 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 // swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} repository repoCompareDiff
// --- // ---
// summary: Get commit comparison information // 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: // produces:
// - application/json // - application/json
// - text/plain
// parameters: // parameters:
// - name: owner // - name: owner
// in: path // in: path
@@ -33,9 +38,16 @@ func CompareDiff(ctx *context.APIContext) {
// required: true // required: true
// - name: basehead // - name: basehead
// in: path // 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 // type: string
// required: true // required: true
// - name: output
// in: query
// description: return the raw comparison as `diff` or `patch` instead of JSON
// type: string
// enum:
// - diff
// - patch
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/Compare" // "$ref": "#/responses/Compare"
@@ -57,6 +69,16 @@ func CompareDiff(ctx *context.APIContext) {
} }
defer closer() 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") verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files") files := ctx.FormString("files") == "" || ctx.FormBool("files")
@@ -88,3 +110,20 @@ func CompareDiff(ctx *context.APIContext) {
Commits: apiCommits, 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
}
}
+42 -3
View File
@@ -201,12 +201,12 @@ func newComparePageInfo() *comparePageInfoType {
} }
// parseCompareInfo parse compare info between two commit for preparing comparing references // 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 baseRepo := ctx.Repo.Repository
fileOnly := ctx.FormBool("file-only") fileOnly := ctx.FormBool("file-only")
// 1 Parse compare router param // 1 Parse compare router param
compareReq := common.ParseCompareRouterParam(ctx.PathParam("*")) compareReq := common.ParseCompareRouterParam(compareParam)
// remove the check when we support compare with carets // remove the check when we support compare with carets
if compareReq.BaseOriRefSuffix != "" { 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 // CompareDiff show different from one commit to another commit
func CompareDiff(ctx *context.Context) { func CompareDiff(ctx *context.Context) {
comparePageInfo := newComparePageInfo() comparePageInfo := newComparePageInfo()
err := comparePageInfo.parseCompareInfo(ctx) err := comparePageInfo.parseCompareInfo(ctx, ctx.PathParam("*"))
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
ctx.NotFound(nil) ctx.NotFound(nil)
return return
@@ -605,6 +605,45 @@ func CompareDiff(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplCompare) 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) { func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Context) {
ci := cpi.compareInfo ci := cpi.compareInfo
if cpi.allowCreatePull { if cpi.allowCreatePull {
+1 -1
View File
@@ -1310,7 +1310,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateIssueForm) form := web.GetForm(ctx).(*forms.CreateIssueForm)
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
comparePageInfo := newComparePageInfo() comparePageInfo := newComparePageInfo()
err := comparePageInfo.parseCompareInfo(ctx) err := comparePageInfo.parseCompareInfo(ctx, ctx.PathParam("*"))
if errors.Is(err, util.ErrNotExist) { if errors.Is(err, util.ErrNotExist) {
ctx.JSONErrorNotFound() ctx.JSONErrorNotFound()
return return
+6 -3
View File
@@ -1269,9 +1269,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeViewNodes) m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeViewNodes)
}) })
m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff) m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists). m.PathGroup("/compare/*", func(g *web.RouterPathGroup) {
Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). g.MatchPath("GET", "/<basehead:*>.diff", repo.MustBeNotEmpty, repo.DownloadCompareDiff)
Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) g.MatchPath("GET", "/<basehead:*>.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) m.Get("/pulls/new/*", repo.PullsNewRedirect)
}, optSignIn, context.RepoAssignment, reqUnitCodeReader) }, optSignIn, context.RepoAssignment, reqUnitCodeReader)
// end "/{username}/{reponame}": repo code: find, compare, list // end "/{username}/{reponame}": repo code: find, compare, list
+78 -65
View File
@@ -4,14 +4,15 @@
package pull package pull
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"regexp"
"slices" "slices"
"strings" "strings"
"time" "time"
"unicode"
"unicode/utf8" "unicode/utf8"
"gitea.dev/models/db" "gitea.dev/models/db"
@@ -767,8 +768,6 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
return errors.Join(errs...) 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) // 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 {
if err := pr.LoadIssue(ctx); err != nil { if err := pr.LoadIssue(ctx); err != nil {
@@ -819,54 +818,44 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
return "" 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]) func buildSquashMergeCommitMessages(mergeMessage string, coAuthors []string) string {
authors := make([]string, 0, len(limitedCommits)) if len(coAuthors) == 0 {
stringBuilder := strings.Builder{} return mergeMessage
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
}
}
} }
// 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 { for _, commit := range limitedCommits {
authorString := commit.Author.String() authorString := commit.Author.String()
if uniqueAuthors.Add(authorString) && authorString != posterSig { if uniqueAuthors.Add(authorString) && authorString != posterSig {
@@ -880,14 +869,14 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
} }
// collect the remaining authors // collect the remaining authors
if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors { if limitFirst >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
skip := limit skip := limitFirst
limit = 30 batchLimit := 30
for { for {
commits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, limit, skip) commits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, batchLimit, skip)
if err != nil { if err != nil {
log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err) log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
return "" return authors
} }
if len(commits) == 0 { if len(commits) == 0 {
break 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 { buf := bytes.TrimSpace(sb.Bytes())
// TODO: this separator line doesn't work with the trailer check (commitMessageTrailersPattern) above if maxMsgSize > 0 && len(buf) > maxMsgSize {
stringBuilder.WriteString("---------\n\n") 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, '.', '.', '.')
} }
buf = append(buf, '\n', '\n')
for _, author := range authors { return util.UnsafeBytesToString(buf)
stringBuilder.WriteString(git.CoAuthoredByTrailer + ": ")
stringBuilder.WriteString(author)
stringBuilder.WriteRune('\n')
}
return stringBuilder.String()
} }
// 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 // 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
+44 -15
View File
@@ -11,28 +11,33 @@ import (
repo_model "gitea.dev/models/repo" repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit" "gitea.dev/models/unit"
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo" "gitea.dev/modules/gitrepo"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// TODO TestPullRequest_PushToBaseRepo // TODO TestPullRequest_PushToBaseRepo
func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) { func TestPullRequest_FormatSquashMergeCommitMessages(t *testing.T) {
// Not a valid trailer section oldest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 1"}}
assert.False(t, commitMessageTrailersPattern.MatchString("")) newest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 2\n\nCommit description."}}
assert.False(t, commitMessageTrailersPattern.MatchString("No trailer."))
assert.False(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob <bob@example.com>\nNot a trailer due to following text.")) defer test.MockVariableValue(&setting.Repository.PullRequest.DefaultMergeMessageSize, 0)()
assert.False(t, commitMessageTrailersPattern.MatchString("Message body not correctly separated from trailer section by empty line.\nSigned-off-by: Bob <bob@example.com>"))
// Valid trailer section assert.Equal(t, "* commit msg 1\n\n* commit msg 2\n\nCommit description.\n\n", formatSquashMergeCommitMessages([]*git.Commit{newest, oldest}))
assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob <bob@example.com>"))
assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob <bob@example.com>\nOther-Trailer: Value")) utf8Msg := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "🌞"}}
assert.True(t, commitMessageTrailersPattern.MatchString("Message body correctly separated from trailer section by empty line.\n\nSigned-off-by: Bob <bob@example.com>")) setting.Repository.PullRequest.DefaultMergeMessageSize = 3
assert.True(t, commitMessageTrailersPattern.MatchString("Multiple trailers.\n\nSigned-off-by: Bob <bob@example.com>\nOther-Trailer: Value")) assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg}))
assert.True(t, commitMessageTrailersPattern.MatchString("Newline after trailer section.\n\nSigned-off-by: Bob <bob@example.com>\n")) setting.Repository.PullRequest.DefaultMergeMessageSize = 4
assert.True(t, commitMessageTrailersPattern.MatchString("No space after colon is accepted.\n\nSigned-off-by:Bob <bob@example.com>")) assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg}))
assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob <bob@example.com> ")) setting.Repository.PullRequest.DefaultMergeMessageSize = 5
assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value")) 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) { 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) 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)
}
}
+14 -2
View File
@@ -8108,8 +8108,10 @@
}, },
"/repos/{owner}/{repo}/compare/{basehead}": { "/repos/{owner}/{repo}/compare/{basehead}": {
"get": { "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": [ "produces": [
"application/json" "application/json",
"text/plain"
], ],
"tags": [ "tags": [
"repository" "repository"
@@ -8133,10 +8135,20 @@
}, },
{ {
"type": "string", "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", "name": "basehead",
"in": "path", "in": "path",
"required": true "required": true
},
{
"enum": [
"diff",
"patch"
],
"type": "string",
"description": "return the raw comparison as `diff` or `patch` instead of JSON",
"name": "output",
"in": "query"
} }
], ],
"responses": { "responses": {
+14 -1
View File
@@ -19468,6 +19468,7 @@
}, },
"/repos/{owner}/{repo}/compare/{basehead}": { "/repos/{owner}/{repo}/compare/{basehead}": {
"get": { "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", "operationId": "repoCompareDiff",
"parameters": [ "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", "in": "path",
"name": "basehead", "name": "basehead",
"required": true, "required": true,
"schema": { "schema": {
"type": "string" "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": { "responses": {
+111
View File
@@ -6,6 +6,7 @@ package integration
import ( import (
"net/http" "net/http"
"net/url" "net/url"
"strings"
"testing" "testing"
auth_model "gitea.dev/models/auth" 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 ")
})
})
}
+47
View File
@@ -167,6 +167,53 @@ Hello from 2
assert.Equal(t, 0, htmlDoc.doc.Find(".pullrequest-form").Length()) 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) { func TestCompareCodeExpand(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { onGiteaRun(t, func(t *testing.T, u *url.URL) {
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+1 -1
View File
@@ -1272,7 +1272,7 @@ Commit description.
commitMessage: `loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message`, commitMessage: `loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message`,
}, },
}, },
expectedMessage: `* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...`, expectedMessage: "* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...\n\n",
}, },
{ {
name: "Test Co-authored-by", name: "Test Co-authored-by",