mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-17 19:10:22 +03:00
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 <bircni@icloud.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+6
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user