blob: f87f43b873fba4a05cace6e1f173e74e2058930c [file] [log] [blame]
iannucci@chromium.org97231b52014-03-26 06:54:55 +00001#!/usr/bin/env python
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +00002# Copyright 2014 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7Tool to update all branches to have the latest changes from their upstreams.
8"""
9
10import argparse
11import collections
12import logging
13import sys
14import textwrap
15
16from pprint import pformat
17
18import git_common as git
19
20
21STARTING_BRANCH_KEY = 'depot-tools.rebase-update.starting-branch'
22
23
24def find_return_branch():
25 """Finds the branch which we should return to after rebase-update completes.
26
27 This value may persist across multiple invocations of rebase-update, if
28 rebase-update runs into a conflict mid-way.
29 """
30 return_branch = git.config(STARTING_BRANCH_KEY)
iannucci@chromium.orgbf157b42014-04-12 00:14:41 +000031 if not return_branch:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000032 return_branch = git.current_branch()
33 if return_branch != 'HEAD':
34 git.set_config(STARTING_BRANCH_KEY, return_branch)
35
36 return return_branch
37
38
39def fetch_remotes(branch_tree):
40 """Fetches all remotes which are needed to update |branch_tree|."""
41 fetch_tags = False
42 remotes = set()
43 tag_set = git.tags()
44 for parent in branch_tree.itervalues():
45 if parent in tag_set:
46 fetch_tags = True
47 else:
48 full_ref = git.run('rev-parse', '--symbolic-full-name', parent)
49 if full_ref.startswith('refs/remotes'):
50 parts = full_ref.split('/')
51 remote_name = parts[2]
52 remotes.add(remote_name)
53
54 fetch_args = []
55 if fetch_tags:
56 # Need to fetch all because we don't know what remote the tag comes from :(
57 # TODO(iannucci): assert that the tags are in the remote fetch refspec
58 fetch_args = ['--all']
59 else:
60 fetch_args.append('--multiple')
61 fetch_args.extend(remotes)
62 # TODO(iannucci): Should we fetch git-svn?
63
64 if not fetch_args: # pragma: no cover
65 print 'Nothing to fetch.'
66 else:
iannucci@chromium.org43f4ecc2014-05-08 19:22:47 +000067 git.run_with_stderr('fetch', *fetch_args, stdout=sys.stdout,
68 stderr=sys.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000069
70
71def remove_empty_branches(branch_tree):
72 tag_set = git.tags()
73 ensure_root_checkout = git.once(lambda: git.run('checkout', git.root()))
74
iannucci@chromium.org512d97d2014-03-31 23:36:10 +000075 deletions = set()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000076 downstreams = collections.defaultdict(list)
77 for branch, parent in git.topo_iter(branch_tree, top_down=False):
78 downstreams[parent].append(branch)
79
80 if git.hash_one(branch) == git.hash_one(parent):
81 ensure_root_checkout()
82
83 logging.debug('branch %s merged to %s', branch, parent)
84
85 for down in downstreams[branch]:
iannucci@chromium.org512d97d2014-03-31 23:36:10 +000086 if down in deletions:
87 continue
88
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000089 if parent in tag_set:
90 git.set_branch_config(down, 'remote', '.')
91 git.set_branch_config(down, 'merge', 'refs/tags/%s' % parent)
92 print ('Reparented %s to track %s [tag] (was tracking %s)'
93 % (down, parent, branch))
94 else:
95 git.run('branch', '--set-upstream-to', parent, down)
96 print ('Reparented %s to track %s (was tracking %s)'
97 % (down, parent, branch))
98
iannucci@chromium.org512d97d2014-03-31 23:36:10 +000099 deletions.add(branch)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000100 print git.run('branch', '-d', branch)
101
102
103def rebase_branch(branch, parent, start_hash):
104 logging.debug('considering %s(%s) -> %s(%s) : %s',
105 branch, git.hash_one(branch), parent, git.hash_one(parent),
106 start_hash)
107
108 # If parent has FROZEN commits, don't base branch on top of them. Instead,
109 # base branch on top of whatever commit is before them.
110 back_ups = 0
111 orig_parent = parent
112 while git.run('log', '-n1', '--format=%s',
113 parent, '--').startswith(git.FREEZE):
114 back_ups += 1
115 parent = git.run('rev-parse', parent+'~')
116
117 if back_ups:
118 logging.debug('Backed parent up by %d from %s to %s',
119 back_ups, orig_parent, parent)
120
121 if git.hash_one(parent) != start_hash:
122 # Try a plain rebase first
123 print 'Rebasing:', branch
124 if not git.rebase(parent, start_hash, branch, abort=True).success:
125 # TODO(iannucci): Find collapsible branches in a smarter way?
126 print "Failed! Attempting to squash", branch, "...",
127 squash_branch = branch+"_squash_attempt"
128 git.run('checkout', '-b', squash_branch)
129 git.squash_current_branch(merge_base=start_hash)
130
131 # Try to rebase the branch_squash_attempt branch to see if it's empty.
132 squash_ret = git.rebase(parent, start_hash, squash_branch, abort=True)
133 empty_rebase = git.hash_one(squash_branch) == git.hash_one(parent)
134 git.run('checkout', branch)
135 git.run('branch', '-D', squash_branch)
136 if squash_ret.success and empty_rebase:
137 print 'Success!'
138 git.squash_current_branch(merge_base=start_hash)
139 git.rebase(parent, start_hash, branch)
140 else:
141 # rebase and leave in mid-rebase state.
142 git.rebase(parent, start_hash, branch)
iannucci@chromium.org56a624a2014-03-26 21:23:09 +0000143 print "Failed!"
144 print
145 print "Here's what git-rebase had to say:"
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000146 print squash_ret.message
147 print
148 print textwrap.dedent(
149 """
150 Squashing failed. You probably have a real merge conflict.
151
152 Your working copy is in mid-rebase. Either:
153 * completely resolve like a normal git-rebase; OR
154 * abort the rebase and mark this branch as dormant:
155 git config branch.%s.dormant true
156
157 And then run `git rebase-update` again to resume.
158 """ % branch)
159 return False
160 else:
161 print '%s up-to-date' % branch
162
163 git.remove_merge_base(branch)
164 git.get_or_create_merge_base(branch)
165
166 return True
167
168
169def main(args=()):
170 parser = argparse.ArgumentParser()
171 parser.add_argument('--verbose', '-v', action='store_true')
172 parser.add_argument('--no_fetch', '-n', action='store_true',
173 help='Skip fetching remotes.')
174 opts = parser.parse_args(args)
175
176 if opts.verbose: # pragma: no cover
177 logging.getLogger().setLevel(logging.DEBUG)
178
179 # TODO(iannucci): snapshot all branches somehow, so we can implement
180 # `git rebase-update --undo`.
181 # * Perhaps just copy packed-refs + refs/ + logs/ to the side?
182 # * commit them to a secret ref?
183 # * Then we could view a summary of each run as a
184 # `diff --stat` on that secret ref.
185
186 if git.in_rebase():
187 # TODO(iannucci): Be able to resume rebase with flags like --continue,
188 # etc.
189 print (
190 'Rebase in progress. Please complete the rebase before running '
191 '`git rebase-update`.'
192 )
193 return 1
194
195 return_branch = find_return_branch()
196
197 if git.current_branch() == 'HEAD':
198 if git.run('status', '--porcelain'):
199 print 'Cannot rebase-update with detached head + uncommitted changes.'
200 return 1
201 else:
202 git.freeze() # just in case there are any local changes.
203
204 skipped, branch_tree = git.get_branch_tree()
205 for branch in skipped:
206 print 'Skipping %s: No upstream specified' % branch
207
208 if not opts.no_fetch:
209 fetch_remotes(branch_tree)
210
211 merge_base = {}
212 for branch, parent in branch_tree.iteritems():
213 merge_base[branch] = git.get_or_create_merge_base(branch, parent)
214
215 logging.debug('branch_tree: %s' % pformat(branch_tree))
216 logging.debug('merge_base: %s' % pformat(merge_base))
217
218 retcode = 0
219 # Rebase each branch starting with the root-most branches and working
220 # towards the leaves.
221 for branch, parent in git.topo_iter(branch_tree):
222 if git.is_dormant(branch):
223 print 'Skipping dormant branch', branch
224 else:
225 ret = rebase_branch(branch, parent, merge_base[branch])
226 if not ret:
227 retcode = 1
228 break
229
230 if not retcode:
231 remove_empty_branches(branch_tree)
232
233 # return_branch may not be there any more.
234 if return_branch in git.branches():
235 git.run('checkout', return_branch)
236 git.thaw()
237 else:
238 root_branch = git.root()
239 if return_branch != 'HEAD':
240 print (
241 "%r was merged with its parent, checking out %r instead."
242 % (return_branch, root_branch)
243 )
244 git.run('checkout', root_branch)
iannucci@chromium.orgbf157b42014-04-12 00:14:41 +0000245 git.set_config(STARTING_BRANCH_KEY, '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000246
247 return retcode
248
249
250if __name__ == '__main__': # pragma: no cover
251 sys.exit(main(sys.argv[1:]))