1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
|
#!/usr/bin/env python3
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Tool to update all branches to have the latest changes from their upstreams.
"""
import argparse
import collections
import logging
import sys
import textwrap
import os
from fnmatch import fnmatch
from pprint import pformat
import gclient_utils
import git_common as git
import setup_color
from third_party import colorama
STARTING_BRANCH_KEY = 'depot-tools.rebase-update.starting-branch'
STARTING_WORKDIR_KEY = 'depot-tools.rebase-update.starting-workdir'
RESET = colorama.Fore.RESET + colorama.Back.RESET + colorama.Style.RESET_ALL
BRIGHT = colorama.Style.BRIGHT
def find_return_branch_workdir():
"""Finds the branch and working directory which we should return to after
rebase-update completes.
These values may persist across multiple invocations of rebase-update, if
rebase-update runs into a conflict mid-way.
"""
return_branch = git.get_config(STARTING_BRANCH_KEY)
workdir = git.get_config(STARTING_WORKDIR_KEY)
if not return_branch:
workdir = os.getcwd()
git.set_config(STARTING_WORKDIR_KEY, workdir)
return_branch = git.current_branch()
if return_branch != 'HEAD':
git.set_config(STARTING_BRANCH_KEY, return_branch)
return return_branch, workdir
def fetch_remotes(branch_tree):
"""Fetches all remotes which are needed to update |branch_tree|."""
fetch_tags = False
remotes = set()
tag_set = git.tags()
fetchspec_map = {}
all_fetchspec_configs = git.get_config_regexp(r'^remote\..*\.fetch')
for key, fetchspec in all_fetchspec_configs:
dest_spec = fetchspec.partition(':')[2]
remote_name = key.split('.')[1]
fetchspec_map[dest_spec] = remote_name
for parent in branch_tree.values():
if parent in tag_set:
fetch_tags = True
else:
full_ref = git.run('rev-parse', '--symbolic-full-name', parent)
for dest_spec, remote_name in fetchspec_map.items():
if fnmatch(full_ref, dest_spec):
remotes.add(remote_name)
break
fetch_args = []
if fetch_tags:
# Need to fetch all because we don't know what remote the tag comes from
# :( TODO(iannucci): assert that the tags are in the remote fetch
# refspec
fetch_args = ['--all']
else:
fetch_args.append('--multiple')
fetch_args.extend(remotes)
# TODO(iannucci): Should we fetch git-svn?
if not fetch_args: # pragma: no cover
print('Nothing to fetch.')
else:
git.run_with_stderr('fetch',
*fetch_args,
stdout=sys.stdout,
stderr=sys.stderr)
def remove_empty_branches(branch_tree):
tag_set = git.tags()
ensure_root_checkout = git.once(lambda: git.run('checkout', git.root()))
deletions = {}
reparents = {}
downstreams = collections.defaultdict(list)
for branch, parent in git.topo_iter(branch_tree, top_down=False):
if git.is_dormant(branch):
continue
downstreams[parent].append(branch)
# If branch and parent have the same tree, then branch has to be marked
# for deletion and its children and grand-children reparented to parent.
if git.hash_one(branch + ":") == git.hash_one(parent + ":"):
ensure_root_checkout()
logging.debug('branch %s merged to %s', branch, parent)
# Mark branch for deletion while remembering the ordering, then add
# all its children as grand-children of its parent and record
# reparenting information if necessary.
deletions[branch] = len(deletions)
for down in downstreams[branch]:
if down in deletions:
continue
# Record the new and old parent for down, or update such a
# record if it already exists. Keep track of the ordering so
# that reparenting happen in topological order.
downstreams[parent].append(down)
if down not in reparents:
reparents[down] = (len(reparents), parent, branch)
else:
order, _, old_parent = reparents[down]
reparents[down] = (order, parent, old_parent)
# Apply all reparenting recorded, in order.
for branch, value in sorted(reparents.items(), key=lambda x: x[1][0]):
_, parent, old_parent = value
if parent in tag_set:
git.set_branch_config(branch, 'remote', '.')
git.set_branch_config(branch, 'merge', 'refs/tags/%s' % parent)
print('Reparented %s to track %s [tag] (was tracking %s)' %
(branch, parent, old_parent))
else:
git.run('branch', '--set-upstream-to', parent, branch)
print('Reparented %s to track %s (was tracking %s)' %
(branch, parent, old_parent))
# Apply all deletions recorded, in order.
for branch, _ in sorted(deletions.items(), key=lambda x: x[1]):
print(git.run('branch', '-d', branch))
def format_branch_name(branch):
return BRIGHT + branch + RESET
def rebase_branch(branch, parent, start_hash, no_squash):
logging.debug('considering %s(%s) -> %s(%s) : %s', branch,
git.hash_one(branch), parent, git.hash_one(parent),
start_hash)
# If parent has FROZEN commits, don't base branch on top of them. Instead,
# base branch on top of whatever commit is before them.
back_ups = 0
orig_parent = parent
while git.run('log', '-n1', '--format=%s', parent,
'--').startswith(git.FREEZE):
back_ups += 1
parent = git.run('rev-parse', parent + '~')
if back_ups:
logging.debug('Backed parent up by %d from %s to %s', back_ups,
orig_parent, parent)
if git.hash_one(parent) != start_hash:
# Try a plain rebase first
print('Rebasing:', format_branch_name(branch))
consider_squashing = git.get_num_commits(branch) != 1 and not (
no_squash)
rebase_ret = git.rebase(parent,
start_hash,
branch,
abort=consider_squashing)
if not rebase_ret.success:
mid_rebase_message = textwrap.dedent("""\
Your working copy is in mid-rebase. Either:
* completely resolve like a normal git-rebase; OR
* abort the rebase and mark this branch as dormant:
git rebase --abort && \\
git config branch.%s.dormant true
And then run `git rebase-update -n` to resume.
""" % branch)
if not consider_squashing:
print(mid_rebase_message)
return False
print("Failed! Attempting to squash",
format_branch_name(branch),
"...",
end=' ')
sys.stdout.flush()
squash_branch = branch + "_squash_attempt"
git.run('checkout', '-b', squash_branch)
git.squash_current_branch(merge_base=start_hash)
# Try to rebase the branch_squash_attempt branch to see if it's
# empty.
squash_ret = git.rebase(parent,
start_hash,
squash_branch,
abort=True)
empty_rebase = git.hash_one(squash_branch) == git.hash_one(parent)
git.run('checkout', branch)
git.run('branch', '-D', squash_branch)
if squash_ret.success and empty_rebase:
print('Success!')
git.squash_current_branch(merge_base=start_hash)
git.rebase(parent, start_hash, branch)
else:
print("Failed!")
print()
# rebase and leave in mid-rebase state.
# This second rebase attempt should always fail in the same
# way that the first one does. If it magically succeeds then
# something very strange has happened.
second_rebase_ret = git.rebase(parent, start_hash, branch)
if second_rebase_ret.success: # pragma: no cover
print("Second rebase succeeded unexpectedly!")
print("Please see: http://crbug.com/425696")
print("First rebased failed with:")
print(rebase_ret.stderr)
else:
print("Here's what git-rebase (squashed) had to say:")
print()
print(squash_ret.stdout)
print(squash_ret.stderr)
print(
textwrap.dedent("""\
Squashing failed. You probably have a real merge conflict.
"""))
print(mid_rebase_message)
return False
else:
print('%s up-to-date' % format_branch_name(branch))
git.remove_merge_base(branch)
git.get_or_create_merge_base(branch)
return True
def with_downstream_branches(base_branches, branch_tree):
"""Returns a set of base_branches and all downstream branches."""
downstream_branches = set()
for branch, parent in git.topo_iter(branch_tree):
if parent in base_branches or parent in downstream_branches:
downstream_branches.add(branch)
return downstream_branches.union(base_branches)
def main(args=None):
if gclient_utils.IsEnvCog():
print(
'rebase-update command is not supported. Please navigate to source '
'control view in the activity bar to rebase your changes.',
file=sys.stderr)
return 1
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', '-v', action='store_true')
parser.add_argument('--keep-going',
'-k',
action='store_true',
help='Keep processing past failed rebases.')
parser.add_argument('--no_fetch',
'--no-fetch',
'-n',
action='store_true',
help='Skip fetching remotes.')
parser.add_argument('--current',
action='store_true',
help='Only rebase the current branch.')
parser.add_argument('--tree',
action='store_true',
help='Rebase all branches downstream from the '
'selected branch(es).')
parser.add_argument('branches',
nargs='*',
help='Branches to be rebased. All branches are assumed '
'if none specified.')
parser.add_argument('--keep-empty',
'-e',
action='store_true',
help='Do not automatically delete empty branches.')
parser.add_argument(
'--no-squash',
action='store_true',
help='Will not try to squash branches if rebasing fails.')
opts = parser.parse_args(args)
if opts.verbose: # pragma: no cover
logging.getLogger().setLevel(logging.DEBUG)
# TODO(iannucci): snapshot all branches somehow, so we can implement
# `git rebase-update --undo`.
# * Perhaps just copy packed-refs + refs/ + logs/ to the side?
# * commit them to a secret ref?
# * Then we could view a summary of each run as a
# `diff --stat` on that secret ref.
if git.in_rebase():
# TODO(iannucci): Be able to resume rebase with flags like --continue,
# etc.
print('Rebase in progress. Please complete the rebase before running '
'`git rebase-update`.')
return 1
return_branch, return_workdir = find_return_branch_workdir()
os.chdir(git.run('rev-parse', '--show-toplevel'))
if git.current_branch() == 'HEAD':
if git.run('status', '--porcelain', '--ignore-submodules=all'):
print(
'Cannot rebase-update with detached head + uncommitted changes.'
)
return 1
else:
git.freeze() # just in case there are any local changes.
branches_to_rebase = set(opts.branches)
if opts.current:
branches_to_rebase.add(git.current_branch())
skipped, branch_tree = git.get_branch_tree(use_limit=not opts.current)
if opts.tree:
branches_to_rebase = with_downstream_branches(branches_to_rebase,
branch_tree)
if branches_to_rebase:
skipped = set(skipped).intersection(branches_to_rebase)
for branch in skipped:
print('Skipping %s: No upstream specified' % format_branch_name(branch))
if not opts.no_fetch:
fetch_remotes(branch_tree)
merge_base = {}
for branch, parent in branch_tree.items():
merge_base[branch] = git.get_or_create_merge_base(branch, parent)
logging.debug('branch_tree: %s' % pformat(branch_tree))
logging.debug('merge_base: %s' % pformat(merge_base))
retcode = 0
unrebased_branches = []
# Rebase each branch starting with the root-most branches and working
# towards the leaves.
for branch, parent in git.topo_iter(branch_tree):
# Only rebase specified branches, unless none specified.
if branches_to_rebase and branch not in branches_to_rebase:
continue
if git.is_dormant(branch):
print('Skipping dormant branch', format_branch_name(branch))
else:
ret = rebase_branch(branch, parent, merge_base[branch],
opts.no_squash)
if not ret:
retcode = 1
if opts.keep_going:
print('--keep-going set, continuing with next branch.')
unrebased_branches.append(branch)
if git.in_rebase():
git.run_with_retcode('rebase', '--abort')
if git.in_rebase(): # pragma: no cover
print(
'Failed to abort rebase. Something is really wrong.'
)
break
else:
break
if unrebased_branches:
print()
print('The following branches could not be cleanly rebased:')
for branch in unrebased_branches:
print(' %s' % format_branch_name(branch))
if not retcode:
if not opts.keep_empty:
remove_empty_branches(branch_tree)
# return_branch may not be there any more.
if return_branch in git.branches(use_limit=False):
git.run('checkout', return_branch)
git.thaw()
else:
root_branch = git.root()
if return_branch != 'HEAD':
print(
"%s was merged with its parent, checking out %s instead." %
(git.unicode_repr(return_branch),
git.unicode_repr(root_branch)))
git.run('checkout', root_branch)
# return_workdir may also not be there any more.
if return_workdir:
try:
os.chdir(return_workdir)
except OSError as e:
print("Unable to return to original workdir %r: %s" %
(return_workdir, e))
git.set_config(STARTING_BRANCH_KEY, '')
git.set_config(STARTING_WORKDIR_KEY, '')
print()
print("Running `git gc --auto` - Ctrl-C to abort is OK.")
git.run('gc', '--auto')
return retcode
if __name__ == '__main__': # pragma: no cover
setup_color.init()
try:
sys.exit(main())
except KeyboardInterrupt:
sys.stderr.write('interrupted\n')
sys.exit(1)
|