Add Pull Request merge options - Ignore white-space for conflict checking, Rebase, Squash merge (#3188)

* Pull request options migration and UI in settings

* Add ignore whitespace functionality

* Fix settings if pull requests are disabled

* Fix migration transaction

* Merge with Rebase functionality

* UI changes and related functionality for pull request merging button

* Implement squash functionality

* Fix rebase merging

* Fix pull request merge tests

* Add squash and rebase tests

* Fix API method to reuse default message functions

* Some refactoring and small fixes

* Remove more hardcoded values from tests

* Remove unneeded check from API method

* Fix variable name and comment typo

* Fix reset commit count after PR merge
This commit is contained in:
Lauris BH 2018-01-05 20:56:50 +02:00 committed by GitHub
parent a192f3052e
commit 8ac1501ad7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 529 additions and 44 deletions

View file

@ -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)
}
// _________ __
// \_ ___ \ ____ _____ _____ ____ _____/ |_
// / \ \/ / _ \ / \ / \_/ __ \ / \ __\

View file

@ -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
-

View file

@ -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

57
models/migrations/v54.go Normal file
View file

@ -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()
}

View file

@ -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]) {

View file

@ -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,

View file

@ -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