aboutsummaryrefslogtreecommitdiff
path: root/git-codereview/sync.go
diff options
context:
space:
mode:
Diffstat (limited to 'git-codereview/sync.go')
-rw-r--r--git-codereview/sync.go241
1 files changed, 240 insertions, 1 deletions
diff --git a/git-codereview/sync.go b/git-codereview/sync.go
index 9ea3e67..0334341 100644
--- a/git-codereview/sync.go
+++ b/git-codereview/sync.go
@@ -4,7 +4,15 @@
package main
-import "strings"
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+)
func cmdSync(args []string) {
expectZeroArgs(args, "sync")
@@ -78,3 +86,234 @@ func checkUnstaged(cmd string) {
"\trun 'git add' and 'git-codereview change' to commit staged changes", cmd)
}
}
+
+type syncBranchStatus struct {
+ Local string
+ Parent string
+ Branch string
+ ParentHash string
+ BranchHash string
+ Conflicts []string
+}
+
+func syncBranchStatusFile() string {
+ return filepath.Join(repoRoot(), ".git/codereview-sync-branch-status")
+}
+
+func readSyncBranchStatus() *syncBranchStatus {
+ data, err := ioutil.ReadFile(syncBranchStatusFile())
+ if err != nil {
+ dief("cannot sync-branch: reading status: %v", err)
+ }
+ status := new(syncBranchStatus)
+ err = json.Unmarshal(data, status)
+ if err != nil {
+ dief("cannot sync-branch: reading status: %v", err)
+ }
+ return status
+}
+
+func writeSyncBranchStatus(status *syncBranchStatus) {
+ js, err := json.MarshalIndent(status, "", "\t")
+ if err != nil {
+ dief("cannot sync-branch: writing status: %v", err)
+ }
+ if err := ioutil.WriteFile(syncBranchStatusFile(), js, 0666); err != nil {
+ dief("cannot sync-branch: writing status: %v", err)
+ }
+}
+
+func cmdSyncBranch(args []string) {
+ os.Setenv("GIT_EDITOR", ":") // do not bring up editor during merge, commit
+
+ var cont bool
+ flags.BoolVar(&cont, "continue", false, "continue after merge conflicts")
+ flags.Parse(args)
+ if len(flag.Args()) > 0 {
+ fmt.Fprintf(stderr(), "Usage: %s sync-branch %s [-continue]\n", progName, globalFlags)
+ exit(2)
+ }
+
+ parent := config()["parent-branch"]
+ if parent == "" {
+ dief("cannot sync-branch: codereview.cfg does not list parent-branch")
+ }
+
+ branch := config()["branch"]
+ if parent == "" {
+ dief("cannot sync-branch: codereview.cfg does not list branch")
+ }
+
+ b := CurrentBranch()
+ if b.DetachedHead() {
+ dief("cannot sync-branch: on detached head")
+ }
+ if len(b.Pending()) > 0 {
+ dief("cannot sync-branch: pending changes exist\n" +
+ "\trun 'git codereview pending' to see them")
+ }
+
+ if cont {
+ if _, err := os.Stat(syncBranchStatusFile()); err != nil {
+ dief("cannot sync-branch -continue: no pending sync-branch status file found")
+ }
+ syncBranchContinue(" -continue", b, readSyncBranchStatus())
+ return
+ }
+
+ if _, err := cmdOutputErr("git", "rev-parse", "--abbrev-ref", "MERGE_HEAD"); err == nil {
+ diePendingMerge("sync-branch")
+ }
+
+ // Don't sync with staged or unstaged changes.
+ // rebase is going to complain if we don't, and we can give a nicer error.
+ checkStaged("sync")
+ checkUnstaged("sync")
+
+ // Make sure client is up-to-date on current branch.
+ // Note that this does a remote fetch of b.OriginBranch() (aka branch).
+ cmdSync(nil)
+
+ // Pull down parent commits too.
+ quiet := "-q"
+ if *verbose > 0 {
+ quiet = "-v"
+ }
+ run("git", "fetch", quiet, "origin", "refs/heads/"+parent+":refs/remotes/origin/"+parent)
+
+ // Write the status file to make sure we can, before starting a merge.
+ status := &syncBranchStatus{
+ Local: b.Name,
+ Parent: parent,
+ ParentHash: gitHash("origin/" + parent),
+ Branch: branch,
+ BranchHash: gitHash("origin/" + branch),
+ }
+ writeSyncBranchStatus(status)
+
+ // Start the merge.
+ _, 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")
+
+ if err != nil {
+ // Check whether the only listed file is codereview.cfg and try again if so.
+ // Build list of unmerged files.
+ for _, s := range nonBlankLines(cmdOutputDir(repoRoot(), "git", "status", "-b", "--porcelain")) {
+ // Unmerged status is anything with a U and also AA and DD.
+ if len(s) >= 4 && s[2] == ' ' && (s[0] == 'U' || s[1] == 'U' || s[0:2] == "AA" || s[0:2] == "DD") {
+ status.Conflicts = append(status.Conflicts, s[3:])
+ }
+ }
+ if len(status.Conflicts) == 0 {
+ // Must have been codereview.cfg that was the problem.
+ // Try continuing the merge.
+ // Note that as of Git 2.12, git merge --continue is a synonym for git commit,
+ // but older Gits do not have merge --continue.
+ var out string
+ out, err = cmdOutputErr("git", "commit", "-m", "TEMPORARY MERGE MESSAGE")
+ if err != nil {
+ printf("git commit failed with no apparent unmerged files:\n%s\n", out)
+ }
+ } else {
+ writeSyncBranchStatus(status)
+ }
+ }
+
+ 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("sync-branch: merge conflicts in:\n\t- %s\n\n"+
+ "Please fix them (use 'git status' to see the list again),\n"+
+ "then 'git add' or 'git rm' to resolve them,\n"+
+ "and then 'git sync-branch -continue' to continue.\n"+
+ "Or run 'git merge --abort' to give up on this sync-branch.\n",
+ strings.Join(status.Conflicts, "\n\t- "))
+ }
+
+ syncBranchContinue("", b, status)
+}
+
+func diePendingMerge(cmd string) {
+ dief("cannot %s: found pending merge\n"+
+ "Run 'git codereview sync-branch -continue' if you fixed\n"+
+ "merge conflicts after a previous sync-branch operation.\n"+
+ "Or run 'git merge --abort' to give up on the sync-branch.\n",
+ cmd)
+}
+
+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)
+ }
+ if h := gitHash("origin/" + status.Branch); h != status.BranchHash {
+ dief("cannot sync-branch%s: branch hash changed: %.7s -> %.7s", flag, status.BranchHash, h)
+ }
+ if b.Name != status.Local {
+ 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
+
+ prefix := ""
+ if strings.HasPrefix(branch, "dev.") || strings.HasPrefix(branch, "release-branch.") {
+ prefix = "[" + branch + "] "
+ }
+ msg := fmt.Sprintf("%sall: merge %s (%.7s) into %s", prefix, parent, parentHash, branch)
+
+ if flag != "" {
+ // 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"+
+ "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)
+ }
+ head := gitHash("HEAD")
+ if head != branchHash {
+ dief("cannot sync-branch%s: HEAD is %.7s, but origin/%s is %.7s", flag, head, branch, branchHash)
+ }
+
+ if HasUnstagedChanges() {
+ dief("cannot sync-branch%s: unstaged changes (unresolved conflicts)\n"+
+ "\tUse 'git status' to see them, 'git add' or 'git rm' to resolve them,\n"+
+ "\tand then run 'git sync-branch -continue' again.\n", flag)
+ }
+
+ run("git", "commit", "-m", msg)
+ }
+
+ // Amend the merge message, which may be auto-generated by git
+ // or may have been written by us during the post-conflict commit above,
+ // 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")
+
+ 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,
+ cmdOutput("git", "log", "--format=format:+ %cd %h %s", "--date=short", "HEAD^1..HEAD^2"))
+ run("git", "commit", "--amend", "-m", msg)
+
+ fmt.Fprintf(stderr(), "\n")
+
+ cmdPending([]string{"-c", "-l"})
+ fmt.Fprintf(stderr(), "\n* Merge commit created.\nRun 'git codereview mail' to send for review.\n")
+}