[git-cl] Add graceful error handling to "git cl archive".

Adds error handling logic for pre-existing tags (which can occur
if "archive" is CTRL-C aborted midway through) and for undeletable
branches (which can happen if they are currently checked out in a
working dir).

Change-Id: I27b6da9f5860c307f49cbeabb1b0ccf9cfb28eb6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1930023
Commit-Queue: Edward Lesmes <ehmaldonado@chromium.org>
Reviewed-by: Edward Lesmes <ehmaldonado@chromium.org>
Auto-Submit: Kevin Marshall <kmarshall@chromium.org>
diff --git a/tests/git_cl_test.py b/tests/git_cl_test.py
index 9ed8a1f..150369c 100755
--- a/tests/git_cl_test.py
+++ b/tests/git_cl_test.py
@@ -2274,6 +2274,7 @@
     self.calls = [
       ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'],),
        'refs/heads/master\nrefs/heads/foo\nrefs/heads/bar'),
+      ((['git', 'for-each-ref', '--format=%(refname)', 'refs/tags'],), ''),
       ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
       ((['git', 'tag', 'git-cl-archived-456-foo', 'foo'],), ''),
       ((['git', 'branch', '-D', 'foo'],), '')
@@ -2287,11 +2288,33 @@
 
     self.assertEqual(0, git_cl.main(['archive', '-f']))
 
+  def test_archive_tag_collision(self):
+    self.mock(git_cl.sys, 'stdout', StringIO())
+
+    self.calls = [
+      ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'],),
+       'refs/heads/master\nrefs/heads/foo\nrefs/heads/bar'),
+      ((['git', 'for-each-ref', '--format=%(refname)', 'refs/tags'],),
+       'refs/tags/git-cl-archived-456-foo'),
+      ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
+      ((['git', 'tag', 'git-cl-archived-456-foo-2', 'foo'],), ''),
+      ((['git', 'branch', '-D', 'foo'],), '')
+    ]
+
+    self.mock(git_cl, 'get_cl_statuses',
+              lambda branches, fine_grained, max_processes:
+              [(MockChangelistWithBranchAndIssue('master', 1), 'open'),
+               (MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
+               (MockChangelistWithBranchAndIssue('bar', 789), 'open')])
+
+    self.assertEqual(0, git_cl.main(['archive', '-f']))
+
   def test_archive_current_branch_fails(self):
     self.mock(git_cl.sys, 'stdout', StringIO())
     self.calls = [
       ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'],),
          'refs/heads/master'),
+      ((['git', 'for-each-ref', '--format=%(refname)', 'refs/tags'],), ''),
       ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
     ]
 
@@ -2307,6 +2330,7 @@
     self.calls = [
       ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'],),
          'refs/heads/master\nrefs/heads/foo\nrefs/heads/bar'),
+      ((['git', 'for-each-ref', '--format=%(refname)', 'refs/tags'],), ''),
       ((['git', 'symbolic-ref', 'HEAD'],), 'master')
     ]
 
@@ -2324,6 +2348,7 @@
     self.calls = [
       ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'],),
          'refs/heads/master\nrefs/heads/foo\nrefs/heads/bar'),
+      ((['git', 'for-each-ref', '--format=%(refname)', 'refs/tags'],), ''),
       ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
       ((['git', 'branch', '-D', 'foo'],), '')
     ]
@@ -2336,6 +2361,29 @@
 
     self.assertEqual(0, git_cl.main(['archive', '-f', '--notags']))
 
+  def test_archive_tag_cleanup_on_branch_deletion_error(self):
+    self.mock(git_cl.sys, 'stdout', StringIO())
+
+    self.calls = [
+      ((['git', 'for-each-ref', '--format=%(refname)', 'refs/heads'],),
+         'refs/heads/master\nrefs/heads/foo\nrefs/heads/bar'),
+      ((['git', 'for-each-ref', '--format=%(refname)', 'refs/tags'],), ''),
+      ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
+      ((['git', 'tag', 'git-cl-archived-456-foo', 'foo'],),
+        'refs/tags/git-cl-archived-456-foo'),
+      ((['git', 'branch', '-D', 'foo'],), CERR1),
+      ((['git', 'tag', '-d', 'git-cl-archived-456-foo'],),
+       'refs/tags/git-cl-archived-456-foo'),
+    ]
+
+    self.mock(git_cl, 'get_cl_statuses',
+              lambda branches, fine_grained, max_processes:
+              [(MockChangelistWithBranchAndIssue('master', 1), 'open'),
+               (MockChangelistWithBranchAndIssue('foo', 456), 'closed'),
+               (MockChangelistWithBranchAndIssue('bar', 789), 'open')])
+
+    self.assertEqual(0, git_cl.main(['archive', '-f']))
+
   def test_cmd_issue_erase_existing(self):
     out = StringIO()
     self.mock(git_cl.sys, 'stdout', out)