Commit 3bff56bb authored by kmarshall's avatar kmarshall Committed by Commit bot

Add "archive" command to git_cl.py.

This command archives branches whose Rietveldt status is closed by
creating new Git tags for each of the branches' heads, and then
deleting the branch. It automatically cleans up the clutter that
accumulates over time in a long-lived Git checkout.

For example, the branch "foo-bar" associated with the
closed issue 1568403002 will be archived to the tag
"git-cl-archived-1568403002-foo-bar".

BUG=616404
R=martiniss@chromium.org,tandrii@chromium.org

Review-Url: https://codereview.chromium.org/1991563005
parent 208c0539
......@@ -2991,6 +2991,10 @@ def get_cl_statuses(changes, fine_grained, max_processes=None):
fetch = lambda cl: (cl, cl.GetStatus())
yield fetch(changes[0])
if not changes:
# Exit early if there was only one branch to fetch.
return
changes_to_fetch = changes[1:]
pool = ThreadPool(
min(max_processes, len(changes_to_fetch))
......@@ -3118,6 +3122,69 @@ def upload_branch_deps(cl, args):
return 0
def CMDarchive(parser, args):
"""Archives and deletes branches associated with closed changelists."""
parser.add_option(
'-j', '--maxjobs', action='store', type=int,
help='The maximum number of jobs to use when retrieving review status')
parser.add_option(
'-f', '--force', action='store_true',
help='Bypasses the confirmation prompt.')
auth.add_auth_options(parser)
options, args = parser.parse_args(args)
if args:
parser.error('Unsupported args: %s' % ' '.join(args))
auth_config = auth.extract_auth_config_from_options(options)
branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
if not branches:
return 0
print 'Finding all branches associated with closed issues...'
changes = [Changelist(branchref=b, auth_config=auth_config)
for b in branches.splitlines()]
alignment = max(5, max(len(c.GetBranch()) for c in changes))
statuses = get_cl_statuses(changes,
fine_grained=True,
max_processes=options.maxjobs)
proposal = [(cl.GetBranch(),
'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
for cl, status in statuses
if status == 'closed']
proposal.sort()
if not proposal:
print 'No branches with closed codereview issues found.'
return 0
current_branch = GetCurrentBranch()
print '\nBranches with closed issues that will be archived:\n'
print '%*s | %s' % (alignment, 'Branch name', 'Archival tag name')
for next_item in proposal:
print '%*s %s' % (alignment, next_item[0], next_item[1])
if any(branch == current_branch for branch, _ in proposal):
print('You are currently on a branch \'%s\' which is associated with a '
'closed codereview issue, so archive cannot proceed. Please '
'checkout another branch and run this command again.' %
current_branch)
return 1
if not options.force:
if ask_for_data('\nProceed with deletion (Y/N)? ').lower() != 'y':
print 'Aborted.'
return 1
for branch, tagname in proposal:
RunGit(['tag', tagname, branch])
RunGit(['branch', '-D', branch])
print '\nJob\'s done!'
return 0
def CMDstatus(parser, args):
"""Show status of changelists.
......
......@@ -1504,6 +1504,65 @@ class TestGitCl(TestCase):
self.assertEqual(0, git_cl.main(['description', '-n', '-']))
self.assertEqual('hi\n\t there\n\nman', ChangelistMock.desc)
def test_archive(self):
self.calls = \
[((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'],),
'refs/heads/master\nrefs/heads/foo\nrefs/heads/bar'),
((['git', 'config', 'branch.master.rietveldissue'],), '1'),
((['git', 'config', 'rietveld.autoupdate'],), ''),
((['git', 'config', 'rietveld.server'],), ''),
((['git', 'config', 'rietveld.server'],), ''),
((['git', 'config', 'branch.foo.rietveldissue'],), '456'),
((['git', 'config', 'rietveld.server'],), ''),
((['git', 'config', 'rietveld.server'],), ''),
((['git', 'config', 'branch.bar.rietveldissue'],), ''),
((['git', 'config', 'branch.bar.gerritissue'],), '789'),
((['git', 'symbolic-ref', 'HEAD'],), 'master'),
((['git', 'tag', 'git-cl-archived-456-foo', 'foo'],), ''),
((['git', 'branch', '-D', 'foo'],), '')]
class MockChangelist():
def __init__(self, branch, issue):
self.branch = branch
self.issue = issue
def GetBranch(self):
return self.branch
def GetIssue(self):
return self.issue
self.mock(git_cl, 'get_cl_statuses',
lambda branches, fine_grained, max_processes:
[(MockChangelist('master', 1), 'open'),
(MockChangelist('foo', 456), 'closed'),
(MockChangelist('bar', 789), 'open')])
self.assertEqual(0, git_cl.main(['archive', '-f']))
def test_archive_current_branch_fails(self):
self.calls = \
[((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'],),
'refs/heads/master'),
((['git', 'config', 'branch.master.rietveldissue'],), '1'),
((['git', 'config', 'rietveld.autoupdate'],), ''),
((['git', 'config', 'rietveld.server'],), ''),
((['git', 'config', 'rietveld.server'],), ''),
((['git', 'symbolic-ref', 'HEAD'],), 'master')]
class MockChangelist():
def __init__(self, branch, issue):
self.branch = branch
self.issue = issue
def GetBranch(self):
return self.branch
def GetIssue(self):
return self.issue
self.mock(git_cl, 'get_cl_statuses',
lambda branches, fine_grained, max_processes:
[(MockChangelist('master', 1), 'closed')])
self.assertEqual(1, git_cl.main(['archive', '-f']))
def test_cmd_issue_erase_existing(self):
out = StringIO.StringIO()
self.mock(git_cl.sys, 'stdout', out)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment