summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
Diffstat (limited to 'models')
-rw-r--r--models/error.go17
-rw-r--r--models/fixtures/repo_unit.yml4
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v54.go57
-rw-r--r--models/pull.go138
-rw-r--r--models/repo.go5
-rw-r--r--models/repo_unit.go34
7 files changed, 235 insertions, 22 deletions
diff --git a/models/error.go b/models/error.go
index fceee21fdf..765b8fa6ca 100644
--- a/models/error.go
+++ b/models/error.go
@@ -878,6 +878,23 @@ func (err ErrPullRequestAlreadyExists) Error() string {
err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
}
+// ErrInvalidMergeStyle represents an error if merging with disabled merge strategy
+type ErrInvalidMergeStyle struct {
+ ID int64
+ Style MergeStyle
+}
+
+// IsErrInvalidMergeStyle checks if an error is a ErrInvalidMergeStyle.
+func IsErrInvalidMergeStyle(err error) bool {
+ _, ok := err.(ErrInvalidMergeStyle)
+ return ok
+}
+
+func (err ErrInvalidMergeStyle) Error() string {
+ return fmt.Sprintf("merge strategy is not allowed or is invalid [repo_id: %d, strategy: %s]",
+ err.ID, err.Style)
+}
+
// _________ __
// \_ ___ \ ____ _____ _____ ____ _____/ |_
// / \ \/ / _ \ / \ / \_/ __ \ / \ __\
diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index e9931453ad..45229cce37 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -30,7 +30,7 @@
id: 5
repo_id: 1
type: 3
- config: "{}"
+ config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowSquash\":true}"
created_unix: 946684810
-
@@ -51,7 +51,7 @@
id: 8
repo_id: 3
type: 3
- config: "{}"
+ config: "{\"IgnoreWhitespaceConflicts\":true,\"AllowMerge\":true,\"AllowRebase\":false,\"AllowSquash\":false}"
created_unix: 946684810
-
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index cbd09afabf..90f286056f 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -160,6 +160,8 @@ var migrations = []Migration{
NewMigration("add lfs lock table", addLFSLock),
// v53 -> v54
NewMigration("add reactions", addReactions),
+ // v54 -> v55
+ NewMigration("add pull request options", addPullRequestOptions),
}
// Migrate database to current version
diff --git a/models/migrations/v54.go b/models/migrations/v54.go
new file mode 100644
index 0000000000..96c26739c6
--- /dev/null
+++ b/models/migrations/v54.go
@@ -0,0 +1,57 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/go-xorm/xorm"
+)
+
+func addPullRequestOptions(x *xorm.Engine) error {
+ // RepoUnit describes all units of a repository
+ type RepoUnit struct {
+ ID int64
+ RepoID int64 `xorm:"INDEX(s)"`
+ Type int `xorm:"INDEX(s)"`
+ Config map[string]interface{} `xorm:"JSON"`
+ CreatedUnix util.TimeStamp `xorm:"INDEX CREATED"`
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ //Updating existing issue units
+ units := make([]*RepoUnit, 0, 100)
+ if err := sess.Where("`type` = ?", V16UnitTypePRs).Find(&units); err != nil {
+ return fmt.Errorf("Query repo units: %v", err)
+ }
+ for _, unit := range units {
+ if unit.Config == nil {
+ unit.Config = make(map[string]interface{})
+ }
+ if _, ok := unit.Config["IgnoreWhitespaceConflicts"]; !ok {
+ unit.Config["IgnoreWhitespaceConflicts"] = false
+ }
+ if _, ok := unit.Config["AllowMerge"]; !ok {
+ unit.Config["AllowMerge"] = true
+ }
+ if _, ok := unit.Config["AllowRebase"]; !ok {
+ unit.Config["AllowRebase"] = true
+ }
+ if _, ok := unit.Config["AllowSquash"]; !ok {
+ unit.Config["AllowSquash"] = true
+ }
+ if _, err := sess.ID(unit.ID).Cols("config").Update(unit); err != nil {
+ return err
+ }
+ }
+ return sess.Commit()
+}
diff --git a/models/pull.go b/models/pull.go
index 47fc1dfb61..c9357e9130 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -16,6 +16,7 @@ import (
"code.gitea.io/git"
"code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
@@ -109,6 +110,28 @@ func (pr *PullRequest) loadIssue(e Engine) (err error) {
return err
}
+// GetDefaultMergeMessage returns default message used when merging pull request
+func (pr *PullRequest) GetDefaultMergeMessage() string {
+ if pr.HeadRepo == nil {
+ var err error
+ pr.HeadRepo, err = GetRepositoryByID(pr.HeadRepoID)
+ if err != nil {
+ log.Error(4, "GetRepositoryById[%d]: %v", pr.HeadRepoID, err)
+ return ""
+ }
+ }
+ return fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch)
+}
+
+// GetDefaultSquashMessage returns default message used when squash and merging pull request
+func (pr *PullRequest) GetDefaultSquashMessage() string {
+ if err := pr.LoadIssue(); err != nil {
+ log.Error(4, "LoadIssue: %v", err)
+ return ""
+ }
+ return fmt.Sprintf("%s (#%d)", pr.Issue.Title, pr.Issue.Index)
+}
+
// APIFormat assumes following fields have been assigned with valid values:
// Required - Issue
// Optional - Merger
@@ -232,15 +255,38 @@ func (pr *PullRequest) CanAutoMerge() bool {
return pr.Status == PullRequestStatusMergeable
}
+// MergeStyle represents the approach to merge commits into base branch.
+type MergeStyle string
+
+const (
+ // MergeStyleMerge create merge commit
+ MergeStyleMerge MergeStyle = "merge"
+ // MergeStyleRebase rebase before merging
+ MergeStyleRebase MergeStyle = "rebase"
+ // MergeStyleSquash squash commits into single commit before merging
+ MergeStyleSquash MergeStyle = "squash"
+)
+
// Merge merges pull request to base repository.
// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
-func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error) {
+func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle MergeStyle, message string) (err error) {
if err = pr.GetHeadRepo(); err != nil {
return fmt.Errorf("GetHeadRepo: %v", err)
} else if err = pr.GetBaseRepo(); err != nil {
return fmt.Errorf("GetBaseRepo: %v", err)
}
+ prUnit, err := pr.BaseRepo.GetUnit(UnitTypePullRequests)
+ if err != nil {
+ return err
+ }
+ prConfig := prUnit.PullRequestsConfig()
+
+ // Check if merge style is correct and allowed
+ if !prConfig.IsMergeStyleAllowed(mergeStyle) {
+ return ErrInvalidMergeStyle{pr.BaseRepo.ID, mergeStyle}
+ }
+
defer func() {
go HookQueue.Add(pr.BaseRepo.ID)
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false)
@@ -289,18 +335,62 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
return fmt.Errorf("git fetch [%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
}
- if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
- fmt.Sprintf("PullRequest.Merge (git merge --no-ff --no-commit): %s", tmpBasePath),
- "git", "merge", "--no-ff", "--no-commit", "head_repo/"+pr.HeadBranch); err != nil {
- return fmt.Errorf("git merge --no-ff --no-commit [%s]: %v - %s", tmpBasePath, err, stderr)
- }
+ switch mergeStyle {
+ case MergeStyleMerge:
+ if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git merge --no-ff --no-commit): %s", tmpBasePath),
+ "git", "merge", "--no-ff", "--no-commit", "head_repo/"+pr.HeadBranch); err != nil {
+ return fmt.Errorf("git merge --no-ff --no-commit [%s]: %v - %s", tmpBasePath, err, stderr)
+ }
- sig := doer.NewGitSig()
- if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
- fmt.Sprintf("PullRequest.Merge (git merge): %s", tmpBasePath),
- "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
- "-m", fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch)); err != nil {
- return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, stderr)
+ sig := doer.NewGitSig()
+ if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git merge): %s", tmpBasePath),
+ "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
+ "-m", message); err != nil {
+ return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, stderr)
+ }
+ case MergeStyleRebase:
+ // Checkout head branch
+ if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git checkout): %s", tmpBasePath),
+ "git", "checkout", "-b", "head_repo_"+pr.HeadBranch, "head_repo/"+pr.HeadBranch); err != nil {
+ return fmt.Errorf("git checkout: %s", stderr)
+ }
+ // Rebase before merging
+ if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git rebase): %s", tmpBasePath),
+ "git", "rebase", "-q", pr.BaseBranch); err != nil {
+ return fmt.Errorf("git rebase [%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
+ }
+ // Checkout base branch again
+ if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git checkout): %s", tmpBasePath),
+ "git", "checkout", pr.BaseBranch); err != nil {
+ return fmt.Errorf("git checkout: %s", stderr)
+ }
+ // Merge fast forward
+ if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git rebase): %s", tmpBasePath),
+ "git", "merge", "--ff-only", "-q", "head_repo_"+pr.HeadBranch); err != nil {
+ return fmt.Errorf("git merge --ff-only [%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
+ }
+ case MergeStyleSquash:
+ // Merge with squash
+ if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git squash): %s", tmpBasePath),
+ "git", "merge", "-q", "--squash", "head_repo/"+pr.HeadBranch); err != nil {
+ return fmt.Errorf("git merge --squash [%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
+ }
+ sig := pr.Issue.Poster.NewGitSig()
+ if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git squash): %s", tmpBasePath),
+ "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
+ "-m", message); err != nil {
+ return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, stderr)
+ }
+ default:
+ return ErrInvalidMergeStyle{pr.BaseRepo.ID, mergeStyle}
}
// Push back to upstream.
@@ -327,6 +417,9 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
log.Error(4, "MergePullRequestAction [%d]: %v", pr.ID, err)
}
+ // Reset cached commit count
+ cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true))
+
// Reload pull request information.
if err = pr.LoadAttributes(); err != nil {
log.Error(4, "LoadAttributes: %v", err)
@@ -349,7 +442,6 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
return nil
}
- // TODO: when squash commits, no need to append merge commit.
// It is possible that head branch is not fully sync with base branch for merge commits,
// so we need to get latest head commit and append merge commit manually
// to avoid strange diff commits produced.
@@ -358,12 +450,14 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
log.Error(4, "GetBranchCommit: %v", err)
return nil
}
- l.PushFront(mergeCommit)
+ if mergeStyle == MergeStyleMerge {
+ l.PushFront(mergeCommit)
+ }
p := &api.PushPayload{
Ref: git.BranchPrefix + pr.BaseBranch,
Before: pr.MergeBase,
- After: pr.MergedCommitID,
+ After: mergeCommit.ID.String(),
CompareURL: setting.AppURL + pr.BaseRepo.ComposeCompareURL(pr.MergeBase, pr.MergedCommitID),
Commits: ListToPushCommits(l).ToAPIPayloadCommits(pr.BaseRepo.HTMLURL()),
Repo: pr.BaseRepo.APIFormat(AccessModeNone),
@@ -563,9 +657,21 @@ func (pr *PullRequest) testPatch() (err error) {
return fmt.Errorf("git read-tree --index-output=%s %s: %v - %s", indexTmpPath, pr.BaseBranch, err, stderr)
}
+ prUnit, err := pr.BaseRepo.GetUnit(UnitTypePullRequests)
+ if err != nil {
+ return err
+ }
+ prConfig := prUnit.PullRequestsConfig()
+
+ args := []string{"apply", "--check", "--cached"}
+ if prConfig.IgnoreWhitespaceConflicts {
+ args = append(args, "--ignore-whitespace")
+ }
+ args = append(args, patchPath)
+
_, stderr, err = process.GetManager().ExecDirEnv(-1, "", fmt.Sprintf("testPatch (git apply --check): %d", pr.BaseRepo.ID),
[]string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()},
- "git", "apply", "--check", "--cached", patchPath)
+ "git", args...)
if err != nil {
for i := range patchConflicts {
if strings.Contains(stderr, patchConflicts[i]) {
diff --git a/models/repo.go b/models/repo.go
index 29fd39ca99..a9e116d6bd 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -427,6 +427,11 @@ func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit {
Type: tp,
Config: new(ExternalTrackerConfig),
}
+ } else if tp == UnitTypePullRequests {
+ return &RepoUnit{
+ Type: tp,
+ Config: new(PullRequestsConfig),
+ }
}
return &RepoUnit{
Type: tp,
diff --git a/models/repo_unit.go b/models/repo_unit.go
index 5100ca1ce2..49b62ec9cd 100644
--- a/models/repo_unit.go
+++ b/models/repo_unit.go
@@ -85,18 +85,44 @@ func (cfg *IssuesConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
+// PullRequestsConfig describes pull requests config
+type PullRequestsConfig struct {
+ IgnoreWhitespaceConflicts bool
+ AllowMerge bool
+ AllowRebase bool
+ AllowSquash bool
+}
+
+// FromDB fills up a PullRequestsConfig from serialized format.
+func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
+ return json.Unmarshal(bs, &cfg)
+}
+
+// ToDB exports a PullRequestsConfig to a serialized format.
+func (cfg *PullRequestsConfig) ToDB() ([]byte, error) {
+ return json.Marshal(cfg)
+}
+
+// IsMergeStyleAllowed returns if merge style is allowed
+func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
+ return mergeStyle == MergeStyleMerge && cfg.AllowMerge ||
+ mergeStyle == MergeStyleRebase && cfg.AllowRebase ||
+ mergeStyle == MergeStyleSquash && cfg.AllowSquash
+}
+
// BeforeSet is invoked from XORM before setting the value of a field of this object.
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
switch colName {
case "type":
switch UnitType(Cell2Int64(val)) {
- case UnitTypeCode, UnitTypePullRequests, UnitTypeReleases,
- UnitTypeWiki:
+ case UnitTypeCode, UnitTypeReleases, UnitTypeWiki:
r.Config = new(UnitConfig)
case UnitTypeExternalWiki:
r.Config = new(ExternalWikiConfig)
case UnitTypeExternalTracker:
r.Config = new(ExternalTrackerConfig)
+ case UnitTypePullRequests:
+ r.Config = new(PullRequestsConfig)
case UnitTypeIssues:
r.Config = new(IssuesConfig)
default:
@@ -116,8 +142,8 @@ func (r *RepoUnit) CodeConfig() *UnitConfig {
}
// PullRequestsConfig returns config for UnitTypePullRequests
-func (r *RepoUnit) PullRequestsConfig() *UnitConfig {
- return r.Config.(*UnitConfig)
+func (r *RepoUnit) PullRequestsConfig() *PullRequestsConfig {
+ return r.Config.(*PullRequestsConfig)
}
// ReleasesConfig returns config for UnitTypeReleases