mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-26 23:53:35 +02:00
Merge branch 'main' into puni9869/ISSUE-36299
This commit is contained in:
commit
689d4b2c7c
2
.github/workflows/cron-renovate.yml
vendored
2
.github/workflows/cron-renovate.yml
vendored
@ -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
|
||||
|
||||
2
.github/workflows/pull-db-tests.yml
vendored
2
.github/workflows/pull-db-tests.yml
vendored
@ -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
|
||||
|
||||
2
Makefile
2
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
|
||||
|
||||
2
go.mod
2
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
|
||||
|
||||
@ -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<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) {
|
||||
s = util.NormalizeStringEOL(s)
|
||||
re := commitMessageTrailerSplit()
|
||||
|
||||
@ -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"},
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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 <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_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.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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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", "/<basehead:*>.diff", repo.MustBeNotEmpty, repo.DownloadCompareDiff)
|
||||
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)
|
||||
}, optSignIn, context.RepoAssignment, reqUnitCodeReader)
|
||||
// end "/{username}/{reponame}": repo code: find, compare, list
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 <bob@example.com>\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 <bob@example.com>"))
|
||||
// Valid trailer section
|
||||
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"))
|
||||
assert.True(t, commitMessageTrailersPattern.MatchString("Message body correctly separated from trailer section by empty line.\n\nSigned-off-by: Bob <bob@example.com>"))
|
||||
assert.True(t, commitMessageTrailersPattern.MatchString("Multiple trailers.\n\nSigned-off-by: Bob <bob@example.com>\nOther-Trailer: Value"))
|
||||
assert.True(t, commitMessageTrailersPattern.MatchString("Newline after trailer section.\n\nSigned-off-by: Bob <bob@example.com>\n"))
|
||||
assert.True(t, commitMessageTrailersPattern.MatchString("No space after colon is accepted.\n\nSigned-off-by:Bob <bob@example.com>"))
|
||||
assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob <bob@example.com> "))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
16
templates/swagger/v1_json.tmpl
generated
16
templates/swagger/v1_json.tmpl
generated
@ -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": {
|
||||
|
||||
15
templates/swagger/v1_openapi3_json.tmpl
generated
15
templates/swagger/v1_openapi3_json.tmpl
generated
@ -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": {
|
||||
|
||||
@ -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 ")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -1272,7 +1272,7 @@ Commit description.
|
||||
commitMessage: `loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message`,
|
||||
},
|
||||
},
|
||||
expectedMessage: `* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...`,
|
||||
expectedMessage: "* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...\n\n",
|
||||
},
|
||||
{
|
||||
name: "Test Co-authored-by",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user