diff options
Diffstat (limited to 'git-codereview/sync.go')
| -rw-r--r-- | git-codereview/sync.go | 140 |
1 files changed, 115 insertions, 25 deletions
diff --git a/git-codereview/sync.go b/git-codereview/sync.go index c38215c..b079c84 100644 --- a/git-codereview/sync.go +++ b/git-codereview/sync.go @@ -127,8 +127,9 @@ func writeSyncBranchStatus(status *syncBranchStatus) { func cmdSyncBranch(args []string) { os.Setenv("GIT_EDITOR", ":") // do not bring up editor during merge, commit - var cont bool + var cont, mergeBackToParent bool flags.BoolVar(&cont, "continue", false, "continue after merge conflicts") + flags.BoolVar(&mergeBackToParent, "merge-back-to-parent", false, "for shutting down the dev branch") flags.Parse(args) if len(flag.Args()) > 0 { fmt.Fprintf(stderr(), "Usage: %s sync-branch %s [-continue]\n", progName, globalFlags) @@ -155,10 +156,17 @@ func cmdSyncBranch(args []string) { } if cont { + // Note: There is no -merge-back-to-parent -continue + // because -merge-back-to-parent never has merge conflicts. + // (It requires that the parent be fully merged into the + // dev branch or it won't even attempt the reverse merge.) + if mergeBackToParent { + dief("cannot use -continue with -merge-back-to-parent") + } if _, err := os.Stat(syncBranchStatusFile()); err != nil { dief("cannot sync-branch -continue: no pending sync-branch status file found") } - syncBranchContinue(" -continue", b, readSyncBranchStatus()) + syncBranchContinue(syncBranchContinueFlag, b, readSyncBranchStatus()) return } @@ -192,11 +200,59 @@ func cmdSyncBranch(args []string) { } writeSyncBranchStatus(status) + parentHash, err := cmdOutputErr("git", "rev-parse", "origin/"+parent) + if err != nil { + dief("cannot sync-branch: cannot resolve origin/%s: %v\n%s", parent, err, parentHash) + } + branchHash, err := cmdOutputErr("git", "rev-parse", "origin/"+branch) + if err != nil { + dief("cannot sync-branch: cannot resolve origin/%s: %v\n%s", branch, err, branchHash) + } + parentHash = trim(parentHash) + branchHash = trim(branchHash) + + // Only --merge-back-to-parent when there's nothing waiting + // to be merged in from parent. If a non-trivial merge needs + // to be done, it should be done first on the dev branch, + // not the parent branch. + if mergeBackToParent { + other := cmdOutput("git", "log", "--format=format:+ %cd %h %s", "--date=short", "origin/"+branch+"..origin/"+parent) + if other != "" { + dief("cannot sync-branch --merge-back-to-parent: parent has new commits.\n"+ + "\trun 'git sync-branch' to bring them into this branch first:\n%s", + other) + } + } + // Start the merge. - _, err := cmdOutputErr("git", "merge", "origin/"+parent) + if mergeBackToParent { + // Change HEAD back to "parent" and merge "branch" into it, + // even though we could instead merge "parent" into "branch". + // This way the parent-branch lineage ends up the first parent + // of the merge, the same as it would when we are doing it by hand + // with a plain "git merge". This may help the display of the + // merge graph in some tools more closely reflect what we did. + run("git", "reset", "--hard", "origin/"+parent) + _, err = cmdOutputErr("git", "merge", "--no-ff", "origin/"+branch) + } else { + _, err = cmdOutputErr("git", "merge", "origin/"+parent) + } - // Resolve codereview.cfg the right way (never take it from the merge). - cmdOutputDir(repoRoot(), "git", "checkout", "HEAD", "--", "codereview.cfg") + // Resolve codereview.cfg the right way - never take it from the merge. + // For a regular sync-branch we keep the branch's. + // For a merge-back-to-parent we take the parent's. + // The codereview.cfg contains the branch config and we don't want + // it to change. + what := branchHash + if mergeBackToParent { + what = parentHash + } + cmdOutputDir(repoRoot(), "git", "checkout", what, "--", "codereview.cfg") + + if mergeBackToParent { + syncBranchContinue(syncBranchMergeBackFlag, b, status) + return + } if err != nil { // Check whether the only listed file is codereview.cfg and try again if so. @@ -224,8 +280,10 @@ func cmdSyncBranch(args []string) { if err != nil { if len(status.Conflicts) == 0 { - dief("cannot sync-branch: git merge failed but no conflicts found\n" + - "(unexpected error, please ask for help!)") + dief("cannot sync-branch: git merge failed but no conflicts found\n"+ + "(unexpected error, please ask for help!)\n\ngit status:\n%s\ngit status -b --porcelain:\n%s", + cmdOutputDir(repoRoot(), "git", "status"), + cmdOutputDir(repoRoot(), "git", "status", "-b", "--porcelain")) } dief("sync-branch: merge conflicts in:\n\t- %s\n\n"+ "Please fix them (use 'git status' to see the list again),\n"+ @@ -246,6 +304,18 @@ func diePendingMerge(cmd string) { cmd) } +func prefixFor(branch string) string { + if strings.HasPrefix(branch, "dev.") || strings.HasPrefix(branch, "release-branch.") { + return "[" + branch + "] " + } + return "" +} + +const ( + syncBranchContinueFlag = " -continue" + syncBranchMergeBackFlag = " -merge-back-to-parent" +) + func syncBranchContinue(flag string, b *Branch, status *syncBranchStatus) { if h := gitHash("origin/" + status.Parent); h != status.ParentHash { dief("cannot sync-branch%s: parent hash changed: %.7s -> %.7s", flag, status.ParentHash, h) @@ -257,34 +327,43 @@ func syncBranchContinue(flag string, b *Branch, status *syncBranchStatus) { dief("cannot sync-branch%s: branch changed underfoot: %s -> %s", flag, status.Local, b.Name) } - branch := status.Branch - parent := status.Parent - branchHash := status.BranchHash - parentHash := status.ParentHash + var ( + dst = status.Branch + dstHash = status.BranchHash + src = status.Parent + srcHash = status.ParentHash + ) + if flag == syncBranchMergeBackFlag { + // This is a reverse merge: commits are flowing + // in the opposite direction from normal. + dst, src = src, dst + dstHash, srcHash = srcHash, dstHash + } - prefix := "" - if strings.HasPrefix(branch, "dev.") || strings.HasPrefix(branch, "release-branch.") { - prefix = "[" + branch + "] " + prefix := prefixFor(dst) + op := "merge" + if flag == syncBranchMergeBackFlag { + op = "REVERSE MERGE" } - msg := fmt.Sprintf("%sall: merge %s (%.7s) into %s", prefix, parent, parentHash, branch) + msg := fmt.Sprintf("%sall: %s %s (%.7s) into %s", prefix, op, src, srcHash, dst) - if flag != "" { + if flag == syncBranchContinueFlag { // Need to commit the merge. // Check that the state of the client is the way we left it before any merge conflicts. mergeHead, err := cmdOutputErr("git", "rev-parse", "MERGE_HEAD") if err != nil { dief("cannot sync-branch%s: no pending merge\n"+ - "If you accidentally ran 'git merge --continue',\n"+ + "If you accidentally ran 'git merge --continue' or 'git commit',\n"+ "then use 'git reset --hard HEAD^' to undo.\n", flag) } mergeHead = trim(mergeHead) - if mergeHead != parentHash { - dief("cannot sync-branch%s: MERGE_HEAD is %.7s, but origin/%s is %.7s", flag, mergeHead, parent, parentHash) + if mergeHead != srcHash { + dief("cannot sync-branch%s: MERGE_HEAD is %.7s, but origin/%s is %.7s", flag, mergeHead, src, srcHash) } head := gitHash("HEAD") - if head != branchHash { - dief("cannot sync-branch%s: HEAD is %.7s, but origin/%s is %.7s", flag, head, branch, branchHash) + if head != dstHash { + dief("cannot sync-branch%s: HEAD is %.7s, but origin/%s is %.7s", flag, head, dst, dstHash) } if HasUnstagedChanges() { @@ -301,15 +380,24 @@ func syncBranchContinue(flag string, b *Branch, status *syncBranchStatus) { // to use our standard format and list the incorporated CLs. // Merge must never sync codereview.cfg, - // because it contains the parent and branch config. - // Force the on-branch copy back while amending the commit. - cmdOutputDir(repoRoot(), "git", "checkout", "origin/"+branch, "--", "codereview.cfg") + // because it contains the src and dst config. + // Force the on-dst copy back while amending the commit. + cmdOutputDir(repoRoot(), "git", "checkout", "origin/"+dst, "--", "codereview.cfg") conflictMsg := "" if len(status.Conflicts) > 0 { conflictMsg = "Conflicts:\n\n- " + strings.Join(status.Conflicts, "\n- ") + "\n\n" } - msg = fmt.Sprintf("%s\n\n%sMerge List:\n\n%s", msg, conflictMsg, + + if flag == syncBranchMergeBackFlag { + msg += fmt.Sprintf("\n\n"+ + "This commit is a REVERSE MERGE.\n"+ + "It merges %s back into its parent branch, %s.\n"+ + "This marks the end of development on %s.\n", + status.Branch, status.Parent, status.Branch) + } + + msg += fmt.Sprintf("\n\n%sMerge List:\n\n%s", conflictMsg, cmdOutput("git", "log", "--format=format:+ %cd %h %s", "--date=short", "HEAD^1..HEAD^2")) run("git", "commit", "--amend", "-m", msg) @@ -317,4 +405,6 @@ func syncBranchContinue(flag string, b *Branch, status *syncBranchStatus) { cmdPending([]string{"-c", "-l"}) fmt.Fprintf(stderr(), "\n* Merge commit created.\nRun 'git codereview mail' to send for review.\n") + + os.Remove(syncBranchStatusFile()) } |
