Skip to content

Commit 536b530

Browse files
committed
feat: implement commit comparison API endpoint
- 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`) Signed-off-by: Bo-Yi Wu <[email protected]>
1 parent 8d14266 commit 536b530

File tree

6 files changed

+461
-16
lines changed

6 files changed

+461
-16
lines changed

modules/structs/repo_compare.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package structs
5+
6+
// Compare represents a comparison between two commits.
7+
type Compare struct {
8+
TotalCommits int `json:"total_commits"` // Total number of commits in the comparison.
9+
Commits []*Commit `json:"commits"` // List of commits in the comparison.
10+
}

routers/api/v1/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,8 @@ func Routes() *web.Route {
10661066
m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate)
10671067

10681068
m.Group("/{username}/{reponame}", func() {
1069+
m.Get("/compare/*", repo.CompareDiff)
1070+
10691071
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
10701072
Delete(reqToken(), reqOwner(), repo.Delete).
10711073
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)

routers/api/v1/repo/compare.go

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"net/http"
8+
"strings"
9+
10+
access_model "code.gitea.io/gitea/models/perm/access"
11+
repo_model "code.gitea.io/gitea/models/repo"
12+
"code.gitea.io/gitea/models/unit"
13+
user_model "code.gitea.io/gitea/models/user"
14+
"code.gitea.io/gitea/modules/git"
15+
"code.gitea.io/gitea/modules/gitrepo"
16+
"code.gitea.io/gitea/modules/log"
17+
api "code.gitea.io/gitea/modules/structs"
18+
"code.gitea.io/gitea/modules/util"
19+
"code.gitea.io/gitea/routers/common"
20+
"code.gitea.io/gitea/services/context"
21+
"code.gitea.io/gitea/services/convert"
22+
)
23+
24+
// ParseCompareInfo parse compare info between two commit for preparing comparing references
25+
func ParseCompareInfo(ctx *context.APIContext) *common.CompareInfo {
26+
baseRepo := ctx.Repo.Repository
27+
ci := &common.CompareInfo{}
28+
29+
fileOnly := ctx.FormBool("file-only")
30+
31+
// Get compared branches information
32+
// A full compare url is of the form:
33+
//
34+
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
35+
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
36+
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
37+
// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}
38+
// 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch}
39+
// 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch}
40+
//
41+
// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.Params("*")
42+
// with the :baseRepo in ctx.Repo.
43+
//
44+
// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
45+
//
46+
// How do we determine the :headRepo?
47+
//
48+
// 1. If :headOwner is not set then the :headRepo = :baseRepo
49+
// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
50+
// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
51+
// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
52+
//
53+
// format: <base branch>...[<head repo>:]<head branch>
54+
// base<-head: master...head:feature
55+
// same repo: master...feature
56+
57+
var (
58+
isSameRepo bool
59+
infoPath string
60+
err error
61+
)
62+
63+
infoPath = ctx.Params("*")
64+
var infos []string
65+
if infoPath == "" {
66+
infos = []string{baseRepo.DefaultBranch, baseRepo.DefaultBranch}
67+
} else {
68+
infos = strings.SplitN(infoPath, "...", 2)
69+
if len(infos) != 2 {
70+
if infos = strings.SplitN(infoPath, "..", 2); len(infos) == 2 {
71+
ci.DirectComparison = true
72+
} else {
73+
infos = []string{baseRepo.DefaultBranch, infoPath}
74+
}
75+
}
76+
}
77+
78+
ci.BaseBranch = infos[0]
79+
80+
// If there is no head repository, it means compare between same repository.
81+
headInfos := strings.Split(infos[1], ":")
82+
if len(headInfos) == 1 {
83+
isSameRepo = true
84+
ci.HeadUser = ctx.Repo.Owner
85+
ci.HeadBranch = headInfos[0]
86+
} else if len(headInfos) == 2 {
87+
headInfosSplit := strings.Split(headInfos[0], "/")
88+
if len(headInfosSplit) == 1 {
89+
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
90+
if err != nil {
91+
if user_model.IsErrUserNotExist(err) {
92+
ctx.NotFound("GetUserByName", nil)
93+
} else {
94+
ctx.ServerError("GetUserByName", err)
95+
}
96+
return nil
97+
}
98+
ci.HeadBranch = headInfos[1]
99+
isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID
100+
if isSameRepo {
101+
ci.HeadRepo = baseRepo
102+
}
103+
} else {
104+
ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
105+
if err != nil {
106+
if repo_model.IsErrRepoNotExist(err) {
107+
ctx.NotFound("GetRepositoryByOwnerAndName", nil)
108+
} else {
109+
ctx.ServerError("GetRepositoryByOwnerAndName", err)
110+
}
111+
return nil
112+
}
113+
if err := ci.HeadRepo.LoadOwner(ctx); err != nil {
114+
if user_model.IsErrUserNotExist(err) {
115+
ctx.NotFound("GetUserByName", nil)
116+
} else {
117+
ctx.ServerError("GetUserByName", err)
118+
}
119+
return nil
120+
}
121+
ci.HeadBranch = headInfos[1]
122+
ci.HeadUser = ci.HeadRepo.Owner
123+
isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
124+
}
125+
} else {
126+
ctx.NotFound("CompareAndPullRequest", nil)
127+
return nil
128+
}
129+
ctx.Repo.PullRequest.SameRepo = isSameRepo
130+
131+
// Check if base branch is valid.
132+
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
133+
baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch)
134+
baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
135+
136+
if !baseIsCommit && !baseIsBranch && !baseIsTag {
137+
// Check if baseBranch is short sha commit hash
138+
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
139+
ci.BaseBranch = baseCommit.ID.String()
140+
} else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
141+
if isSameRepo {
142+
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
143+
} else {
144+
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadRepo.FullName()) + ":" + util.PathEscapeSegments(ci.HeadBranch))
145+
}
146+
return nil
147+
} else {
148+
ctx.NotFound("IsRefExist", nil)
149+
return nil
150+
}
151+
}
152+
153+
// Now we have the repository that represents the base
154+
155+
// The current base and head repositories and branches may not
156+
// actually be the intended branches that the user wants to
157+
// create a pull-request from - but also determining the head
158+
// repo is difficult.
159+
160+
// We will want therefore to offer a few repositories to set as
161+
// our base and head
162+
163+
// 1. First if the baseRepo is a fork get the "RootRepo" it was
164+
// forked from
165+
var rootRepo *repo_model.Repository
166+
if baseRepo.IsFork {
167+
err = baseRepo.GetBaseRepo(ctx)
168+
if err != nil {
169+
if !repo_model.IsErrRepoNotExist(err) {
170+
ctx.ServerError("Unable to find root repo", err)
171+
return nil
172+
}
173+
} else {
174+
rootRepo = baseRepo.BaseRepo
175+
}
176+
}
177+
178+
// 2. Now if the current user is not the owner of the baseRepo,
179+
// check if they have a fork of the base repo and offer that as
180+
// "OwnForkRepo"
181+
var ownForkRepo *repo_model.Repository
182+
if ctx.Doer != nil && baseRepo.OwnerID != ctx.Doer.ID {
183+
repo := repo_model.GetForkedRepo(ctx, ctx.Doer.ID, baseRepo.ID)
184+
if repo != nil {
185+
ownForkRepo = repo
186+
}
187+
}
188+
189+
has := ci.HeadRepo != nil
190+
// 3. If the base is a forked from "RootRepo" and the owner of
191+
// the "RootRepo" is the :headUser - set headRepo to that
192+
if !has && rootRepo != nil && rootRepo.OwnerID == ci.HeadUser.ID {
193+
ci.HeadRepo = rootRepo
194+
has = true
195+
}
196+
197+
// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer
198+
// set the headRepo to the ownFork
199+
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == ci.HeadUser.ID {
200+
ci.HeadRepo = ownForkRepo
201+
has = true
202+
}
203+
204+
// 5. If the headOwner has a fork of the baseRepo - use that
205+
if !has {
206+
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID)
207+
has = ci.HeadRepo != nil
208+
}
209+
210+
// 6. If the baseRepo is a fork and the headUser has a fork of that use that
211+
if !has && baseRepo.IsFork {
212+
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ForkID)
213+
has = ci.HeadRepo != nil
214+
}
215+
216+
// 7. Finally open the git repo
217+
if isSameRepo {
218+
ci.HeadRepo = ctx.Repo.Repository
219+
ci.HeadGitRepo = ctx.Repo.GitRepo
220+
} else if has {
221+
ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo)
222+
if err != nil {
223+
ctx.ServerError("OpenRepository", err)
224+
return nil
225+
}
226+
defer ci.HeadGitRepo.Close()
227+
} else {
228+
ctx.NotFound("ParseCompareInfo", nil)
229+
return nil
230+
}
231+
232+
// Now we need to assert that the ctx.Doer has permission to read
233+
// the baseRepo's code and pulls
234+
// (NOT headRepo's)
235+
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
236+
if err != nil {
237+
ctx.ServerError("GetUserRepoPermission", err)
238+
return nil
239+
}
240+
if !permBase.CanRead(unit.TypeCode) {
241+
if log.IsTrace() {
242+
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
243+
ctx.Doer,
244+
baseRepo,
245+
permBase)
246+
}
247+
ctx.NotFound("ParseCompareInfo", nil)
248+
return nil
249+
}
250+
251+
// If we're not merging from the same repo:
252+
if !isSameRepo {
253+
// Assert ctx.Doer has permission to read headRepo's codes
254+
permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
255+
if err != nil {
256+
ctx.ServerError("GetUserRepoPermission", err)
257+
return nil
258+
}
259+
if !permHead.CanRead(unit.TypeCode) {
260+
if log.IsTrace() {
261+
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
262+
ctx.Doer,
263+
ci.HeadRepo,
264+
permHead)
265+
}
266+
ctx.NotFound("ParseCompareInfo", nil)
267+
return nil
268+
}
269+
}
270+
271+
// Check if head branch is valid.
272+
headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch)
273+
headIsBranch := ci.HeadGitRepo.IsBranchExist(ci.HeadBranch)
274+
headIsTag := ci.HeadGitRepo.IsTagExist(ci.HeadBranch)
275+
if !headIsCommit && !headIsBranch && !headIsTag {
276+
// Check if headBranch is short sha commit hash
277+
if headCommit, _ := ci.HeadGitRepo.GetCommit(ci.HeadBranch); headCommit != nil {
278+
ci.HeadBranch = headCommit.ID.String()
279+
ctx.Data["HeadBranch"] = ci.HeadBranch
280+
} else {
281+
ctx.NotFound("IsRefExist", nil)
282+
return nil
283+
}
284+
}
285+
286+
baseBranchRef := ci.BaseBranch
287+
if baseIsBranch {
288+
baseBranchRef = git.BranchPrefix + ci.BaseBranch
289+
} else if baseIsTag {
290+
baseBranchRef = git.TagPrefix + ci.BaseBranch
291+
}
292+
headBranchRef := ci.HeadBranch
293+
if headIsBranch {
294+
headBranchRef = git.BranchPrefix + ci.HeadBranch
295+
} else if headIsTag {
296+
headBranchRef = git.TagPrefix + ci.HeadBranch
297+
}
298+
299+
ci.CompareInfo, err = ci.HeadGitRepo.GetCompareInfo(baseRepo.RepoPath(), baseBranchRef, headBranchRef, ci.DirectComparison, fileOnly)
300+
if err != nil {
301+
ctx.ServerError("GetCompareInfo", err)
302+
return nil
303+
}
304+
305+
return ci
306+
}
307+
308+
// CompareDiff compare two branches or commits
309+
func CompareDiff(ctx *context.APIContext) {
310+
// swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} Get commit comparison information
311+
// ---
312+
// summary: Get commit comparison information
313+
// produces:
314+
// - application/json
315+
// parameters:
316+
// - name: owner
317+
// in: path
318+
// description: owner of the repo
319+
// type: string
320+
// required: true
321+
// - name: repo
322+
// in: path
323+
// description: name of the repo
324+
// type: string
325+
// required: true
326+
// - name: basehead
327+
// in: path
328+
// description: compare two branches or commits
329+
// type: string
330+
// required: true
331+
// responses:
332+
// "200":
333+
// "$ref": "#/responses/Compare"
334+
// "404":
335+
// "$ref": "#/responses/notFound"
336+
337+
if ctx.Repo.GitRepo == nil {
338+
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
339+
if err != nil {
340+
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
341+
return
342+
}
343+
ctx.Repo.GitRepo = gitRepo
344+
defer gitRepo.Close()
345+
}
346+
347+
ci := ParseCompareInfo(ctx)
348+
defer func() {
349+
if ci != nil && ci.HeadGitRepo != nil {
350+
ci.HeadGitRepo.Close()
351+
}
352+
}()
353+
if ctx.Written() {
354+
return
355+
}
356+
357+
apiCommits := make([]*api.Commit, 0, len(ci.CompareInfo.Commits))
358+
userCache := make(map[string]*user_model.User)
359+
for i := 0; i < len(ci.CompareInfo.Commits); i++ {
360+
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.CompareInfo.Commits[i], userCache,
361+
convert.ToCommitOptions{
362+
Stat: true,
363+
Verification: ctx.FormBool("verification"),
364+
Files: ctx.FormBool("files"),
365+
})
366+
if err != nil {
367+
ctx.ServerError("toCommit", err)
368+
return
369+
}
370+
apiCommits = append(apiCommits, apiCommit)
371+
}
372+
373+
ctx.JSON(http.StatusOK, &api.Compare{
374+
TotalCommits: len(ci.CompareInfo.Commits),
375+
Commits: apiCommits,
376+
})
377+
}

0 commit comments

Comments
 (0)