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
diff --git a/tests/git_cl_test.py b/tests/git_cl_test.py
index aa0ee54..eed3fe5 100755
--- a/tests/git_cl_test.py
+++ b/tests/git_cl_test.py
@@ -1504,6 +1504,65 @@
     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)