123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- #!/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)
|