summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Frysinger <vapier@google.com>2019-09-11 18:43:17 -0400
committerMike Frysinger <vapier@google.com>2019-11-16 19:55:02 +0000
commit6da17751ca4e3b90834ca763f448ddc39b32651b (patch)
tree4962f9913b689de734d3d11e981d3a2e18841504
parent2ba5a1e96347e735b12998c057a6c1eed79712e9 (diff)
downloadgit-repo-6da17751ca4e3b90834ca763f448ddc39b32651b.tar.gz
prune: handle branches that track missing branches
Series of steps: * Create a local "b1" branch with `repo start b1` that tracks a remote branch (totally fine) * Manually create a local "b2" branch with `git branch --track b1 b2` that tracks the local "b1" (uh-oh...) * Delete the local "b1" branch manually or via `repo prune` (....) * Try to process the "b2" branch with `repo prune` Since b2 tracks a branch that no longer exists, everything blows up at this point as we try to probe the non-existent ref. Instead, we should flag this as unknown and leave it up to the user to resolve. This probably could come up if a local branch was tracking a remote branch that was deleted from the server, and users ran something like `repo sync --prune` which cleaned up the remote refs. Bug: https://crbug.com/gerrit/11485 Change-Id: I6b6b6041943944b8efa6e2ad0b8b10f13a75a5c2 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/236793 Reviewed-by: David Pursehouse <dpursehouse@collab.net> Reviewed-by: Kirtika Ruchandani <kirtika@google.com> Reviewed-by: Mike Frysinger <vapier@google.com> Tested-by: Mike Frysinger <vapier@google.com>
-rwxr-xr-xproject.py39
-rw-r--r--subcmds/prune.py13
-rw-r--r--tests/test_project.py74
3 files changed, 114 insertions, 12 deletions
diff --git a/project.py b/project.py
index 5ffa5424..7811d6ba 100755
--- a/project.py
+++ b/project.py
@@ -134,6 +134,7 @@ class DownloadedChange(object):
134 134
135class ReviewableBranch(object): 135class ReviewableBranch(object):
136 _commit_cache = None 136 _commit_cache = None
137 _base_exists = None
137 138
138 def __init__(self, project, branch, base): 139 def __init__(self, project, branch, base):
139 self.project = project 140 self.project = project
@@ -147,14 +148,19 @@ class ReviewableBranch(object):
147 @property 148 @property
148 def commits(self): 149 def commits(self):
149 if self._commit_cache is None: 150 if self._commit_cache is None:
150 self._commit_cache = self.project.bare_git.rev_list('--abbrev=8', 151 args = ('--abbrev=8', '--abbrev-commit', '--pretty=oneline', '--reverse',
151 '--abbrev-commit', 152 '--date-order', not_rev(self.base), R_HEADS + self.name, '--')
152 '--pretty=oneline', 153 try:
153 '--reverse', 154 self._commit_cache = self.project.bare_git.rev_list(*args)
154 '--date-order', 155 except GitError:
155 not_rev(self.base), 156 # We weren't able to probe the commits for this branch. Was it tracking
156 R_HEADS + self.name, 157 # a branch that no longer exists? If so, return no commits. Otherwise,
157 '--') 158 # rethrow the error as we don't know what's going on.
159 if self.base_exists:
160 raise
161
162 self._commit_cache = []
163
158 return self._commit_cache 164 return self._commit_cache
159 165
160 @property 166 @property
@@ -173,6 +179,23 @@ class ReviewableBranch(object):
173 R_HEADS + self.name, 179 R_HEADS + self.name,
174 '--') 180 '--')
175 181
182 @property
183 def base_exists(self):
184 """Whether the branch we're tracking exists.
185
186 Normally it should, but sometimes branches we track can get deleted.
187 """
188 if self._base_exists is None:
189 try:
190 self.project.bare_git.rev_parse('--verify', not_rev(self.base))
191 # If we're still here, the base branch exists.
192 self._base_exists = True
193 except GitError:
194 # If we failed to verify, the base branch doesn't exist.
195 self._base_exists = False
196
197 return self._base_exists
198
176 def UploadForReview(self, people, 199 def UploadForReview(self, people,
177 auto_topic=False, 200 auto_topic=False,
178 draft=False, 201 draft=False,
diff --git a/subcmds/prune.py b/subcmds/prune.py
index dc8b8c9d..ff2fba1d 100644
--- a/subcmds/prune.py
+++ b/subcmds/prune.py
@@ -51,11 +51,16 @@ class Prune(PagedCommand):
51 out.project('project %s/' % project.relpath) 51 out.project('project %s/' % project.relpath)
52 out.nl() 52 out.nl()
53 53
54 commits = branch.commits 54 print('%s %-33s ' % (
55 date = branch.date
56 print('%s %-33s (%2d commit%s, %s)' % (
57 branch.name == project.CurrentBranch and '*' or ' ', 55 branch.name == project.CurrentBranch and '*' or ' ',
58 branch.name, 56 branch.name), end='')
57
58 if not branch.base_exists:
59 print('(ignoring: tracking branch is gone: %s)' % (branch.base,))
60 else:
61 commits = branch.commits
62 date = branch.date
63 print('(%2d commit%s, %s)' % (
59 len(commits), 64 len(commits),
60 len(commits) != 1 and 's' or ' ', 65 len(commits) != 1 and 's' or ' ',
61 date)) 66 date))
diff --git a/tests/test_project.py b/tests/test_project.py
index 9b289e11..77126dff 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -18,11 +18,30 @@
18 18
19from __future__ import print_function 19from __future__ import print_function
20 20
21import contextlib
22import os
23import shutil
24import subprocess
25import tempfile
21import unittest 26import unittest
22 27
28import git_config
23import project 29import project
24 30
25 31
32@contextlib.contextmanager
33def TempGitTree():
34 """Create a new empty git checkout for testing."""
35 # TODO(vapier): Convert this to tempfile.TemporaryDirectory once we drop
36 # Python 2 support entirely.
37 try:
38 tempdir = tempfile.mkdtemp(prefix='repo-tests')
39 subprocess.check_call(['git', 'init'], cwd=tempdir)
40 yield tempdir
41 finally:
42 shutil.rmtree(tempdir)
43
44
26class RepoHookShebang(unittest.TestCase): 45class RepoHookShebang(unittest.TestCase):
27 """Check shebang parsing in RepoHook.""" 46 """Check shebang parsing in RepoHook."""
28 47
@@ -60,3 +79,58 @@ class RepoHookShebang(unittest.TestCase):
60 for shebang, interp in DATA: 79 for shebang, interp in DATA:
61 self.assertEqual(project.RepoHook._ExtractInterpFromShebang(shebang), 80 self.assertEqual(project.RepoHook._ExtractInterpFromShebang(shebang),
62 interp) 81 interp)
82
83
84class FakeProject(object):
85 """A fake for Project for basic functionality."""
86
87 def __init__(self, worktree):
88 self.worktree = worktree
89 self.gitdir = os.path.join(worktree, '.git')
90 self.name = 'fakeproject'
91 self.work_git = project.Project._GitGetByExec(
92 self, bare=False, gitdir=self.gitdir)
93 self.bare_git = project.Project._GitGetByExec(
94 self, bare=True, gitdir=self.gitdir)
95 self.config = git_config.GitConfig.ForRepository(gitdir=self.gitdir)
96
97
98class ReviewableBranchTests(unittest.TestCase):
99 """Check ReviewableBranch behavior."""
100
101 def test_smoke(self):
102 """A quick run through everything."""
103 with TempGitTree() as tempdir:
104 fakeproj = FakeProject(tempdir)
105
106 # Generate some commits.
107 with open(os.path.join(tempdir, 'readme'), 'w') as fp:
108 fp.write('txt')
109 fakeproj.work_git.add('readme')
110 fakeproj.work_git.commit('-mAdd file')
111 fakeproj.work_git.checkout('-b', 'work')
112 fakeproj.work_git.rm('-f', 'readme')
113 fakeproj.work_git.commit('-mDel file')
114
115 # Start off with the normal details.
116 rb = project.ReviewableBranch(
117 fakeproj, fakeproj.config.GetBranch('work'), 'master')
118 self.assertEqual('work', rb.name)
119 self.assertEqual(1, len(rb.commits))
120 self.assertIn('Del file', rb.commits[0])
121 d = rb.unabbrev_commits
122 self.assertEqual(1, len(d))
123 short, long = next(iter(d.items()))
124 self.assertTrue(long.startswith(short))
125 self.assertTrue(rb.base_exists)
126 # Hard to assert anything useful about this.
127 self.assertTrue(rb.date)
128
129 # Now delete the tracking branch!
130 fakeproj.work_git.branch('-D', 'master')
131 rb = project.ReviewableBranch(
132 fakeproj, fakeproj.config.GetBranch('work'), 'master')
133 self.assertEqual(0, len(rb.commits))
134 self.assertFalse(rb.base_exists)
135 # Hard to assert anything useful about this.
136 self.assertTrue(rb.date)