git_squash_branch_tree.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. #!/usr/bin/env python3
  2. # Copyright 2024 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. Tool to squash all branches and their downstream branches. Useful to avoid
  7. potential conflicts during a git rebase-update with multiple stacked CLs.
  8. '''
  9. import argparse
  10. import collections
  11. import git_common as git
  12. import sys
  13. # Squash a branch, taking care to rebase the branch on top of the new commit
  14. # position of its upstream branch.
  15. def squash_branch(branch, initial_hashes):
  16. print('Squashing branch %s.' % branch)
  17. assert initial_hashes[branch] == git.hash_one(branch)
  18. upstream_branch = git.upstream(branch)
  19. old_upstream_branch = initial_hashes[upstream_branch]
  20. # Because the branch's upstream has potentially changed from squashing it,
  21. # the current branch is rebased on top of the new upstream.
  22. git.run('rebase', '--onto', upstream_branch, old_upstream_branch, branch,
  23. '--update-refs')
  24. # Now do the squashing.
  25. git.run('checkout', branch)
  26. git.squash_current_branch()
  27. # Squashes all branches that are part of the subtree starting at `branch`.
  28. def squash_subtree(branch, initial_hashes, downstream_branches):
  29. # The upstream default never has to be squashed (e.g. origin/main).
  30. if branch != git.upstream_default():
  31. squash_branch(branch, initial_hashes)
  32. # Recurse on downstream branches, if any.
  33. for downstream_branch in downstream_branches[branch]:
  34. squash_subtree(downstream_branch, initial_hashes, downstream_branches)
  35. def main(args=None):
  36. parser = argparse.ArgumentParser()
  37. parser.add_argument('--ignore-no-upstream',
  38. action='store_true',
  39. help='Allows proceeding if any branch has no '
  40. 'upstreams.')
  41. parser.add_argument('--branch',
  42. '-b',
  43. type=str,
  44. default=git.current_branch(),
  45. help='The name of the branch who\'s subtree must be '
  46. 'squashed. Defaults to the current branch.')
  47. opts = parser.parse_args(args)
  48. if git.is_dirty_git_tree('squash-branch-tree'):
  49. return 1
  50. branches_without_upstream, tree = git.get_branch_tree()
  51. if not opts.ignore_no_upstream and branches_without_upstream:
  52. print('Cannot use `git squash-branch-tree` since the following\n'
  53. 'branches don\'t have an upstream:')
  54. for branch in branches_without_upstream:
  55. print(f' - {branch}')
  56. print('Use --ignore-no-upstream to ignore this check and proceed.')
  57. return 1
  58. diverged_branches = git.get_diverged_branches(tree)
  59. if diverged_branches:
  60. print('Cannot use `git squash-branch-tree` since the following\n'
  61. 'branches have diverged from their upstream and could cause\n'
  62. 'conflicts:')
  63. for diverged_branch in diverged_branches:
  64. print(f' - {diverged_branch}')
  65. return 1
  66. # Before doing the squashing, save the current branch checked out branch so
  67. # we can go back to it at the end.
  68. return_branch = git.current_branch()
  69. initial_hashes = git.get_hashes(tree)
  70. downstream_branches = git.get_downstream_branches(tree)
  71. squash_subtree(opts.branch, initial_hashes, downstream_branches)
  72. git.run('checkout', return_branch)
  73. return 0
  74. if __name__ == '__main__': # pragma: no cover
  75. try:
  76. sys.exit(main(sys.argv[1:]))
  77. except KeyboardInterrupt:
  78. sys.stderr.write('interrupted\n')
  79. sys.exit(1)