You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
101 lines
3.6 KiB
Python
101 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright 2024 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 squash all branches and their downstream branches. Useful to avoid
|
|
potential conflicts during a git rebase-update with multiple stacked CLs.
|
|
'''
|
|
|
|
import argparse
|
|
import collections
|
|
import git_common as git
|
|
import sys
|
|
|
|
|
|
# Squash a branch, taking care to rebase the branch on top of the new commit
|
|
# position of its upstream branch.
|
|
def squash_branch(branch, initial_hashes):
|
|
print('Squashing branch %s.' % branch)
|
|
assert initial_hashes[branch] == git.hash_one(branch)
|
|
|
|
upstream_branch = git.upstream(branch)
|
|
old_upstream_branch = initial_hashes[upstream_branch]
|
|
|
|
# Because the branch's upstream has potentially changed from squashing it,
|
|
# the current branch is rebased on top of the new upstream.
|
|
git.run('rebase', '--onto', upstream_branch, old_upstream_branch, branch,
|
|
'--update-refs')
|
|
|
|
# Now do the squashing.
|
|
git.run('checkout', branch)
|
|
git.squash_current_branch()
|
|
|
|
|
|
# Squashes all branches that are part of the subtree starting at `branch`.
|
|
def squash_subtree(branch, initial_hashes, downstream_branches):
|
|
# The upstream default never has to be squashed (e.g. origin/main).
|
|
if branch != git.upstream_default():
|
|
squash_branch(branch, initial_hashes)
|
|
|
|
# Recurse on downstream branches, if any.
|
|
for downstream_branch in downstream_branches[branch]:
|
|
squash_subtree(downstream_branch, initial_hashes, downstream_branches)
|
|
|
|
|
|
def main(args=None):
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--ignore-no-upstream',
|
|
action='store_true',
|
|
help='Allows proceeding if any branch has no '
|
|
'upstreams.')
|
|
parser.add_argument('--branch',
|
|
'-b',
|
|
type=str,
|
|
default=git.current_branch(),
|
|
help='The name of the branch who\'s subtree must be '
|
|
'squashed. Defaults to the current branch.')
|
|
opts = parser.parse_args(args)
|
|
|
|
if git.is_dirty_git_tree('squash-branch-tree'):
|
|
return 1
|
|
|
|
branches_without_upstream, tree = git.get_branch_tree()
|
|
|
|
if not opts.ignore_no_upstream and branches_without_upstream:
|
|
print('Cannot use `git squash-branch-tree` since the following\n'
|
|
'branches don\'t have an upstream:')
|
|
for branch in branches_without_upstream:
|
|
print(f' - {branch}')
|
|
print('Use --ignore-no-upstream to ignore this check and proceed.')
|
|
return 1
|
|
|
|
diverged_branches = git.get_diverged_branches(tree)
|
|
if diverged_branches:
|
|
print('Cannot use `git squash-branch-tree` since the following\n'
|
|
'branches have diverged from their upstream and could cause\n'
|
|
'conflicts:')
|
|
for diverged_branch in diverged_branches:
|
|
print(f' - {diverged_branch}')
|
|
return 1
|
|
|
|
# Before doing the squashing, save the current branch checked out branch so
|
|
# we can go back to it at the end.
|
|
return_branch = git.current_branch()
|
|
|
|
initial_hashes = git.get_hashes(tree)
|
|
downstream_branches = git.get_downstream_branches(tree)
|
|
squash_subtree(opts.branch, initial_hashes, downstream_branches)
|
|
|
|
git.run('checkout', return_branch)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__': # pragma: no cover
|
|
try:
|
|
sys.exit(main(sys.argv[1:]))
|
|
except KeyboardInterrupt:
|
|
sys.stderr.write('interrupted\n')
|
|
sys.exit(1)
|