Add git cl squash-closed behavior

Introduces a new git cl command `squash-closed`. Similar to archive,
this will operate on all closed branches. Unlike archive, rather than
removing these branches and potentially leaving their downstreams in a
wedged state, this will squash the branch and appropriately reparent the
downstream. This makes this a good function to run in preparation of a
`git rebase-update`, especially if there are chained git branches, as
this will then shrink any merged branches to a single commit which
should cleanly apply and drop off.

Bug: 40264739
Change-Id: I1836f944d43f5f3dcebbebf06437bef890da96ca
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/6230362
Reviewed-by: Yiwei Zhang <yiwzhang@google.com>
Commit-Queue: Alexander Cooper <alcooper@chromium.org>
Auto-Submit: Alexander Cooper <alcooper@chromium.org>
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
Commit-Queue: Yiwei Zhang <yiwzhang@google.com>
This commit is contained in:
Alexander Cooper
2025-02-05 11:18:41 -08:00
committed by LUCI CQ
parent c630492293
commit 682a6e1194
2 changed files with 190 additions and 0 deletions

View File

@@ -2986,6 +2986,110 @@ class TestGitCl(unittest.TestCase):
0, git_cl.main(['archive', '-f', '-p',
'archived/{issue}-{branch}']))
@unittest.skipIf(gclient_utils.IsEnvCog(),
'not supported in non-git environment')
def test_squash_closed(self):
self.calls = [
((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
((['git', 'checkout', 'foo'], ), ''),
((['git', 'checkout', 'main'], ), ''),
]
mock.patch(
'git_cl.get_cl_statuses',
lambda branches, fine_grained, max_processes: [
(MockChangelistWithBranchAndIssue('main', 1), 'open'),
(MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
(MockChangelistWithBranchAndIssue('bar', 789), 'open')
]).start()
mock.patch('git_common.current_branch', return_value='main').start()
mock.patch('git_squash_branch.main', return_value=0).start()
self.assertEqual(0, git_cl.main(['squash-closed', '-f']))
@unittest.skipIf(gclient_utils.IsEnvCog(),
'not supported in non-git environment')
def test_squash_closed_dry_run(self):
self.calls = [
((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
]
mock.patch(
'git_cl.get_cl_statuses',
lambda branches, fine_grained, max_processes: [
(MockChangelistWithBranchAndIssue('main', 1), 'open'),
(MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
(MockChangelistWithBranchAndIssue('bar', 789), 'open')
]).start()
self.assertEqual(0, git_cl.main(['squash-closed', '-d']))
@unittest.skipIf(gclient_utils.IsEnvCog(),
'not supported in non-git environment')
def test_squash_closed_current_branch_fails(self):
self.calls = [
((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
]
mock.patch(
'git_cl.get_cl_statuses',
lambda branches, fine_grained, max_processes: [
(MockChangelistWithBranchAndIssue('main', 1), 'closed'),
]).start()
self.assertEqual(1, git_cl.main(['squash-closed', '-f']))
@unittest.skipIf(gclient_utils.IsEnvCog(),
'not supported in non-git environment')
def test_squash_closed_reset_on_failure(self):
self.calls = [
((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
((['git', 'checkout', 'foo'], ), ''),
((['git', 'checkout', 'main'], ), ''),
]
mock.patch(
'git_cl.get_cl_statuses',
lambda branches, fine_grained, max_processes: [
(MockChangelistWithBranchAndIssue('main', 1), 'open'),
(MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
(MockChangelistWithBranchAndIssue('bar', 789), 'open')
]).start()
mock.patch('git_common.current_branch', return_value='main').start()
mock.patch('git_squash_branch.main', return_value=1).start()
self.assertEqual(1, git_cl.main(['squash-closed', '-f']))
@unittest.skipIf(gclient_utils.IsEnvCog(),
'not supported in non-git environment')
def test_squash_closed_clean_exit_no_closed_branches(self):
self.calls = [
((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'], ),
'refs/heads/main\nrefs/heads/foo\nrefs/heads/bar'),
]
mock.patch(
'git_cl.get_cl_statuses',
lambda branches, fine_grained, max_processes: [
(MockChangelistWithBranchAndIssue('main', 1), 'open'),
(MockChangelistWithBranchAndIssue('foo', 456), 'open'),
(MockChangelistWithBranchAndIssue('bar', 789), 'open')
]).start()
mock.patch('git_common.current_branch', return_value='main').start()
mock.patch('git_squash_branch.main', return_value=0).start()
self.assertEqual(0, git_cl.main(['squash-closed', '-f']))
@unittest.skipIf(gclient_utils.IsEnvCog(),
'not supported in non-git environment')
def test_squash_closed_abort_on_dirty_tree(self):
mock.patch('git_common.is_dirty_git_tree', return_value=True).start()
self.assertEqual(1, git_cl.main(['squash-closed', '-f']))
@unittest.skipIf(gclient_utils.IsEnvCog(),
'not supported in non-git environment')
def test_cmd_issue_erase_existing(self):