diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/error.go | 17 | ||||
-rw-r--r-- | models/fixtures/repo_unit.yml | 4 | ||||
-rw-r--r-- | models/migrations/migrations.go | 2 | ||||
-rw-r--r-- | models/migrations/v54.go | 57 | ||||
-rw-r--r-- | models/pull.go | 138 | ||||
-rw-r--r-- | models/repo.go | 5 | ||||
-rw-r--r-- | models/repo_unit.go | 34 |
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 |