Skip to content

Commit 22bf2ca

Browse files
authored
Make API "compare" accept commit IDs (#32801)
1 parent 01b1896 commit 22bf2ca

File tree

10 files changed

+151
-121
lines changed

10 files changed

+151
-121
lines changed

modules/git/commit.go

+14
Original file line numberDiff line numberDiff line change
@@ -474,3 +474,17 @@ func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSetting
474474
}
475475
return c.repo.GetDefaultPublicGPGKey(forceUpdate)
476476
}
477+
478+
func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool {
479+
minLen := util.OptionalArg(minLength, objFmt.FullLength())
480+
if len(s) < minLen || len(s) > objFmt.FullLength() {
481+
return false
482+
}
483+
for _, c := range s {
484+
isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
485+
if !isHex {
486+
return false
487+
}
488+
}
489+
return true
490+
}

modules/git/ref.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ func (ref RefName) RemoteName() string {
142142

143143
// ShortName returns the short name of the reference name
144144
func (ref RefName) ShortName() string {
145-
refName := string(ref)
146145
if ref.IsBranch() {
147146
return ref.BranchName()
148147
}
@@ -158,8 +157,7 @@ func (ref RefName) ShortName() string {
158157
if ref.IsFor() {
159158
return ref.ForBranchName()
160159
}
161-
162-
return refName
160+
return string(ref) // usually it is a commit ID
163161
}
164162

165163
// RefGroup returns the group type of the reference

modules/git/repo_ref.go

+28
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,31 @@ func parseTags(refs []string) []string {
6161
}
6262
return results
6363
}
64+
65+
// UnstableGuessRefByShortName does the best guess to see whether a "short name" provided by user is a branch, tag or commit.
66+
// It could guess wrongly if the input is already ambiguous. For example:
67+
// * "refs/heads/the-name" vs "refs/heads/refs/heads/the-name"
68+
// * "refs/tags/1234567890" vs commit "1234567890"
69+
// In most cases, it SHOULD AVOID using this function, unless there is an irresistible reason (eg: make API friendly to end users)
70+
// If the function is used, the caller SHOULD CHECK the ref type carefully.
71+
func (repo *Repository) UnstableGuessRefByShortName(shortName string) RefName {
72+
if repo.IsBranchExist(shortName) {
73+
return RefNameFromBranch(shortName)
74+
}
75+
if repo.IsTagExist(shortName) {
76+
return RefNameFromTag(shortName)
77+
}
78+
if strings.HasPrefix(shortName, "refs/") {
79+
if repo.IsReferenceExist(shortName) {
80+
return RefName(shortName)
81+
}
82+
}
83+
commit, err := repo.GetCommit(shortName)
84+
if err == nil {
85+
commitIDString := commit.ID.String()
86+
if strings.HasPrefix(commitIDString, shortName) {
87+
return RefName(commitIDString)
88+
}
89+
}
90+
return ""
91+
}

modules/globallock/globallock_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func TestLockAndDo(t *testing.T) {
6464
}
6565

6666
func testLockAndDo(t *testing.T) {
67-
const concurrency = 1000
67+
const concurrency = 50
6868

6969
ctx := context.Background()
7070
count := 0

routers/api/v1/repo/compare.go

+6-9
Original file line numberDiff line numberDiff line change
@@ -64,22 +64,19 @@ func CompareDiff(ctx *context.APIContext) {
6464
}
6565
}
6666

67-
_, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{
68-
Base: infos[0],
69-
Head: infos[1],
70-
})
67+
compareResult, closer := parseCompareInfo(ctx, api.CreatePullRequestOption{Base: infos[0], Head: infos[1]})
7168
if ctx.Written() {
7269
return
7370
}
74-
defer headGitRepo.Close()
71+
defer closer()
7572

7673
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
7774
files := ctx.FormString("files") == "" || ctx.FormBool("files")
7875

79-
apiCommits := make([]*api.Commit, 0, len(ci.Commits))
76+
apiCommits := make([]*api.Commit, 0, len(compareResult.compareInfo.Commits))
8077
userCache := make(map[string]*user_model.User)
81-
for i := 0; i < len(ci.Commits); i++ {
82-
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.Commits[i], userCache,
78+
for i := 0; i < len(compareResult.compareInfo.Commits); i++ {
79+
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareResult.compareInfo.Commits[i], userCache,
8380
convert.ToCommitOptions{
8481
Stat: true,
8582
Verification: verification,
@@ -93,7 +90,7 @@ func CompareDiff(ctx *context.APIContext) {
9390
}
9491

9592
ctx.JSON(http.StatusOK, &api.Compare{
96-
TotalCommits: len(ci.Commits),
93+
TotalCommits: len(compareResult.compareInfo.Commits),
9794
Commits: apiCommits,
9895
})
9996
}

routers/api/v1/repo/pull.go

+73-74
Original file line numberDiff line numberDiff line change
@@ -389,8 +389,7 @@ func CreatePullRequest(ctx *context.APIContext) {
389389

390390
form := *web.GetForm(ctx).(*api.CreatePullRequestOption)
391391
if form.Head == form.Base {
392-
ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame",
393-
"Invalid PullRequest: There are no changes between the head and the base")
392+
ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", "Invalid PullRequest: There are no changes between the head and the base")
394393
return
395394
}
396395

@@ -401,14 +400,22 @@ func CreatePullRequest(ctx *context.APIContext) {
401400
)
402401

403402
// Get repo/branch information
404-
headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form)
403+
compareResult, closer := parseCompareInfo(ctx, form)
405404
if ctx.Written() {
406405
return
407406
}
408-
defer headGitRepo.Close()
407+
defer closer()
408+
409+
if !compareResult.baseRef.IsBranch() || !compareResult.headRef.IsBranch() {
410+
ctx.Error(http.StatusUnprocessableEntity, "BaseHeadInvalidRefType", "Invalid PullRequest: base and head must be branches")
411+
return
412+
}
409413

410414
// Check if another PR exists with the same targets
411-
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, issues_model.PullRequestFlowGithub)
415+
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.headRepo.ID, ctx.Repo.Repository.ID,
416+
compareResult.headRef.ShortName(), compareResult.baseRef.ShortName(),
417+
issues_model.PullRequestFlowGithub,
418+
)
412419
if err != nil {
413420
if !issues_model.IsErrPullRequestNotExist(err) {
414421
ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err)
@@ -484,13 +491,13 @@ func CreatePullRequest(ctx *context.APIContext) {
484491
DeadlineUnix: deadlineUnix,
485492
}
486493
pr := &issues_model.PullRequest{
487-
HeadRepoID: headRepo.ID,
494+
HeadRepoID: compareResult.headRepo.ID,
488495
BaseRepoID: repo.ID,
489-
HeadBranch: headBranch,
490-
BaseBranch: baseBranch,
491-
HeadRepo: headRepo,
496+
HeadBranch: compareResult.headRef.ShortName(),
497+
BaseBranch: compareResult.baseRef.ShortName(),
498+
HeadRepo: compareResult.headRepo,
492499
BaseRepo: repo,
493-
MergeBase: compareInfo.MergeBase,
500+
MergeBase: compareResult.compareInfo.MergeBase,
494501
Type: issues_model.PullRequestGitea,
495502
}
496503

@@ -1080,71 +1087,62 @@ func MergePullRequest(ctx *context.APIContext) {
10801087
ctx.Status(http.StatusOK)
10811088
}
10821089

1083-
func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) {
1084-
baseRepo := ctx.Repo.Repository
1090+
type parseCompareInfoResult struct {
1091+
headRepo *repo_model.Repository
1092+
headGitRepo *git.Repository
1093+
compareInfo *git.CompareInfo
1094+
baseRef git.RefName
1095+
headRef git.RefName
1096+
}
10851097

1098+
// parseCompareInfo returns non-nil if it succeeds, it always writes to the context and returns nil if it fails
1099+
func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (result *parseCompareInfoResult, closer func()) {
1100+
var err error
10861101
// Get compared branches information
10871102
// format: <base branch>...[<head repo>:]<head branch>
10881103
// base<-head: master...head:feature
10891104
// same repo: master...feature
1105+
baseRepo := ctx.Repo.Repository
1106+
baseRefToGuess := form.Base
10901107

1091-
// TODO: Validate form first?
1092-
1093-
baseBranch := form.Base
1094-
1095-
var (
1096-
headUser *user_model.User
1097-
headBranch string
1098-
isSameRepo bool
1099-
err error
1100-
)
1101-
1102-
// If there is no head repository, it means pull request between same repository.
1103-
headInfos := strings.Split(form.Head, ":")
1104-
if len(headInfos) == 1 {
1105-
isSameRepo = true
1106-
headUser = ctx.Repo.Owner
1107-
headBranch = headInfos[0]
1108+
headUser := ctx.Repo.Owner
1109+
headRefToGuess := form.Head
1110+
if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 {
1111+
// If there is no head repository, it means pull request between same repository.
1112+
// Do nothing here because the head variables have been assigned above.
11081113
} else if len(headInfos) == 2 {
1114+
// There is a head repository (the head repository could also be the same base repo)
1115+
headRefToGuess = headInfos[1]
11091116
headUser, err = user_model.GetUserByName(ctx, headInfos[0])
11101117
if err != nil {
11111118
if user_model.IsErrUserNotExist(err) {
11121119
ctx.NotFound("GetUserByName")
11131120
} else {
11141121
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
11151122
}
1116-
return nil, nil, nil, "", ""
1123+
return nil, nil
11171124
}
1118-
headBranch = headInfos[1]
1119-
// The head repository can also point to the same repo
1120-
isSameRepo = ctx.Repo.Owner.ID == headUser.ID
11211125
} else {
11221126
ctx.NotFound()
1123-
return nil, nil, nil, "", ""
1127+
return nil, nil
11241128
}
11251129

1126-
ctx.Repo.PullRequest.SameRepo = isSameRepo
1127-
log.Trace("Repo path: %q, base branch: %q, head branch: %q", ctx.Repo.GitRepo.Path, baseBranch, headBranch)
1128-
// Check if base branch is valid.
1129-
if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) {
1130-
ctx.NotFound("BaseNotExist")
1131-
return nil, nil, nil, "", ""
1132-
}
1130+
isSameRepo := ctx.Repo.Owner.ID == headUser.ID
11331131

11341132
// Check if current user has fork of repository or in the same repository.
11351133
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
11361134
if headRepo == nil && !isSameRepo {
1137-
err := baseRepo.GetBaseRepo(ctx)
1135+
err = baseRepo.GetBaseRepo(ctx)
11381136
if err != nil {
11391137
ctx.Error(http.StatusInternalServerError, "GetBaseRepo", err)
1140-
return nil, nil, nil, "", ""
1138+
return nil, nil
11411139
}
11421140

11431141
// Check if baseRepo's base repository is the same as headUser's repository.
11441142
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
11451143
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
11461144
ctx.NotFound("GetBaseRepo")
1147-
return nil, nil, nil, "", ""
1145+
return nil, nil
11481146
}
11491147
// Assign headRepo so it can be used below.
11501148
headRepo = baseRepo.BaseRepo
@@ -1154,67 +1152,68 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
11541152
if isSameRepo {
11551153
headRepo = ctx.Repo.Repository
11561154
headGitRepo = ctx.Repo.GitRepo
1155+
closer = func() {} // no need to close the head repo because it shares the base repo
11571156
} else {
11581157
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
11591158
if err != nil {
11601159
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
1161-
return nil, nil, nil, "", ""
1160+
return nil, nil
11621161
}
1162+
closer = func() { _ = headGitRepo.Close() }
11631163
}
1164+
defer func() {
1165+
if result == nil && !isSameRepo {
1166+
_ = headGitRepo.Close()
1167+
}
1168+
}()
11641169

11651170
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
11661171
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
11671172
if err != nil {
1168-
headGitRepo.Close()
11691173
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
1170-
return nil, nil, nil, "", ""
1174+
return nil, nil
11711175
}
1176+
11721177
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
1173-
if log.IsTrace() {
1174-
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v",
1175-
ctx.Doer,
1176-
baseRepo,
1177-
permBase)
1178-
}
1179-
headGitRepo.Close()
1178+
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
11801179
ctx.NotFound("Can't read pulls or can't read UnitTypeCode")
1181-
return nil, nil, nil, "", ""
1180+
return nil, nil
11821181
}
11831182

1184-
// user should have permission to read headrepo's codes
1183+
// user should have permission to read headRepo's codes
1184+
// TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it.
11851185
permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
11861186
if err != nil {
1187-
headGitRepo.Close()
11881187
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
1189-
return nil, nil, nil, "", ""
1188+
return nil, nil
11901189
}
11911190
if !permHead.CanRead(unit.TypeCode) {
1192-
if log.IsTrace() {
1193-
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
1194-
ctx.Doer,
1195-
headRepo,
1196-
permHead)
1197-
}
1198-
headGitRepo.Close()
1191+
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead)
11991192
ctx.NotFound("Can't read headRepo UnitTypeCode")
1200-
return nil, nil, nil, "", ""
1193+
return nil, nil
12011194
}
12021195

1203-
// Check if head branch is valid.
1204-
if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) {
1205-
headGitRepo.Close()
1196+
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess)
1197+
headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess)
1198+
1199+
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.GitRepo.Path, baseRefToGuess, baseRef, headRefToGuess, headRef)
1200+
1201+
baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
1202+
headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())
1203+
// Check if base&head ref are valid.
1204+
if !baseRefValid || !headRefValid {
12061205
ctx.NotFound()
1207-
return nil, nil, nil, "", ""
1206+
return nil, nil
12081207
}
12091208

1210-
compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false)
1209+
compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseRef.ShortName(), headRef.ShortName(), false, false)
12111210
if err != nil {
1212-
headGitRepo.Close()
12131211
ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err)
1214-
return nil, nil, nil, "", ""
1212+
return nil, nil
12151213
}
12161214

1217-
return headRepo, headGitRepo, compareInfo, baseBranch, headBranch
1215+
result = &parseCompareInfoResult{headRepo: headRepo, headGitRepo: headGitRepo, compareInfo: compareInfo, baseRef: baseRef, headRef: headRef}
1216+
return result, closer
12181217
}
12191218

12201219
// UpdatePullRequest merge PR's baseBranch into headBranch

routers/web/repo/issue.go

-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"fmt"
1010
"html/template"
1111
"net/http"
12-
"net/url"
1312
"strconv"
1413
"strings"
1514

@@ -114,7 +113,6 @@ func MustAllowPulls(ctx *context.Context) {
114113
// User can send pull request if owns a forked repository.
115114
if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
116115
ctx.Repo.PullRequest.Allowed = true
117-
ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Doer.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName)
118116
}
119117
}
120118

0 commit comments

Comments
 (0)