diff options
Diffstat (limited to 'git-codereview/sync.go')
| -rw-r--r-- | git-codereview/sync.go | 241 |
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") +} |
