- Add new `Compare` struct to represent comparison between two commits - Introduce new API endpoint `/compare/*` to get commit comparison information - Create new file `repo_compare.go` with the `Compare` struct definition - Add new file `compare.go` in `routers/api/v1/repo` to handle comparison logic - Add new file `compare.go` in `routers/common` to define `CompareInfo` struct - Refactor `ParseCompareInfo` function to use `common.CompareInfo` struct - Update Swagger documentation to include the new API endpoint for commit comparison - Remove duplicate `CompareInfo` struct from `routers/web/repo/compare.go` - Adjust base path in Swagger template to be relative (`/api/v1`) GitHub API https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits --------- Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>tags/v1.22.0-rc1
// Copyright 2024 The Gitea Authors. All rights reserved. | |||||
// SPDX-License-Identifier: MIT | |||||
package structs | |||||
// Compare represents a comparison between two commits. | |||||
type Compare struct { | |||||
TotalCommits int `json:"total_commits"` // Total number of commits in the comparison. | |||||
Commits []*Commit `json:"commits"` // List of commits in the comparison. | |||||
} |
m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate) | m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate) | ||||
m.Group("/{username}/{reponame}", func() { | m.Group("/{username}/{reponame}", func() { | ||||
m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff) | |||||
m.Combo("").Get(reqAnyRepoReader(), repo.Get). | m.Combo("").Get(reqAnyRepoReader(), repo.Get). | ||||
Delete(reqToken(), reqOwner(), repo.Delete). | Delete(reqToken(), reqOwner(), repo.Delete). | ||||
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) | Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) |
// Copyright 2024 The Gitea Authors. All rights reserved. | |||||
// SPDX-License-Identifier: MIT | |||||
package repo | |||||
import ( | |||||
"net/http" | |||||
"strings" | |||||
user_model "code.gitea.io/gitea/models/user" | |||||
"code.gitea.io/gitea/modules/gitrepo" | |||||
api "code.gitea.io/gitea/modules/structs" | |||||
"code.gitea.io/gitea/services/context" | |||||
"code.gitea.io/gitea/services/convert" | |||||
) | |||||
// CompareDiff compare two branches or commits | |||||
func CompareDiff(ctx *context.APIContext) { | |||||
// swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} Get commit comparison information | |||||
// --- | |||||
// summary: Get commit comparison information | |||||
// produces: | |||||
// - application/json | |||||
// parameters: | |||||
// - name: owner | |||||
// in: path | |||||
// description: owner of the repo | |||||
// type: string | |||||
// required: true | |||||
// - name: repo | |||||
// in: path | |||||
// description: name of the repo | |||||
// type: string | |||||
// required: true | |||||
// - name: basehead | |||||
// in: path | |||||
// description: compare two branches or commits | |||||
// type: string | |||||
// required: true | |||||
// responses: | |||||
// "200": | |||||
// "$ref": "#/responses/Compare" | |||||
// "404": | |||||
// "$ref": "#/responses/notFound" | |||||
if ctx.Repo.GitRepo == nil { | |||||
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) | |||||
if err != nil { | |||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err) | |||||
return | |||||
} | |||||
ctx.Repo.GitRepo = gitRepo | |||||
defer gitRepo.Close() | |||||
} | |||||
infoPath := ctx.Params("*") | |||||
infos := []string{ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository.DefaultBranch} | |||||
if infoPath != "" { | |||||
infos = strings.SplitN(infoPath, "...", 2) | |||||
if len(infos) != 2 { | |||||
if infos = strings.SplitN(infoPath, "..", 2); len(infos) != 2 { | |||||
infos = []string{ctx.Repo.Repository.DefaultBranch, infoPath} | |||||
} | |||||
} | |||||
} | |||||
_, _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{ | |||||
Base: infos[0], | |||||
Head: infos[1], | |||||
}) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
defer headGitRepo.Close() | |||||
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") | |||||
files := ctx.FormString("files") == "" || ctx.FormBool("files") | |||||
apiCommits := make([]*api.Commit, 0, len(ci.Commits)) | |||||
userCache := make(map[string]*user_model.User) | |||||
for i := 0; i < len(ci.Commits); i++ { | |||||
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.Commits[i], userCache, | |||||
convert.ToCommitOptions{ | |||||
Stat: true, | |||||
Verification: verification, | |||||
Files: files, | |||||
}) | |||||
if err != nil { | |||||
ctx.ServerError("toCommit", err) | |||||
return | |||||
} | |||||
apiCommits = append(apiCommits, apiCommit) | |||||
} | |||||
ctx.JSON(http.StatusOK, &api.Compare{ | |||||
TotalCommits: len(ci.Commits), | |||||
Commits: apiCommits, | |||||
}) | |||||
} |
// in:body | // in:body | ||||
Body api.NewIssuePinsAllowed `json:"body"` | Body api.NewIssuePinsAllowed `json:"body"` | ||||
} | } | ||||
// swagger:response Compare | |||||
type swaggerCompare struct { | |||||
// in:body | |||||
Body api.Compare `json:"body"` | |||||
} |
// Copyright 2024 The Gitea Authors. All rights reserved. | |||||
// SPDX-License-Identifier: MIT | |||||
package common | |||||
import ( | |||||
repo_model "code.gitea.io/gitea/models/repo" | |||||
user_model "code.gitea.io/gitea/models/user" | |||||
"code.gitea.io/gitea/modules/git" | |||||
) | |||||
// CompareInfo represents the collected results from ParseCompareInfo | |||||
type CompareInfo struct { | |||||
HeadUser *user_model.User | |||||
HeadRepo *repo_model.Repository | |||||
HeadGitRepo *git.Repository | |||||
CompareInfo *git.CompareInfo | |||||
BaseBranch string | |||||
HeadBranch string | |||||
DirectComparison bool | |||||
} |
api "code.gitea.io/gitea/modules/structs" | api "code.gitea.io/gitea/modules/structs" | ||||
"code.gitea.io/gitea/modules/typesniffer" | "code.gitea.io/gitea/modules/typesniffer" | ||||
"code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
"code.gitea.io/gitea/routers/common" | |||||
"code.gitea.io/gitea/services/context" | "code.gitea.io/gitea/services/context" | ||||
"code.gitea.io/gitea/services/context/upload" | "code.gitea.io/gitea/services/context/upload" | ||||
"code.gitea.io/gitea/services/gitdiff" | "code.gitea.io/gitea/services/gitdiff" | ||||
} | } | ||||
} | } | ||||
// CompareInfo represents the collected results from ParseCompareInfo | |||||
type CompareInfo struct { | |||||
HeadUser *user_model.User | |||||
HeadRepo *repo_model.Repository | |||||
HeadGitRepo *git.Repository | |||||
CompareInfo *git.CompareInfo | |||||
BaseBranch string | |||||
HeadBranch string | |||||
DirectComparison bool | |||||
} | |||||
// ParseCompareInfo parse compare info between two commit for preparing comparing references | // ParseCompareInfo parse compare info between two commit for preparing comparing references | ||||
func ParseCompareInfo(ctx *context.Context) *CompareInfo { | |||||
func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { | |||||
baseRepo := ctx.Repo.Repository | baseRepo := ctx.Repo.Repository | ||||
ci := &CompareInfo{} | |||||
ci := &common.CompareInfo{} | |||||
fileOnly := ctx.FormBool("file-only") | fileOnly := ctx.FormBool("file-only") | ||||
// PrepareCompareDiff renders compare diff page | // PrepareCompareDiff renders compare diff page | ||||
func PrepareCompareDiff( | func PrepareCompareDiff( | ||||
ctx *context.Context, | ctx *context.Context, | ||||
ci *CompareInfo, | |||||
ci *common.CompareInfo, | |||||
whitespaceBehavior git.TrustedCmdArgs, | whitespaceBehavior git.TrustedCmdArgs, | ||||
) bool { | ) bool { | ||||
var ( | var ( |
} | } | ||||
} | } | ||||
}, | }, | ||||
"/repos/{owner}/{repo}/compare/{basehead}": { | |||||
"get": { | |||||
"produces": [ | |||||
"application/json" | |||||
], | |||||
"tags": [ | |||||
"Get", | |||||
"commit", | |||||
"comparison" | |||||
], | |||||
"summary": "Get commit comparison information", | |||||
"operationId": "information", | |||||
"parameters": [ | |||||
{ | |||||
"type": "string", | |||||
"description": "owner of the repo", | |||||
"name": "owner", | |||||
"in": "path", | |||||
"required": true | |||||
}, | |||||
{ | |||||
"type": "string", | |||||
"description": "name of the repo", | |||||
"name": "repo", | |||||
"in": "path", | |||||
"required": true | |||||
}, | |||||
{ | |||||
"type": "string", | |||||
"description": "compare two branches or commits", | |||||
"name": "basehead", | |||||
"in": "path", | |||||
"required": true | |||||
} | |||||
], | |||||
"responses": { | |||||
"200": { | |||||
"$ref": "#/responses/Compare" | |||||
}, | |||||
"404": { | |||||
"$ref": "#/responses/notFound" | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
"/repos/{owner}/{repo}/contents": { | "/repos/{owner}/{repo}/contents": { | ||||
"get": { | "get": { | ||||
"produces": [ | "produces": [ | ||||
}, | }, | ||||
"x-go-package": "code.gitea.io/gitea/modules/structs" | "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
}, | }, | ||||
"Compare": { | |||||
"type": "object", | |||||
"title": "Compare represents a comparison between two commits.", | |||||
"properties": { | |||||
"commits": { | |||||
"type": "array", | |||||
"items": { | |||||
"$ref": "#/definitions/Commit" | |||||
}, | |||||
"x-go-name": "Commits" | |||||
}, | |||||
"total_commits": { | |||||
"type": "integer", | |||||
"format": "int64", | |||||
"x-go-name": "TotalCommits" | |||||
} | |||||
}, | |||||
"x-go-package": "code.gitea.io/gitea/modules/structs" | |||||
}, | |||||
"ContentsResponse": { | "ContentsResponse": { | ||||
"description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content", | "description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content", | ||||
"type": "object", | "type": "object", | ||||
} | } | ||||
} | } | ||||
}, | }, | ||||
"Compare": { | |||||
"description": "", | |||||
"schema": { | |||||
"$ref": "#/definitions/Compare" | |||||
} | |||||
}, | |||||
"ContentsListResponse": { | "ContentsListResponse": { | ||||
"description": "ContentsListResponse", | "description": "ContentsListResponse", | ||||
"schema": { | "schema": { |
// Copyright 2024 The Gitea Authors. All rights reserved. | |||||
// SPDX-License-Identifier: MIT | |||||
package integration | |||||
import ( | |||||
"net/http" | |||||
"testing" | |||||
auth_model "code.gitea.io/gitea/models/auth" | |||||
"code.gitea.io/gitea/models/unittest" | |||||
user_model "code.gitea.io/gitea/models/user" | |||||
api "code.gitea.io/gitea/modules/structs" | |||||
"code.gitea.io/gitea/tests" | |||||
"github.com/stretchr/testify/assert" | |||||
) | |||||
func TestAPICompareBranches(t *testing.T) { | |||||
defer tests.PrepareTestEnv(t)() | |||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | |||||
// Login as User2. | |||||
session := loginUser(t, user.Name) | |||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) | |||||
repoName := "repo20" | |||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/%s/compare/add-csv...remove-files-b", repoName). | |||||
AddTokenAuth(token) | |||||
resp := MakeRequest(t, req, http.StatusOK) | |||||
var apiResp *api.Compare | |||||
DecodeJSON(t, resp, &apiResp) | |||||
assert.Equal(t, 2, apiResp.TotalCommits) | |||||
assert.Len(t, apiResp.Commits, 2) | |||||
} |