diff options
author | Mike Frysinger <vapier@google.com> | 2019-09-11 18:43:17 -0400 |
---|---|---|
committer | Mike Frysinger <vapier@google.com> | 2019-11-16 19:55:02 +0000 |
commit | 6da17751ca4e3b90834ca763f448ddc39b32651b (patch) | |
tree | 4962f9913b689de734d3d11e981d3a2e18841504 | |
parent | 2ba5a1e96347e735b12998c057a6c1eed79712e9 (diff) | |
download | git-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-x | project.py | 39 | ||||
-rw-r--r-- | subcmds/prune.py | 13 | ||||
-rw-r--r-- | tests/test_project.py | 74 |
3 files changed, 114 insertions, 12 deletions
@@ -134,6 +134,7 @@ class DownloadedChange(object): | |||
134 | 134 | ||
135 | class ReviewableBranch(object): | 135 | class 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 | ||
19 | from __future__ import print_function | 19 | from __future__ import print_function |
20 | 20 | ||
21 | import contextlib | ||
22 | import os | ||
23 | import shutil | ||
24 | import subprocess | ||
25 | import tempfile | ||
21 | import unittest | 26 | import unittest |
22 | 27 | ||
28 | import git_config | ||
23 | import project | 29 | import project |
24 | 30 | ||
25 | 31 | ||
32 | @contextlib.contextmanager | ||
33 | def 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 | |||
26 | class RepoHookShebang(unittest.TestCase): | 45 | class 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 | |||
84 | class 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 | |||
98 | class 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) | ||