1// Copyright 2014 The Go Authors.  All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package main
6
7import (
8	"encoding/json"
9	"flag"
10	"fmt"
11	"io/ioutil"
12	"os"
13	"path/filepath"
14	"strings"
15)
16
17func cmdSync(args []string) {
18	expectZeroArgs(args, "sync")
19
20	// Get current branch and commit ID for fixup after pull.
21	b := CurrentBranch()
22	b.NeedOriginBranch("sync")
23	var id string
24	if work := b.Pending(); len(work) > 0 {
25		id = work[0].ChangeID
26	}
27
28	// If this is a Gerrit repo, disable the status advice that
29	// tells users to run 'git push' and so on, like the marked (<<<) lines:
30	//
31	//	% git status
32	//	On branch master
33	//	Your branch is ahead of 'origin/master' by 3 commits. <<<
34	//	  (use "git push" to publish your local commits)      <<<
35	//	...
36	//
37	// (This advice is inappropriate when using Gerrit.)
38	if len(b.Pending()) > 0 && haveGerrit() {
39		// Only disable if statusHints is unset in the local config.
40		// This allows users who really want them to put them back
41		// in the .git/config for the Gerrit-cloned repo.
42		_, err := cmdOutputErr("git", "config", "--local", "advice.statusHints")
43		if err != nil {
44			run("git", "config", "--local", "advice.statusHints", "false")
45		}
46	}
47
48	// Don't sync with staged or unstaged changes.
49	// rebase is going to complain if we don't, and we can give a nicer error.
50	checkStaged("sync")
51	checkUnstaged("sync")
52
53	// Pull remote changes into local branch.
54	// We do this in one command so that people following along with 'git sync -v'
55	// see fewer commands to understand.
56	// We want to pull in the remote changes from the upstream branch
57	// and rebase the current pending commit (if any) on top of them.
58	// If there is no pending commit, the pull will do a fast-forward merge.
59	if *verbose > 1 {
60		run("git", "pull", "-q", "-r", "-v", "origin", strings.TrimPrefix(b.OriginBranch(), "origin/"))
61	} else {
62		run("git", "pull", "-q", "-r", "origin", strings.TrimPrefix(b.OriginBranch(), "origin/"))
63	}
64
65	b = CurrentBranch() // discard any cached information
66	if len(b.Pending()) == 1 && b.Submitted(id) {
67		// If the change commit has been submitted,
68		// roll back change leaving any changes unstaged.
69		// Pull should have done this for us, but check just in case.
70		run("git", "reset", b.Branchpoint())
71	}
72}
73
74func checkStaged(cmd string) {
75	if HasStagedChanges() {
76		dief("cannot %s: staged changes exist\n"+
77			"\trun 'git status' to see changes\n"+
78			"\trun 'git-codereview change' to commit staged changes", cmd)
79	}
80}
81
82func checkUnstaged(cmd string) {
83	if HasUnstagedChanges() {
84		dief("cannot %s: unstaged changes exist\n"+
85			"\trun 'git status' to see changes\n"+
86			"\trun 'git stash' to save unstaged changes\n"+
87			"\trun 'git add' and 'git-codereview change' to commit staged changes", cmd)
88	}
89}
90
91type syncBranchStatus struct {
92	Local      string
93	Parent     string
94	Branch     string
95	ParentHash string
96	BranchHash string
97	Conflicts  []string
98}
99
100func syncBranchStatusFile() string {
101	return filepath.Join(repoRoot(), ".git/codereview-sync-branch-status")
102}
103
104func readSyncBranchStatus() *syncBranchStatus {
105	data, err := ioutil.ReadFile(syncBranchStatusFile())
106	if err != nil {
107		dief("cannot sync-branch: reading status: %v", err)
108	}
109	status := new(syncBranchStatus)
110	err = json.Unmarshal(data, status)
111	if err != nil {
112		dief("cannot sync-branch: reading status: %v", err)
113	}
114	return status
115}
116
117func writeSyncBranchStatus(status *syncBranchStatus) {
118	js, err := json.MarshalIndent(status, "", "\t")
119	if err != nil {
120		dief("cannot sync-branch: writing status: %v", err)
121	}
122	if err := ioutil.WriteFile(syncBranchStatusFile(), js, 0666); err != nil {
123		dief("cannot sync-branch: writing status: %v", err)
124	}
125}
126
127func cmdSyncBranch(args []string) {
128	os.Setenv("GIT_EDITOR", ":") // do not bring up editor during merge, commit
129
130	var cont, mergeBackToParent bool
131	flags.BoolVar(&cont, "continue", false, "continue after merge conflicts")
132	flags.BoolVar(&mergeBackToParent, "merge-back-to-parent", false, "for shutting down the dev branch")
133	flags.Parse(args)
134	if len(flag.Args()) > 0 {
135		fmt.Fprintf(stderr(), "Usage: %s sync-branch %s [-continue]\n", progName, globalFlags)
136		exit(2)
137	}
138
139	parent := config()["parent-branch"]
140	if parent == "" {
141		dief("cannot sync-branch: codereview.cfg does not list parent-branch")
142	}
143
144	branch := config()["branch"]
145	if parent == "" {
146		dief("cannot sync-branch: codereview.cfg does not list branch")
147	}
148
149	b := CurrentBranch()
150	if b.DetachedHead() {
151		dief("cannot sync-branch: on detached head")
152	}
153	if len(b.Pending()) > 0 {
154		dief("cannot sync-branch: pending changes exist\n" +
155			"\trun 'git codereview pending' to see them")
156	}
157
158	if cont {
159		// Note: There is no -merge-back-to-parent -continue
160		// because -merge-back-to-parent never has merge conflicts.
161		// (It requires that the parent be fully merged into the
162		// dev branch or it won't even attempt the reverse merge.)
163		if mergeBackToParent {
164			dief("cannot use -continue with -merge-back-to-parent")
165		}
166		if _, err := os.Stat(syncBranchStatusFile()); err != nil {
167			dief("cannot sync-branch -continue: no pending sync-branch status file found")
168		}
169		syncBranchContinue(syncBranchContinueFlag, b, readSyncBranchStatus())
170		return
171	}
172
173	if _, err := cmdOutputErr("git", "rev-parse", "--abbrev-ref", "MERGE_HEAD"); err == nil {
174		diePendingMerge("sync-branch")
175	}
176
177	// Don't sync with staged or unstaged changes.
178	// rebase is going to complain if we don't, and we can give a nicer error.
179	checkStaged("sync")
180	checkUnstaged("sync")
181
182	// Make sure client is up-to-date on current branch.
183	// Note that this does a remote fetch of b.OriginBranch() (aka branch).
184	cmdSync(nil)
185
186	// Pull down parent commits too.
187	quiet := "-q"
188	if *verbose > 0 {
189		quiet = "-v"
190	}
191	run("git", "fetch", quiet, "origin", "refs/heads/"+parent+":refs/remotes/origin/"+parent)
192
193	// Write the status file to make sure we can, before starting a merge.
194	status := &syncBranchStatus{
195		Local:      b.Name,
196		Parent:     parent,
197		ParentHash: gitHash("origin/" + parent),
198		Branch:     branch,
199		BranchHash: gitHash("origin/" + branch),
200	}
201	writeSyncBranchStatus(status)
202
203	parentHash, err := cmdOutputErr("git", "rev-parse", "origin/"+parent)
204	if err != nil {
205		dief("cannot sync-branch: cannot resolve origin/%s: %v\n%s", parent, err, parentHash)
206	}
207	branchHash, err := cmdOutputErr("git", "rev-parse", "origin/"+branch)
208	if err != nil {
209		dief("cannot sync-branch: cannot resolve origin/%s: %v\n%s", branch, err, branchHash)
210	}
211	parentHash = trim(parentHash)
212	branchHash = trim(branchHash)
213
214	// Only --merge-back-to-parent when there's nothing waiting
215	// to be merged in from parent. If a non-trivial merge needs
216	// to be done, it should be done first on the dev branch,
217	// not the parent branch.
218	if mergeBackToParent {
219		other := cmdOutput("git", "log", "--format=format:+ %cd %h %s", "--date=short", "origin/"+branch+"..origin/"+parent)
220		if other != "" {
221			dief("cannot sync-branch --merge-back-to-parent: parent has new commits.\n"+
222				"\trun 'git sync-branch' to bring them into this branch first:\n%s",
223				other)
224		}
225	}
226
227	// Start the merge.
228	if mergeBackToParent {
229		// Change HEAD back to "parent" and merge "branch" into it,
230		// even though we could instead merge "parent" into "branch".
231		// This way the parent-branch lineage ends up the first parent
232		// of the merge, the same as it would when we are doing it by hand
233		// with a plain "git merge". This may help the display of the
234		// merge graph in some tools more closely reflect what we did.
235		run("git", "reset", "--hard", "origin/"+parent)
236		_, err = cmdOutputErr("git", "merge", "--no-ff", "origin/"+branch)
237	} else {
238		_, err = cmdOutputErr("git", "merge", "origin/"+parent)
239	}
240
241	// Resolve codereview.cfg the right way - never take it from the merge.
242	// For a regular sync-branch we keep the branch's.
243	// For a merge-back-to-parent we take the parent's.
244	// The codereview.cfg contains the branch config and we don't want
245	// it to change.
246	what := branchHash
247	if mergeBackToParent {
248		what = parentHash
249	}
250	cmdOutputDir(repoRoot(), "git", "checkout", what, "--", "codereview.cfg")
251
252	if mergeBackToParent {
253		syncBranchContinue(syncBranchMergeBackFlag, b, status)
254		return
255	}
256
257	if err != nil {
258		// Check whether the only listed file is codereview.cfg and try again if so.
259		// Build list of unmerged files.
260		for _, s := range nonBlankLines(cmdOutputDir(repoRoot(), "git", "status", "-b", "--porcelain")) {
261			// Unmerged status is anything with a U and also AA and DD.
262			if len(s) >= 4 && s[2] == ' ' && (s[0] == 'U' || s[1] == 'U' || s[0:2] == "AA" || s[0:2] == "DD") {
263				status.Conflicts = append(status.Conflicts, s[3:])
264			}
265		}
266		if len(status.Conflicts) == 0 {
267			// Must have been codereview.cfg that was the problem.
268			// Try continuing the merge.
269			// Note that as of Git 2.12, git merge --continue is a synonym for git commit,
270			// but older Gits do not have merge --continue.
271			var out string
272			out, err = cmdOutputErr("git", "commit", "-m", "TEMPORARY MERGE MESSAGE")
273			if err != nil {
274				printf("git commit failed with no apparent unmerged files:\n%s\n", out)
275			}
276		} else {
277			writeSyncBranchStatus(status)
278		}
279	}
280
281	if err != nil {
282		if len(status.Conflicts) == 0 {
283			dief("cannot sync-branch: git merge failed but no conflicts found\n"+
284				"(unexpected error, please ask for help!)\n\ngit status:\n%s\ngit status -b --porcelain:\n%s",
285				cmdOutputDir(repoRoot(), "git", "status"),
286				cmdOutputDir(repoRoot(), "git", "status", "-b", "--porcelain"))
287		}
288		dief("sync-branch: merge conflicts in:\n\t- %s\n\n"+
289			"Please fix them (use 'git status' to see the list again),\n"+
290			"then 'git add' or 'git rm' to resolve them,\n"+
291			"and then 'git sync-branch -continue' to continue.\n"+
292			"Or run 'git merge --abort' to give up on this sync-branch.\n",
293			strings.Join(status.Conflicts, "\n\t- "))
294	}
295
296	syncBranchContinue("", b, status)
297}
298
299func diePendingMerge(cmd string) {
300	dief("cannot %s: found pending merge\n"+
301		"Run 'git codereview sync-branch -continue' if you fixed\n"+
302		"merge conflicts after a previous sync-branch operation.\n"+
303		"Or run 'git merge --abort' to give up on the sync-branch.\n",
304		cmd)
305}
306
307func prefixFor(branch string) string {
308	if strings.HasPrefix(branch, "dev.") || strings.HasPrefix(branch, "release-branch.") {
309		return "[" + branch + "] "
310	}
311	return ""
312}
313
314const (
315	syncBranchContinueFlag  = " -continue"
316	syncBranchMergeBackFlag = " -merge-back-to-parent"
317)
318
319func syncBranchContinue(flag string, b *Branch, status *syncBranchStatus) {
320	if h := gitHash("origin/" + status.Parent); h != status.ParentHash {
321		dief("cannot sync-branch%s: parent hash changed: %.7s -> %.7s", flag, status.ParentHash, h)
322	}
323	if h := gitHash("origin/" + status.Branch); h != status.BranchHash {
324		dief("cannot sync-branch%s: branch hash changed: %.7s -> %.7s", flag, status.BranchHash, h)
325	}
326	if b.Name != status.Local {
327		dief("cannot sync-branch%s: branch changed underfoot: %s -> %s", flag, status.Local, b.Name)
328	}
329
330	var (
331		dst     = status.Branch
332		dstHash = status.BranchHash
333		src     = status.Parent
334		srcHash = status.ParentHash
335	)
336	if flag == syncBranchMergeBackFlag {
337		// This is a reverse merge: commits are flowing
338		// in the opposite direction from normal.
339		dst, src = src, dst
340		dstHash, srcHash = srcHash, dstHash
341	}
342
343	prefix := prefixFor(dst)
344	op := "merge"
345	if flag == syncBranchMergeBackFlag {
346		op = "REVERSE MERGE"
347	}
348	msg := fmt.Sprintf("%sall: %s %s (%.7s) into %s", prefix, op, src, srcHash, dst)
349
350	if flag == syncBranchContinueFlag {
351		// Need to commit the merge.
352
353		// Check that the state of the client is the way we left it before any merge conflicts.
354		mergeHead, err := cmdOutputErr("git", "rev-parse", "MERGE_HEAD")
355		if err != nil {
356			dief("cannot sync-branch%s: no pending merge\n"+
357				"If you accidentally ran 'git merge --continue' or 'git commit',\n"+
358				"then use 'git reset --hard HEAD^' to undo.\n", flag)
359		}
360		mergeHead = trim(mergeHead)
361		if mergeHead != srcHash {
362			dief("cannot sync-branch%s: MERGE_HEAD is %.7s, but origin/%s is %.7s", flag, mergeHead, src, srcHash)
363		}
364		head := gitHash("HEAD")
365		if head != dstHash {
366			dief("cannot sync-branch%s: HEAD is %.7s, but origin/%s is %.7s", flag, head, dst, dstHash)
367		}
368
369		if HasUnstagedChanges() {
370			dief("cannot sync-branch%s: unstaged changes (unresolved conflicts)\n"+
371				"\tUse 'git status' to see them, 'git add' or 'git rm' to resolve them,\n"+
372				"\tand then run 'git sync-branch -continue' again.\n", flag)
373		}
374
375		run("git", "commit", "-m", msg)
376	}
377
378	// Amend the merge message, which may be auto-generated by git
379	// or may have been written by us during the post-conflict commit above,
380	// to use our standard format and list the incorporated CLs.
381
382	// Merge must never sync codereview.cfg,
383	// because it contains the src and dst config.
384	// Force the on-dst copy back while amending the commit.
385	cmdOutputDir(repoRoot(), "git", "checkout", "origin/"+dst, "--", "codereview.cfg")
386
387	conflictMsg := ""
388	if len(status.Conflicts) > 0 {
389		conflictMsg = "Conflicts:\n\n- " + strings.Join(status.Conflicts, "\n- ") + "\n\n"
390	}
391
392	if flag == syncBranchMergeBackFlag {
393		msg += fmt.Sprintf("\n\n"+
394			"This commit is a REVERSE MERGE.\n"+
395			"It merges %s back into its parent branch, %s.\n"+
396			"This marks the end of development on %s.\n",
397			status.Branch, status.Parent, status.Branch)
398	}
399
400	msg += fmt.Sprintf("\n\n%sMerge List:\n\n%s", conflictMsg,
401		cmdOutput("git", "log", "--format=format:+ %cd %h %s", "--date=short", "HEAD^1..HEAD^2"))
402	run("git", "commit", "--amend", "-m", msg)
403
404	fmt.Fprintf(stderr(), "\n")
405
406	cmdPending([]string{"-c", "-l"})
407	fmt.Fprintf(stderr(), "\n* Merge commit created.\nRun 'git codereview mail' to send for review.\n")
408
409	os.Remove(syncBranchStatusFile())
410}
411