From 1363b097e2c59964ea9907a4d13d6f120113b862 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 15 Jun 2026 08:20:43 -0700 Subject: [PATCH 1/7] chore(deps): update action dependencies (#38121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | redis | service | digest | `e74c9b9` → `a505f8b` | | [renovatebot/github-action](https://redirect.github.com/renovatebot/github-action) | action | patch | `v46.1.14` → `v46.1.15` | --- ### Release Notes
renovatebot/github-action (renovatebot/github-action) ### [`v46.1.15`](https://redirect.github.com/renovatebot/github-action/releases/tag/v46.1.15) [Compare Source](https://redirect.github.com/renovatebot/github-action/compare/v46.1.14...v46.1.15) ##### Documentation - update references to actions/checkout to v6.0.3 ([#​1033](https://redirect.github.com/renovatebot/github-action/issues/1033)) ([fb473e1](https://redirect.github.com/renovatebot/github-action/commit/fb473e186b4d3622f844c27052e93a8c5fda1717)) - update references to renovatebot/github-action to v46.1.14 ([34e09dd](https://redirect.github.com/renovatebot/github-action/commit/34e09dd76c34de1c47342613280a76eed7409e63)) ##### Miscellaneous Chores - **deps:** update linters to v8.60.0 ([1abcc51](https://redirect.github.com/renovatebot/github-action/commit/1abcc518dc56e8b8dc3679e53a454e8a06f06047)) - **deps:** update node.js to v24.16.0 ([7bbd8b1](https://redirect.github.com/renovatebot/github-action/commit/7bbd8b12ba39417da412895715d31c2304897b1c)) - **deps:** update pnpm to v10.34.1 ([fc48fa8](https://redirect.github.com/renovatebot/github-action/commit/fc48fa8e31dd5915d2cc35558846455f271630fe)) - **deps:** update semantic-release monorepo to v12.0.8 ([7ae9fb9](https://redirect.github.com/renovatebot/github-action/commit/7ae9fb9e94d999c8ba8b1371ffff0e5521f18f42)) ##### Build System - **deps:** lock file maintenance ([3e7e656](https://redirect.github.com/renovatebot/github-action/commit/3e7e6563b31485ac0fac48701c13e12737fec9ea)) ##### Continuous Integration - **deps:** update actions/checkout action to v6.0.3 ([bb87b51](https://redirect.github.com/renovatebot/github-action/commit/bb87b5131ab4a1fd5fad5b0b1a27995a99a89c61)) - **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.171.3 ([f4736a8](https://redirect.github.com/renovatebot/github-action/commit/f4736a876f6e93d22501a6dbeebc3c79f15d954e)) - **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.173.0 ([4374486](https://redirect.github.com/renovatebot/github-action/commit/4374486206593aaf0106b4be898e4fa7dfffd670)) - **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.214.5 ([3fbdafe](https://redirect.github.com/renovatebot/github-action/commit/3fbdafedb1c77e92a6c3a8560ab0c53aff06fad3)) - **deps:** update ghcr.io/zizmorcore/zizmor docker tag to v1.25.2 ([#​1034](https://redirect.github.com/renovatebot/github-action/issues/1034)) ([58252bc](https://redirect.github.com/renovatebot/github-action/commit/58252bce69198e9a1bdc863c40f3139fb1e6cb74)) - **deps:** update zizmorcore/zizmor-action action to v0.5.6 ([b8cc935](https://redirect.github.com/renovatebot/github-action/commit/b8cc935bc11c1ba8e6510fa2b9782919e6bb79f5))
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Only on Monday (`* * * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). Co-authored-by: bircni --- .github/workflows/cron-renovate.yml | 2 +- .github/workflows/pull-db-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cron-renovate.yml b/.github/workflows/cron-renovate.yml index 4db83a336d..843527ba20 100644 --- a/.github/workflows/cron-renovate.yml +++ b/.github/workflows/cron-renovate.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: renovatebot/github-action@693b9ef15eec82123529a37c782242f091365961 # v46.1.14 + - uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15 with: renovate-version: ${{ env.RENOVATE_VERSION }} configurationFile: renovate.json5 diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 4cc8d25bbb..a35937f266 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -131,7 +131,7 @@ jobs: ports: - "7700:7700" redis: - image: redis:latest@sha256:e74c9b933d78e2829583d88f92793f4524752a15ac59c8baff2dd5ed000b7432 + image: redis:latest@sha256:a505f8b9d8ac3ff7b0848055b4abf1901d6d77606774aa1e38bd37f1197ed2b5 options: >- # wait until redis has started --health-cmd "redis-cli ping" --health-interval 5s From b4cb192fba71fe280d67ce6f8076cdefa46569b3 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 15 Jun 2026 09:16:16 -0700 Subject: [PATCH 2/7] chore(deps): update pnpm to v11.5.3 (#38133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [pnpm](https://pnpm.io) ([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) | [`11.5.2` → `11.5.3`](https://renovatebot.com/diffs/npm/pnpm/11.5.2/11.5.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/11.5.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/11.5.2/11.5.3?slim=true) | --- ### Release Notes
pnpm/pnpm (pnpm) ### [`v11.5.3`](https://redirect.github.com/pnpm/pnpm/blob/HEAD/pnpm/CHANGELOG.md#1153) [Compare Source](https://redirect.github.com/pnpm/pnpm/compare/v11.5.2...v11.5.3) ##### Patch Changes - Stopped expanding environment variables in repository-controlled registry/proxy request destinations and registry credential values from `.npmrc`, and in workspace registry URLs from `pnpm-workspace.yaml`. Move dynamic registry URL and token configuration to trusted user, global, CLI, or environment config. - Resolve package-manager bootstrap dependencies with trusted user or CLI registry and network config, and reject package-manager env-lockfile records that do not use registry package paths with integrity-only resolutions before auto-switch execution. - Avoid writing `packageManagerDependencies` to `pnpm-lock.yaml` when package manager policy is set to `onFail: ignore` or `pmOnFail: ignore` [#​12228](https://redirect.github.com/pnpm/pnpm/issues/12228). - Avoid running dependency-status auto-install when the dependency status is unavailable without a project manifest. - Using the `$` version reference syntax in `overrides` (e.g. `"react": "$react"`) now prints a deprecation warning. The syntax still works, but [catalogs](https://pnpm.io/catalogs) are the recommended way to keep an overridden version in sync with the rest of the workspace. Reference a catalog entry with the `catalog:` protocol instead. - Fixed `pnpm config get globalconfig` to return the global `config.yaml` path again [pnpm/pnpm#11962](https://redirect.github.com/pnpm/pnpm/issues/11962). - Fixed bare `--color` so it does not consume the following CLI flag, allowing command shorthands like `--parallel` to expand correctly and forms like `pnpm --color with current ` to dispatch the inner command instead of failing with `MISSING_WITH_CURRENT_CMD`. - Fix `pnpm install` ignoring `enableGlobalVirtualStore` toggle by including it in the workspace state settings check [#​12142](https://redirect.github.com/pnpm/pnpm/issues/12142). - Security: pnpm now verifies the npm registry signature of a package-manager binary before spawning it, so a cloned repository cannot make pnpm download and execute an arbitrary native binary. This covers two paths that select an executable from repository-controlled input: - **pacquet install engine** — declaring `pacquet` (or `@pnpm/pacquet`) in `configDependencies` opts in to pnpm's Rust install engine. pnpm now verifies that the installed `pacquet` shim and the host's `@pacquet/-` binary carry a valid npm registry signature for their exact `name@version`, and refuses to run pacquet (failing the command) if the signature does not verify or cannot be checked. The only graceful fallback to pnpm's own engine is when pacquet has no binary for the current platform. - **automatic version switch / `self-update`** — the `packageManager` / `devEngines.packageManager` field makes pnpm download and run a specific pnpm version. pnpm now verifies the registry signature of `pnpm`, `@pnpm/exe`, and the host platform binary before installing/spawning them, and refuses to run an engine whose signature does not match a published, signed release. The check runs only on an actual download (store cache miss), so it does not add a network round trip to every command. In both cases the signature is verified over the *installed* integrity, against npm's public signing keys that ship embedded in the pnpm CLI (like corepack), so bytes substituted via a tampered lockfile or a repository-controlled registry fail verification — and a registry the user did not vouch for cannot supply its own signing keys. The signed packument is fetched from the configured registry, so an npm mirror works transparently. Verification fails closed: if it cannot be completed (for example, the registry is unreachable), the command fails rather than running an unverified binary. The embedded keys are kept current by a release-time check against npm's signing-keys endpoint. - Made peer-dependent deduplication deterministic. When a peer-suffixed package variant was a subset of two or more mutually incompatible larger variants, the variant it collapsed into depended on the order importers were resolved in, which varies between machines. This could resolve the same workspace to different lockfiles on different platforms and make `pnpm dedupe --check` alternate between passing and failing. - Reject invalid package names and versions from staged tarball manifests before deriving filenames for `pnpm stage download`. - Clarified in CLI help that the pnpm store is trusted shared state and store integrity checks are corruption detection, not a tamper boundary for untrusted store writers. - Reject reserved manifest `bin` names (`""`, `"."`, `".."`, and scoped forms such as `@scope/..`) when resolving a package's bins. These names previously passed the bin-name guard and, when joined to the global bin directory during global remove/update/add operations, could resolve to the global bin directory itself or its parent and have it recursively deleted. - Require trusted package identity before package-name `allowBuilds` entries can approve lifecycle scripts for git, git-hosted tarball, direct tarball, and local directory artifacts. To approve one of those artifacts explicitly, use its peer-suffix-free lockfile depPath as the `allowBuilds` key. Lockfile verification now rejects lockfiles where a registry-style dependency path (`name@semver`) is backed by a git, directory, or git-hosted tarball resolution (`ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`), so the dependency path is a reliable artifact identity by the time scripts can run. - Security: pnpm now verifies the OpenPGP signature of a downloaded Node.js runtime's `SHASUMS256.txt` before trusting its integrity hashes. When a repository requests a Node.js runtime (e.g. via `devEngines.runtime` / `useNodeVersion`), the download mirror is repository-configurable through `node-mirror:`. The integrity of the downloaded binary was only checked against `SHASUMS256.txt` fetched from that same mirror — a circular check that a malicious mirror could satisfy by serving a tampered binary together with a matching `SHASUMS256.txt`. pnpm then executes the binary (for example to run lifecycle scripts). pnpm now fetches `SHASUMS256.txt.sig` and verifies the detached OpenPGP signature against the Node.js release team's public keys, which ship embedded in the pnpm CLI. A mirror that serves a tampered binary cannot also produce a valid signature, so the download fails to verify. The embedded keys are kept current by a release-time check against the canonical `nodejs/release-keys` list. The musl variants from the hardcoded `unofficial-builds.nodejs.org` mirror are not repository-configurable and are signed by a different key, so they continue to be trusted over TLS.
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Only on Monday (`* * * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). Co-authored-by: bircni --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6fc24a21f..ff7f8f1be0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "packageManager": "pnpm@11.5.2", + "packageManager": "pnpm@11.5.3", "engines": { "node": ">= 22.18.0", "pnpm": ">= 11.0.0" From 052feee34a807fcf6964ce93e2792379cbf34c71 Mon Sep 17 00:00:00 2001 From: Rafail Giavrimis <47496212+grafail@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:37:15 +0100 Subject: [PATCH 3/7] feat: add raw diff/patch endpoint for repository comparisons (#37632) ## Summary Adds `GET /repos/{owner}/{repo}/compare/{basehead}.{diffType:diff|patch}`, mirroring the existing `/git/commits/{sha}.{diffType}` endpoint but for comparisons between two arbitrary refs. The new endpoint streams a raw unified diff or `git format-patch` output between any two refs: GET /repos/{owner}/{repo}/compare/main...feature.diff GET /repos/{owner}/{repo}/compare/v1.0..v1.1.patch GET /repos/{owner}/{repo}/compare/abc1234...def5678.diff Resolves #5561, #13416 and #17165. AI was used while creating this PR. Automated tests were added as per the contribution policy. --------- Co-authored-by: bircni Co-authored-by: Claude Opus 4.8 --- routers/api/v1/repo/compare.go | 41 +++++++- routers/web/repo/compare.go | 45 ++++++++- routers/web/repo/pull.go | 2 +- routers/web/web.go | 9 +- templates/swagger/v1_json.tmpl | 16 ++- templates/swagger/v1_openapi3_json.tmpl | 15 ++- tests/integration/api_repo_compare_test.go | 111 +++++++++++++++++++++ tests/integration/compare_test.go | 47 +++++++++ 8 files changed, 275 insertions(+), 11 deletions(-) diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 081ea5e91b..ddd5a9bb21 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -11,6 +11,7 @@ import ( api "gitea.dev/modules/structs" "gitea.dev/services/context" "gitea.dev/services/convert" + git_service "gitea.dev/services/git" ) // CompareDiff compare two branches or commits @@ -18,8 +19,12 @@ func CompareDiff(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} repository repoCompareDiff // --- // summary: Get commit comparison information + // description: | + // By default returns JSON commit comparison information. The raw diff or patch can be + // requested with the `output` query parameter set to `diff` or `patch` respectively. // produces: // - application/json + // - text/plain // parameters: // - name: owner // in: path @@ -33,9 +38,16 @@ func CompareDiff(ctx *context.APIContext) { // required: true // - name: basehead // in: path - // description: compare two branches or commits + // description: compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs, including branch names that contain slashes. // type: string // required: true + // - name: output + // in: query + // description: return the raw comparison as `diff` or `patch` instead of JSON + // type: string + // enum: + // - diff + // - patch // responses: // "200": // "$ref": "#/responses/Compare" @@ -57,6 +69,16 @@ func CompareDiff(ctx *context.APIContext) { } defer closer() + // ?output=diff|patch returns the raw output, otherwise the JSON comparison is returned. + switch ctx.FormString("output") { + case "diff": + downloadCompareDiffOrPatch(ctx, compareInfo, false) + return + case "patch": + downloadCompareDiffOrPatch(ctx, compareInfo, true) + return + } + verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") files := ctx.FormString("files") == "" || ctx.FormBool("files") @@ -88,3 +110,20 @@ func CompareDiff(ctx *context.APIContext) { Commits: apiCommits, }) } + +// downloadCompareDiffOrPatch writes a comparison's raw diff or patch to the response. +func downloadCompareDiffOrPatch(ctx *context.APIContext, compareInfo *git_service.CompareInfo, patch bool) { + ctx.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8") + compareArg := compareInfo.BaseCommitID + compareInfo.CompareSeparator + compareInfo.HeadCommitID + + var err error + if patch { + err = compareInfo.HeadGitRepo.GetPatch(compareArg, ctx.Resp) + } else { + err = compareInfo.HeadGitRepo.GetDiff(compareArg, ctx.Resp) + } + if err != nil { + ctx.APIErrorInternal(err) + return + } +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 45735fc8fe..5a9fa1c1d1 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -201,12 +201,12 @@ func newComparePageInfo() *comparePageInfoType { } // parseCompareInfo parse compare info between two commit for preparing comparing references -func (cpi *comparePageInfoType) parseCompareInfo(ctx *context.Context) error { +func (cpi *comparePageInfoType) parseCompareInfo(ctx *context.Context, compareParam string) error { baseRepo := ctx.Repo.Repository fileOnly := ctx.FormBool("file-only") // 1 Parse compare router param - compareReq := common.ParseCompareRouterParam(ctx.PathParam("*")) + compareReq := common.ParseCompareRouterParam(compareParam) // remove the check when we support compare with carets if compareReq.BaseOriRefSuffix != "" { @@ -545,7 +545,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor // CompareDiff show different from one commit to another commit func CompareDiff(ctx *context.Context) { comparePageInfo := newComparePageInfo() - err := comparePageInfo.parseCompareInfo(ctx) + err := comparePageInfo.parseCompareInfo(ctx, ctx.PathParam("*")) if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { ctx.NotFound(nil) return @@ -605,6 +605,45 @@ func CompareDiff(ctx *context.Context) { ctx.HTML(http.StatusOK, tplCompare) } +// DownloadCompareDiff render a comparison's raw unified diff +func DownloadCompareDiff(ctx *context.Context) { + downloadCompareDiffOrPatch(ctx, false) +} + +// DownloadComparePatch render a comparison as a git format-patch +func DownloadComparePatch(ctx *context.Context) { + downloadCompareDiffOrPatch(ctx, true) +} + +func downloadCompareDiffOrPatch(ctx *context.Context, patch bool) { + // The route captures `basehead` separately so the `.diff`/`.patch` suffix is + // stripped from the catch-all `*` param parseCompareInfo would otherwise read. + cpi := newComparePageInfo() + if err := cpi.parseCompareInfo(ctx, ctx.PathParam("basehead")); err != nil { + if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { + ctx.NotFound(nil) + } else { + ctx.ServerError("ParseCompareInfo", err) + } + return + } + ci := cpi.compareInfo + + ctx.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8") + compareArg := ci.BaseCommitID + ci.CompareSeparator + ci.HeadCommitID + + var err error + if patch { + err = ci.HeadGitRepo.GetPatch(compareArg, ctx.Resp) + } else { + err = ci.HeadGitRepo.GetDiff(compareArg, ctx.Resp) + } + if err != nil { + ctx.ServerError("DownloadCompareDiffOrPatch", err) + return + } +} + func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Context) { ci := cpi.compareInfo if cpi.allowCreatePull { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 650f94110f..81283a2538 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1310,7 +1310,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateIssueForm) repo := ctx.Repo.Repository comparePageInfo := newComparePageInfo() - err := comparePageInfo.parseCompareInfo(ctx) + err := comparePageInfo.parseCompareInfo(ctx, ctx.PathParam("*")) if errors.Is(err, util.ErrNotExist) { ctx.JSONErrorNotFound() return diff --git a/routers/web/web.go b/routers/web/web.go index ee30b614c8..bde886177c 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1269,9 +1269,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeViewNodes) }) m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff) - m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists). - Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). - Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) + m.PathGroup("/compare/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/.diff", repo.MustBeNotEmpty, repo.DownloadCompareDiff) + g.MatchPath("GET", "/.patch", repo.MustBeNotEmpty, repo.DownloadComparePatch) + g.MatchPath("GET", "/<*:*>", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff) + g.MatchPath("POST", "/<*:*>", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) + }) m.Get("/pulls/new/*", repo.PullsNewRedirect) }, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{reponame}": repo code: find, compare, list diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index fa41917aab..37a358a26b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -8108,8 +8108,10 @@ }, "/repos/{owner}/{repo}/compare/{basehead}": { "get": { + "description": "By default returns JSON commit comparison information. The raw diff or patch can be\nrequested with the `output` query parameter set to `diff` or `patch` respectively.\n", "produces": [ - "application/json" + "application/json", + "text/plain" ], "tags": [ "repository" @@ -8133,10 +8135,20 @@ }, { "type": "string", - "description": "compare two branches or commits", + "description": "compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs, including branch names that contain slashes.", "name": "basehead", "in": "path", "required": true + }, + { + "enum": [ + "diff", + "patch" + ], + "type": "string", + "description": "return the raw comparison as `diff` or `patch` instead of JSON", + "name": "output", + "in": "query" } ], "responses": { diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 68a495d546..7829544526 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -19468,6 +19468,7 @@ }, "/repos/{owner}/{repo}/compare/{basehead}": { "get": { + "description": "By default returns JSON commit comparison information. The raw diff or patch can be\nrequested with the `output` query parameter set to `diff` or `patch` respectively.\n", "operationId": "repoCompareDiff", "parameters": [ { @@ -19489,13 +19490,25 @@ } }, { - "description": "compare two branches or commits", + "description": "compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs, including branch names that contain slashes.", "in": "path", "name": "basehead", "required": true, "schema": { "type": "string" } + }, + { + "description": "return the raw comparison as `diff` or `patch` instead of JSON", + "in": "query", + "name": "output", + "schema": { + "enum": [ + "diff", + "patch" + ], + "type": "string" + } } ], "responses": { diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go index 0d8cec7776..5bb8cf6bde 100644 --- a/tests/integration/api_repo_compare_test.go +++ b/tests/integration/api_repo_compare_test.go @@ -6,6 +6,7 @@ package integration import ( "net/http" "net/url" + "strings" "testing" auth_model "gitea.dev/models/auth" @@ -62,3 +63,113 @@ func TestAPICompareBranches(t *testing.T) { }) }) } + +func TestAPIDownloadCompareDiffOrPatch(t *testing.T) { + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + + t.Run("BranchToBranchDiff", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + body := resp.Body.String() + assert.Contains(t, body, "diff --git ") + }) + + t.Run("BranchToBranchPatch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b?output=patch").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + body := resp.Body.String() + assert.True(t, strings.HasPrefix(body, "From "), "patch output should start with a format-patch header, got: %q", body[:min(40, len(body))]) + }) + + t.Run("CommitToCommitDiff", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("BranchToCommitDiff", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // 8babce96... is the head of remove-files-b; pairing it with add-csv guarantees a non-empty diff. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...8babce967f21b9dfa6987f943b91093dac58a4f0?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("TwoDotSeparator", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv..remove-files-b?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("SlashedBranchName", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // user2/repo1's `feature/1` branch contains a slash; the route must match it + // without URL-encoding. master and feature/1 happen to share a SHA in the fixture, + // so we only assert the route resolves (200 OK) rather than checking diff content. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/compare/master...feature/1?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + }) + + t.Run("UnknownOutputReturnsJSON", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Only "diff"/"patch" switch to raw output; any other value falls through to JSON. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b?output=foo").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + apiResp := DecodeJSON(t, resp, &api.Compare{}) + assert.Equal(t, 2, apiResp.TotalCommits) + }) + + t.Run("SingleRefImplicitBase", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // No `...`/`..` separator: parseCompareInfo defaults the base to the + // repo's PR target branch (master for repo20) and compares it against + // the given head. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv?output=diff").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("PrivateRepoAnonymous", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // repo16 is private; an unauthenticated request must not leak its existence. + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo16/compare/master...good-sign?output=diff") + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("CrossRepoFork", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13}) + repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11}) + user13Sess := loginUser(t, "user13") + user13Token := getTokenForLoggedInUser(t, user13Sess, auth_model.AccessTokenScopeWriteRepository) + + _, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "cross-repo-diff"}, map[string]string{"hello.txt": "hi\n"}) + require.NoError(t, err) + + req := NewRequest(t, "GET", "/api/v1/repos/user12/repo10/compare/master...user13:cross-repo-diff?output=diff").AddTokenAuth(user13Token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + }) +} diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index ac2e014d92..d5f5bc2cc2 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -167,6 +167,53 @@ Hello from 2 assert.Equal(t, 0, htmlDoc.doc.Find(".pullrequest-form").Length()) } +func TestCompareDownloadDiffOrPatch(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + + t.Run("BranchToBranchDiff", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv...remove-files-b.diff") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("BranchToBranchPatch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv...remove-files-b.patch") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.True(t, strings.HasPrefix(resp.Body.String(), "From "), "patch output should start with a format-patch header") + }) + + t.Run("SingleRefImplicitBase", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv.diff") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "diff --git ") + }) + + t.Run("InvalidBaseRef", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo20/compare/does-not-exist...remove-files-b.diff") + session.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("PrivateRepoAnonymous", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // repo16 is private; an unauthenticated request must not leak its existence. + req := NewRequest(t, "GET", "/user2/repo16/compare/master...good-sign.diff") + MakeRequest(t, req, http.StatusNotFound) + }) +} + func TestCompareCodeExpand(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) From 0eba0e371f360eb91af2df3b881b6ee7bca91d2f Mon Sep 17 00:00:00 2001 From: metsw24-max Date: Mon, 15 Jun 2026 22:44:14 +0530 Subject: [PATCH 4/7] fix(packages): validate module version in goproxy ParsePackage (#38104) **Unvalidated version in goproxy ParsePackage** The module version is read straight from the zip directory path and never checked, so a crafted upload can leave a newline in it; `EnumeratePackageVersions` then writes each stored version on its own line for the `@v/list` endpoint, letting a module advertise fabricated versions to `go` clients. Validated the parsed version with `semver.IsValid` inside the parser, matching the version checks the other package parsers already do. Co-authored-by: Lunny Xiao --- go.mod | 2 +- modules/packages/goproxy/metadata.go | 10 ++++++++++ modules/packages/goproxy/metadata_test.go | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a5f42c6caf..9d23a99410 100644 --- a/go.mod +++ b/go.mod @@ -105,6 +105,7 @@ require ( go.yaml.in/yaml/v4 v4.0.0-rc.5 golang.org/x/crypto v0.53.0 golang.org/x/image v0.42.0 + golang.org/x/mod v0.37.0 golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.21.0 @@ -267,7 +268,6 @@ require ( go.uber.org/zap/exp v0.3.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect - golang.org/x/mod v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect diff --git a/modules/packages/goproxy/metadata.go b/modules/packages/goproxy/metadata.go index b19738db56..db50ac7faa 100644 --- a/modules/packages/goproxy/metadata.go +++ b/modules/packages/goproxy/metadata.go @@ -10,6 +10,8 @@ import ( "strings" "gitea.dev/modules/util" + + "golang.org/x/mod/semver" ) const ( @@ -20,6 +22,7 @@ const ( var ( ErrInvalidStructure = util.NewInvalidArgumentErrorf("package has invalid structure") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") ErrGoModFileTooLarge = util.NewInvalidArgumentErrorf("go.mod file is too large") ) @@ -54,6 +57,13 @@ func ParsePackage(r io.ReaderAt, size int64) (*Package, error) { Name: strings.TrimSuffix(nameAndVersion, "@"+parts[1]), Version: versionParts[0], } + + // the version is taken verbatim from the zip path and later written + // one per line into the @v/list proxy response, so it has to be a + // valid module version (no newlines or other stray characters) + if !semver.IsValid(p.Version) { + return nil, ErrInvalidVersion + } } if len(versionParts) > 1 { diff --git a/modules/packages/goproxy/metadata_test.go b/modules/packages/goproxy/metadata_test.go index 4e7f394f8b..24db359651 100644 --- a/modules/packages/goproxy/metadata_test.go +++ b/modules/packages/goproxy/metadata_test.go @@ -59,6 +59,16 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, "module gitea.com/go-gitea/gitea", p.GoMod) }) + t.Run("InvalidVersion", func(t *testing.T) { + data := createArchive(map[string][]byte{ + packageName + "@v1.0.0\nv99.0.0/go.mod": []byte("module " + packageName), + }) + + p, err := ParsePackage(data, int64(data.Len())) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + }) + t.Run("Valid", func(t *testing.T) { data := createArchive(map[string][]byte{ packageName + "@" + packageVersion + "/subdir/go.mod": []byte("invalid"), From 7997c1ccadf2f0b1a0275a499f0c95c182c79895 Mon Sep 17 00:00:00 2001 From: bircni Date: Mon, 15 Jun 2026 19:55:31 +0200 Subject: [PATCH 5/7] fix(pull): preserve squash message trailers and additional commit messages (#37954) * Closes #37950 * Closes #37946 * Fixes https://github.com/go-gitea/gitea/issues/37529 --------- Co-authored-by: wxiaoguang --- modules/git/commit_message.go | 4 +- modules/git/commit_message_test.go | 2 + services/pull/pull.go | 143 +++++++++++++++------------ services/pull/pull_test.go | 59 ++++++++--- tests/integration/pull_merge_test.go | 2 +- 5 files changed, 128 insertions(+), 82 deletions(-) diff --git a/modules/git/commit_message.go b/modules/git/commit_message.go index 8fd3601f0d..0729609d87 100644 --- a/modules/git/commit_message.go +++ b/modules/git/commit_message.go @@ -70,9 +70,11 @@ func (c *CommitMessage) MessageTrailer() CommitMessageTrailerValues { var commitMessageTrailerSplit = sync.OnceValue(func() *regexp.Regexp { // the sep is either something like "\n---\n" or "\n\n" in the body, or at the start of the body like "---\n" - return regexp.MustCompile(`(?s)^(?P.*?)(?P^|^\n|^-{3,}\n|\n-{3,}\n|\n\n)(?P(?:[A-Za-z0-9][-A-Za-z0-9]*:[^\n]*\n?)*)$`) + return regexp.MustCompile(`(?s)^(?P.*?)(?P^|^\n|^-{3,}\n+|\n-{3,}\n+|\n\n)(?P(?:[A-Za-z0-9][-A-Za-z0-9]*:[^\n]*\n?)*\n*)$`) }) +// CommitMessageSplitTrailer tries to split the message by the trailer separator +// content + sep + trailer will reconstruct the original message func CommitMessageSplitTrailer(s string) (content, sep, trailer string) { s = util.NormalizeStringEOL(s) re := commitMessageTrailerSplit() diff --git a/modules/git/commit_message_test.go b/modules/git/commit_message_test.go index 049f1c03f7..1d13f81803 100644 --- a/modules/git/commit_message_test.go +++ b/modules/git/commit_message_test.go @@ -26,8 +26,10 @@ func TestCommitMessageTrailer(t *testing.T) { {"a", "a", "", ""}, {"a\n\nk", "a\n\nk", "", ""}, {"a\n\nk:v", "a", "\n\n", "k:v"}, + {"a\n\nk:v\n\n", "a", "\n\n", "k:v\n\n"}, {"a\n--\nk:v", "a\n--\nk:v", "", ""}, {"a\n---\nk:v", "a", "\n---\n", "k:v"}, + {"a\n\n---\n\nk:v", "a\n", "\n---\n\n", "k:v"}, {"k: v", "", "", "k: v"}, {"\nk:v", "", "\n", "k:v"}, diff --git a/services/pull/pull.go b/services/pull/pull.go index 4491406b33..13f8ffa8df 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -4,14 +4,15 @@ package pull import ( + "bytes" "context" "errors" "fmt" "io" - "regexp" "slices" "strings" "time" + "unicode" "unicode/utf8" "gitea.dev/models/db" @@ -767,8 +768,6 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re return errors.Join(errs...) } -var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`) - // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one) func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string { if err := pr.LoadIssue(ctx); err != nil { @@ -819,54 +818,44 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ return "" } - posterSig := pr.Issue.Poster.NewGitSig().String() + mergeMessage := strings.TrimSpace(pr.Issue.Content) // use PR's title and description as squash commit message + if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages { + mergeMessage = formatSquashMergeCommitMessages(limitedCommits) // use PR's commit messages as squash commit message + } + coAuthors := collectSquashMergeCommitCoAuthors(ctx, gitRepo, pr, headCommitRef, mergeBaseRef, limit, limitedCommits) + return buildSquashMergeCommitMessages(mergeMessage, coAuthors) +} - uniqueAuthors := make(container.Set[string]) - authors := make([]string, 0, len(limitedCommits)) - stringBuilder := strings.Builder{} - - if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages { - // use PR's title and description as squash commit message - message := strings.TrimSpace(pr.Issue.Content) - stringBuilder.WriteString(message) - if stringBuilder.Len() > 0 { - stringBuilder.WriteRune('\n') - if !commitMessageTrailersPattern.MatchString(message) { - // TODO: this trailer check doesn't work with the separator line added below for the co-authors - stringBuilder.WriteRune('\n') - } - } - } else { - // use PR's commit messages as squash commit message - // commits list is in reverse chronological order - maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize - for _, commit := range slices.Backward(limitedCommits) { - msg := strings.TrimSpace(commit.MessageUTF8()) - if msg == "" { - continue - } - - // This format follows GitHub's squash commit message style, - // even if there are other "* " in the commit message body, they are written as-is. - // Maybe, ideally, we should indent those lines too. - _, _ = fmt.Fprintf(&stringBuilder, "* %s\n\n", msg) - if maxMsgSize > 0 && stringBuilder.Len() >= maxMsgSize { - tmp := stringBuilder.String() - wasValidUtf8 := utf8.ValidString(tmp) - tmp = tmp[:maxMsgSize] + "..." - if wasValidUtf8 { - // If the message was valid UTF-8 before truncation, ensure it remains valid after truncation - // For non-utf8 messages, we can't do much about it, end users should use utf-8 as much as possible - tmp = strings.ToValidUTF8(tmp, "") - } - stringBuilder.Reset() - stringBuilder.WriteString(tmp) - break - } - } +func buildSquashMergeCommitMessages(mergeMessage string, coAuthors []string) string { + if len(coAuthors) == 0 { + return mergeMessage } - // collect co-authors + msgContent, msgSep, msgTrailer := git.CommitMessageSplitTrailer(mergeMessage) + if (msgSep == "" || msgSep == "\n\n") && msgTrailer == "" { + msgContent = strings.TrimRightFunc(msgContent, unicode.IsSpace) + msgSep = "\n\n---------\n\n" + } + var sb strings.Builder + sb.WriteString(msgContent) + sb.WriteString(msgSep) + if msgTrailer = strings.TrimSpace(msgTrailer); msgTrailer != "" { + sb.WriteString(msgTrailer) + sb.WriteRune('\n') + } + for _, author := range coAuthors { + sb.WriteString(git.CoAuthoredByTrailer + ": ") + sb.WriteString(author) + sb.WriteRune('\n') + } + return sb.String() +} + +func collectSquashMergeCommitCoAuthors(ctx context.Context, gitRepo *git.Repository, pr *issues_model.PullRequest, headCommitRef, mergeBaseRef git.RefName, limitFirst int, limitedCommits []*git.Commit) []string { + posterSig := pr.Issue.Poster.NewGitSig().String() + uniqueAuthors := make(container.Set[string]) + authors := make([]string, 0, len(limitedCommits)) + for _, commit := range limitedCommits { authorString := commit.Author.String() if uniqueAuthors.Add(authorString) && authorString != posterSig { @@ -880,14 +869,14 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ } // collect the remaining authors - if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors { - skip := limit - limit = 30 + if limitFirst >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors { + skip := limitFirst + batchLimit := 30 for { - commits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, limit, skip) + commits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, batchLimit, skip) if err != nil { log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err) - return "" + return authors } if len(commits) == 0 { break @@ -901,22 +890,46 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ } } } - skip += limit + skip += batchLimit + } + } + return authors +} + +func formatSquashMergeCommitMessages(commits []*git.Commit) string { + maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize + sb := &bytes.Buffer{} + // commits list is in reverse chronological order + for _, commit := range slices.Backward(commits) { + msg := strings.TrimSpace(commit.MessageUTF8()) + if msg == "" { + continue + } + + // This format follows GitHub's squash commit message style, + // even if there are other "* " in the commit message body, they are written as-is. + // Maybe, ideally, we should indent those lines too. + _, _ = fmt.Fprintf(sb, "* %s\n\n", msg) + if maxMsgSize > 0 && sb.Len() >= maxMsgSize { + break } } - if stringBuilder.Len() > 0 && len(authors) > 0 { - // TODO: this separator line doesn't work with the trailer check (commitMessageTrailersPattern) above - stringBuilder.WriteString("---------\n\n") + buf := bytes.TrimSpace(sb.Bytes()) + if maxMsgSize > 0 && len(buf) > maxMsgSize { + buf = buf[:maxMsgSize] + for { + r, sz := utf8.DecodeLastRune(buf) + if r == utf8.RuneError && sz == 1 { + buf = buf[:len(buf)-1] + continue + } + break + } + buf = append(buf, '.', '.', '.') } - - for _, author := range authors { - stringBuilder.WriteString(git.CoAuthoredByTrailer + ": ") - stringBuilder.WriteString(author) - stringBuilder.WriteRune('\n') - } - - return stringBuilder.String() + buf = append(buf, '\n', '\n') + return util.UnsafeBytesToString(buf) } // GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go index 0648cd383d..554c9ed577 100644 --- a/services/pull/pull_test.go +++ b/services/pull/pull_test.go @@ -11,28 +11,33 @@ import ( repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" "gitea.dev/models/unittest" + "gitea.dev/modules/git" "gitea.dev/modules/gitrepo" + "gitea.dev/modules/setting" + "gitea.dev/modules/test" "github.com/stretchr/testify/assert" ) // TODO TestPullRequest_PushToBaseRepo -func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) { - // Not a valid trailer section - assert.False(t, commitMessageTrailersPattern.MatchString("")) - assert.False(t, commitMessageTrailersPattern.MatchString("No trailer.")) - assert.False(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nNot a trailer due to following text.")) - assert.False(t, commitMessageTrailersPattern.MatchString("Message body not correctly separated from trailer section by empty line.\nSigned-off-by: Bob ")) - // Valid trailer section - assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob ")) - assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nOther-Trailer: Value")) - assert.True(t, commitMessageTrailersPattern.MatchString("Message body correctly separated from trailer section by empty line.\n\nSigned-off-by: Bob ")) - assert.True(t, commitMessageTrailersPattern.MatchString("Multiple trailers.\n\nSigned-off-by: Bob \nOther-Trailer: Value")) - assert.True(t, commitMessageTrailersPattern.MatchString("Newline after trailer section.\n\nSigned-off-by: Bob \n")) - assert.True(t, commitMessageTrailersPattern.MatchString("No space after colon is accepted.\n\nSigned-off-by:Bob ")) - assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob ")) - assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value")) +func TestPullRequest_FormatSquashMergeCommitMessages(t *testing.T) { + oldest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 1"}} + newest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 2\n\nCommit description."}} + + defer test.MockVariableValue(&setting.Repository.PullRequest.DefaultMergeMessageSize, 0)() + + assert.Equal(t, "* commit msg 1\n\n* commit msg 2\n\nCommit description.\n\n", formatSquashMergeCommitMessages([]*git.Commit{newest, oldest})) + + utf8Msg := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "🌞"}} + setting.Repository.PullRequest.DefaultMergeMessageSize = 3 + assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg})) + setting.Repository.PullRequest.DefaultMergeMessageSize = 4 + assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg})) + setting.Repository.PullRequest.DefaultMergeMessageSize = 5 + assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg})) + setting.Repository.PullRequest.DefaultMergeMessageSize = 6 + assert.Equal(t, "* 🌞\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg})) } func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) { @@ -88,3 +93,27 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage) } + +func TestBuildSquashMergeCommitMessages(t *testing.T) { + cases := []struct { + msg string + coAuthors []string + expected string + }{ + {"title", nil, "title"}, + {"title", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"}, + {"title\n\n", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"}, + {"title\n\nKey: val", []string{"the-user"}, "title\n\nKey: val\nCo-authored-by: the-user\n"}, + {"title\n\n----\nKey: val", []string{"the-user"}, "title\n\n----\nKey: val\nCo-authored-by: the-user\n"}, + {"title\n\n----\nKey: val\n\n", []string{"the-user"}, "title\n\n----\nKey: val\nCo-authored-by: the-user\n"}, + + {"title\n\nbody", nil, "title\n\nbody"}, + {"title\n\nbody", []string{"the-user"}, "title\n\nbody\n\n---------\n\nCo-authored-by: the-user\n"}, + {"title\n\nbody\n\nKey: val", []string{"the-user"}, "title\n\nbody\n\nKey: val\nCo-authored-by: the-user\n"}, + {"title\n\nbody\n\n----\nKey: val", []string{"the-user"}, "title\n\nbody\n\n----\nKey: val\nCo-authored-by: the-user\n"}, + } + for _, c := range cases { + msg := buildSquashMergeCommitMessages(c.msg, c.coAuthors) + assert.Equal(t, c.expected, msg, "msg: %s", c.msg) + } +} diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 8469057620..ae1f8a3490 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -1272,7 +1272,7 @@ Commit description. commitMessage: `loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message`, }, }, - expectedMessage: `* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...`, + expectedMessage: "* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...\n\n", }, { name: "Test Co-authored-by", From 8ff8422307d88467610aa30cc49a922630083ce6 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 15 Jun 2026 13:23:13 -0700 Subject: [PATCH 6/7] chore(deps): update module github.com/go-swagger/go-swagger to v0.34.1 (#38122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [github.com/go-swagger/go-swagger](https://redirect.github.com/go-swagger/go-swagger) | `v0.34.0` → `v0.34.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-swagger%2fgo-swagger/v0.34.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-swagger%2fgo-swagger/v0.34.0/v0.34.1?slim=true) | --- ### Release Notes
go-swagger/go-swagger (github.com/go-swagger/go-swagger) ### [`v0.34.1`](https://redirect.github.com/go-swagger/go-swagger/releases/tag/v0.34.1) [Compare Source](https://redirect.github.com/go-swagger/go-swagger/compare/v0.34.0...v0.34.1) go-swagger release 0.34.1 *** Released on 2026 Jun 05 ##### [0.34.1](https://redirect.github.com/go-swagger/go-swagger/tree/v0.34.1) - 2026-06-05 Fixed regression on initialisms (codegen) **Full Changelog**: 7 commits in this release. *** ##### Fixed bugs - fix(codegen): fixed regression from v0.34.0 : initialisms skipped by [@​fredbi](https://redirect.github.com/fredbi) in [#​3360](https://redirect.github.com/go-swagger/go-swagger/pull/3360) [...](https://redirect.github.com/go-swagger/go-swagger/commit/e5c5fec88a5ad02a2ded264bbecd276b83340d12) ##### Miscellaneous tasks - fix(ci): fixed release announcement to discord by [@​fredbi](https://redirect.github.com/fredbi) in [#​3354](https://redirect.github.com/go-swagger/go-swagger/pull/3354) [...](https://redirect.github.com/go-swagger/go-swagger/commit/d9244f53d41c79c3788867a4a031bea920cd418b) - ci: fixed event by [@​fredbi](https://redirect.github.com/fredbi) [...](https://redirect.github.com/go-swagger/go-swagger/commit/ba881e358085d4c4c004c5f61697d74f65390fa1) - ci: repair release by [@​fredbi](https://redirect.github.com/fredbi) in [#​3353](https://redirect.github.com/go-swagger/go-swagger/pull/3353) [...](https://redirect.github.com/go-swagger/go-swagger/commit/70b7c214ce3d9174956293f45f54d06ee3c4112f) ##### Updates - chore(deps): bump the development-dependencies group with 3 updates by [@​dependabot\[bot\]](https://redirect.github.com/dependabot\[bot]) in [#​3359](https://redirect.github.com/go-swagger/go-swagger/pull/3359) [...](https://redirect.github.com/go-swagger/go-swagger/commit/65d6af084bb829d38bf53fc4d6cdc4a0cacec3ba) - chore(deps): bump golang from `91eda97` to `f23e8b2` in the development-dependencies group across 1 directory by [@​dependabot\[bot\]](https://redirect.github.com/dependabot\[bot]) in [#​3356](https://redirect.github.com/go-swagger/go-swagger/pull/3356) [...](https://redirect.github.com/go-swagger/go-swagger/commit/b1dfdf0c14f4de2d90b6de3b64619effcc82d67c) - chore(deps): bump the development-dependencies group with 2 updates by [@​dependabot\[bot\]](https://redirect.github.com/dependabot\[bot]) in [#​3355](https://redirect.github.com/go-swagger/go-swagger/pull/3355) [...](https://redirect.github.com/go-swagger/go-swagger/commit/340826c27f8ecd6d597d708505db83bf16033dcd) *** ##### People who contributed to this release - [@​fredbi](https://redirect.github.com/fredbi) *** **[go-swagger](https://redirect.github.com/go-swagger/go-swagger) license terms** [![License][license-badge]][license-url] [license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg [license-url]: https://redirect.github.com/go-swagger/go-swagger/?tab=Apache-2.0-1-ov-file#readme *** Released by [GoReleaser](https://redirect.github.com/goreleaser/goreleaser).
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Only on Monday (`* * * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). Co-authored-by: bircni --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 939264233f..86d21dcf44 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-che GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.0 # renovate: datasource=go +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.1 # renovate: datasource=go XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go From 89d11314f98e2826b30793c9b7da27d65faf9ec3 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 16 Jun 2026 01:27:50 +0000 Subject: [PATCH 7/7] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 9bff2c24be..c1e11779d1 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -2205,10 +2205,10 @@ "repo.settings.trust_model.collaborator.desc": "Déanfar sínithe bailí ó chomhoibritheoirí an stórais seo a mharcáil mar \"iontaofa\", cibé acu a mheaitseálann siad an tiomnóir nó nach meaitseálann. Seachas sin, déanfar sínithe bailí a mharcáil mar \"neamhiontaofa\" má mheaitseálann an síniú an tiomnóir agus \"gan mheaitseáil\" mura bhfuil.", "repo.settings.trust_model.committer": "Coimisitheoir", "repo.settings.trust_model.committer.long": "Tiomnaithe: Sínithe muiníne a mheaitseálann tiomnóirí. Meaitseálann sé seo iompar GitHub agus cuirfidh sé iallach ar thiomnóirí atá sínithe ag Gitea Gitea a bheith mar an tiomnóir.", - "repo.settings.trust_model.committer.desc": "Ní mharcálfar sínithe bailí mar \"iontaofa\" ach amháin má mheaitseálann siad an tiomnaí, nó marcálfar iad mar \"gan mheaitseáil\". Cuireann sé seo iallach ar Gitea a bheith ina tiomnaí ar thiomnuithe sínithe, agus an tiomnaí iarbhír marcáilte mar Chomhúdaraithe ag: agus Co-thiomnaithe ag: leantóir sa tiomnú. Caithfidh eochair réamhshocraithe Gitea a bheith ag teacht le húsáideoir sa bhunachar sonraí.", + "repo.settings.trust_model.committer.desc": "Ní mharcálfar sínithe bailí mar \"iontaofa\" ach amháin má mheaitseálann siad an tiomnóir, nó marcálfar iad mar \"gan mheaitseáil\". Cuireann sé seo iallach ar Gitea a bheith ina thiomnóir ar thiomnuithe sínithe, agus an tiomnóir iarbhír marcáilte mar leantóir Co-authored-by: sa thiomnú. Caithfidh eochair réamhshocraithe Gitea a bheith ag teacht le húsáideoir sa bhunachar sonraí.", "repo.settings.trust_model.collaboratorcommitter": "Comhoibritheo+Coimiteoir", "repo.settings.trust_model.collaboratorcommitter.long": "Comhoibrí+Coiste: sínithe muiníne ó chomhoibrithe a mheaitseálann an tiomnóir", - "repo.settings.trust_model.collaboratorcommitter.desc": "Marcálfar sínithe bailí ó chomhoibritheoirí an stórais seo mar \"iontaofa\" má mheaitseálann siad an tiomnaí. Seachas sin, marcálfar sínithe bailí mar \"neamhiontaofa\" má mheaitseálann an síniú an tiomnaí agus \"gan mheaitseáil\" murach sin. Cuirfidh sé seo iallach ar Gitea a bheith marcáilte mar an tiomnaí ar thiomnuithe sínithe, agus an tiomnaí iarbhír marcáilte mar Chomhúdaraithe ag: agus Co-Tiomnaithe ag: leantóir sa tiomnú. Ní mór don eochair réamhshocraithe Gitea a bheith ag teacht le húsáideoir sa bhunachar sonraí.", + "repo.settings.trust_model.collaboratorcommitter.desc": "Marcálfar sínithe bailí ó chomhoibritheoirí an stórais seo mar \"iontaofa\" má mheaitseálann siad an tiomnóir. Seachas sin, marcálfar sínithe bailí mar \"neamhiontaofa\" má mheaitseálann an síniú an tiomnóir agus \"gan mheaitseáil\" murach sin. Cuirfidh sé seo iallach ar Gitea a bheith marcáilte mar an tiomnóir ar thiomnuithe sínithe, agus an tiomnóir iarbhír marcáilte mar leantóir Co-Authored-By: sa tiomnú. Ní mór don eochair réamhshocraithe Gitea a bheith ag teacht le húsáideoir sa bhunachar sonraí.", "repo.settings.wiki_delete": "Scrios Sonraí Vicí", "repo.settings.wiki_delete_desc": "Tá sonraí wiki stóras a scriosadh buan agus ní féidir iad a chur ar ais.", "repo.settings.wiki_delete_notices_1": "- Scriosfaidh agus díchumasóidh sé seo an stóras vicí do %s go buan.", @@ -2599,6 +2599,9 @@ "repo.diff.review.reject": "Iarr athruithe", "repo.diff.review.self_approve": "Ní féidir le húdair iarratais tarraing a n-iarratas tarraingthe féin a chead", "repo.diff.committed_by": "tiomanta ag", + "repo.diff.coauthored_by": "comhúdaraithe ag", + "repo.commits.avatar_stack_and": "agus", + "repo.commits.avatar_stack_people": "%d duine", "repo.diff.protected": "Cosanta", "repo.diff.image.side_by_side": "Taobh le Taobh", "repo.diff.image.swipe": "Scaoil", @@ -2862,6 +2865,14 @@ "org.teams.all_repositories_read_permission_desc": "Tugann an fhoireann seo rochtain do Léamh ar gach stórais: is féidir le baill amharc ar stórais agus iad a chlónáil.", "org.teams.all_repositories_write_permission_desc": "Tugann an fhoireann seo rochtain do Scríobh ar gach stórais: is féidir le baill léamh ó stórais agus iad a bhrú chucu.", "org.teams.all_repositories_admin_permission_desc": "Tugann an fhoireann seo rochtain Riarthóra ar gach stóras: is féidir le comhaltaí léamh, brú a dhéanamh agus comhoibritheoirí a chur le stórtha.", + "org.teams.visibility": "Infheictheacht", + "org.teams.visibility_private": "Príobháideach", + "org.teams.visibility_private_helper": "Le feiceáil ag baill foirne agus úinéirí eagraíochta amháin.", + "org.teams.visibility_limited": "Teoranta", + "org.teams.visibility_limited_helper": "Infheicthe ag gach ball den eagraíocht seo.", + "org.teams.visibility_public": "Poiblí", + "org.teams.visibility_public_helper": "Infheicthe ag aon úsáideoir atá sínithe isteach.", + "org.teams.owners_visibility_fixed": "Ní féidir infheictheacht fhoireann na nÚinéirí a athrú.", "org.teams.invite.title": "Tugadh cuireadh duit dul isteach i bhfoireann %s san eagraíocht %s.", "org.teams.invite.by": "Ar cuireadh ó %s", "org.teams.invite.description": "Cliceáil ar an gcnaipe thíos le do thoil chun dul isteach san fhoireann.", @@ -3774,6 +3785,7 @@ "actions.runs.no_matching_online_runner_helper": "Gan aon reathaí ar líne a mheaitseáil le lipéad: %s", "actions.runs.no_job_without_needs": "Caithfidh post amháin ar a laghad a bheith sa sreabhadh oibre gan spleáchas.", "actions.runs.no_job": "Caithfidh post amháin ar a laghad a bheith sa sreabhadh oibre", + "actions.runs.invalid_reusable_workflow_uses": "Sreabhadh oibre in-athúsáidte neamhbhailí \"úsáidí\": %s", "actions.runs.actor": "Aisteoir", "actions.runs.status": "Stádas", "actions.runs.actors_no_select": "Gach aisteoir", @@ -3794,13 +3806,17 @@ "actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre", "actions.runs.summary": "Achoimre", "actions.runs.all_jobs": "Gach post", + "actions.runs.job_summaries": "Achoimrí poist", "actions.runs.expand_caller_jobs": "Taispeáin poist an ghlaoiteora sreabha oibre in-athúsáidte seo", "actions.runs.collapse_caller_jobs": "Folaigh poist an ghlaoiteora sreabha oibre in-athúsáidte seo", "actions.runs.attempt": "Iarracht", "actions.runs.latest": "Is déanaí", "actions.runs.latest_attempt": "An iarracht is déanaí", "actions.runs.triggered_via": "Spreagtha trí %s", - "actions.runs.total_duration": "Fad iomlán:", + "actions.runs.rerun_triggered": "Athrith spreagtha", + "actions.runs.back_to_pull_request": "Ar ais chuig an iarratas tarraingthe", + "actions.runs.back_to_workflow": "Ar ais chuig an sreabhadh oibre", + "actions.runs.total_duration": "Fad iomlán", "actions.runs.workflow_dependencies": "Spleáchais ar Shreabhadh Oibre", "actions.runs.graph_jobs_count_1": "%d post", "actions.runs.graph_jobs_count_n": "%d poist",