Merge ~racb/git-ubuntu:fix-rich-history-unescaped-patch into git-ubuntu:master
- Git
- lp:~racb/git-ubuntu
- fix-rich-history-unescaped-patch
- Merge into master
Status: | Merged |
---|---|
Merged at revision: | 0376feae802c37ba6e71b7eca899230c32bc7904 |
Proposed branch: | ~racb/git-ubuntu:fix-rich-history-unescaped-patch |
Merge into: | git-ubuntu:master |
Diff against target: |
262 lines (+81/-58) 4 files modified
doc/STYLE.md (+0/-9) gitubuntu/rich_history.py (+17/-15) gitubuntu/rich_history_test.py (+64/-14) gitubuntu/test_fixtures.py (+0/-20) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Bryce Harrington | Approve | ||
Review via email: mp+383597@code.launchpad.net |
Commit message
Make Jenkins happy
Description of the change
Server Team CI bot (server-team-bot) wrote : | # |
Bryce Harrington (bryce) wrote : | # |
8f84439e71276f9
- Test refactoring, to make it use a single repo rather than source
and dest ones.
- Drops the repo_fixture
- LGTM
√ Testsuite result: passed
1c52d81028861d1
- Adds xfail test for bug about patch in message
- I would suggest some verbage changes to the test description:
"""Commit messages with patch-like contents should round trip
If a commit message in rich history contains text in a patch-like
format (such as a diff of some file), it should not prevent correct
round-tripping. "git format-patch" followed by "git am" fails this
test, for example.
...
"""
- + :param repo: our standard repo fixture.
Should be ":param Repo repo: our..."
√ Testsuite result: passed
c2555935fcb8fc6
- Changes from using git am to git cherry-pick, the main implication
is needing to hand it commit id's rather than patch mbox's.
- This feels like it will be far more robust
- One xfail test case is re-enabled
√ Testsuite result: passed
- LGTM
e30452d73045c9a
- LGTM
This MP is well organized, first introducing an xfail test case that catches the bug, then the fix, and separating out a preliminary refactoring to support the test, and a cleanup after. I verified the xfail case does indeed fail initially, and is fixed in subsequent commits.
The fix itself seems a lot more robust to me, because it shifts to rely on git cherry-pick rather than git am and in my experience the former is a lot stricter and thus more reliable for our use case. cherry-pick leverages git's internal functionality, while git am's nature has it limited by text file i/o and the patch file format.
I have a couple suggestions on improvements for test case pydocs, but nothing major and no need for re-review.
Robie Basak (racb) wrote : | # |
Thanks. Agreed with the docstring change. I have updated the patchset and pushed. Now awaiting CI result before landing.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:0376feae802
https:/
Executed test runs:
SUCCESS: VM Setup
SUCCESS: Build
SUCCESS: Unit Tests
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Preview Diff
1 | diff --git a/doc/STYLE.md b/doc/STYLE.md |
2 | index 8a54d95..eb1e166 100644 |
3 | --- a/doc/STYLE.md |
4 | +++ b/doc/STYLE.md |
5 | @@ -101,12 +101,3 @@ We use [pytest fixtures](https://docs.pytest.org/en/latest/fixture.html) |
6 | to help us with this. Use of pytest's built-in `tmpdir` fixture is very |
7 | common, as is `repo` and `pygit2_repo` as defined in |
8 | `gitubuntu.test_fixtures`. |
9 | - |
10 | -Sometimes a test needs multiple instances of these fixtures. |
11 | -`gitubuntu.test_fixtures.repo_factory` achieves this for the `repo` |
12 | -fixture, and |
13 | -`gitubuntu.rich_history_test::test_rich_history_preservation` is an |
14 | -example of its use. A similar pattern may be used if multiple instances |
15 | -of other fixtures become necessary. In time it may make sense to |
16 | -refactor this into a generic "make a factory out of this fixture" |
17 | -function. |
18 | diff --git a/gitubuntu/rich_history.py b/gitubuntu/rich_history.py |
19 | index ac178fb..c474f3f 100644 |
20 | --- a/gitubuntu/rich_history.py |
21 | +++ b/gitubuntu/rich_history.py |
22 | @@ -63,11 +63,13 @@ def export_upload_tag(repo, path, upload_tag, commit_map): |
23 | """Export a single upload tag for later reconstruction |
24 | |
25 | If successful, two files will be created in the output directory: one with |
26 | - extension '.base', and one with extension '.mbox'. The base name of these |
27 | + extension '.base', and one with extension '.pick'. The base name of these |
28 | files is the version string from the upload tag. The '.base' file will |
29 | contain a single LF-terminated string which is the version string from the |
30 | - import tag on which the upload tag patchset is based. The '.mbox' file will |
31 | - contain the patchset as exported using 'git format-patch --stdout'. |
32 | + import tag on which the upload tag patchset is based. The '.pick' file will |
33 | + contain the patchset described as a series of commit hash strings that can |
34 | + be picked in order, one on each line, as described by 'git rev-list |
35 | + --reverse'. |
36 | |
37 | Note that the upload and import tag names are already escaped according to |
38 | dep14, so the output filenames will have their version parts escaped |
39 | @@ -81,7 +83,7 @@ def export_upload_tag(repo, path, upload_tag, commit_map): |
40 | :raises BaseNotFoundError: if a base import tag cannot be found. |
41 | :raises MultipleParentError: if multiple parents for a commit are found. |
42 | :raises subprocess.CalledProcessError: if the underlying call to "git |
43 | - format-patch" fails. |
44 | + rev-list" fails. |
45 | :returns: None. |
46 | """ |
47 | import_tag = get_upload_tag_base(repo, upload_tag, commit_map) |
48 | @@ -96,12 +98,12 @@ def export_upload_tag(repo, path, upload_tag, commit_map): |
49 | initial_env=repo._initial_env, |
50 | update_env=repo.env, |
51 | ) |
52 | - with open(os.path.join(path, export_name) + '.mbox', 'w') as f: |
53 | + with open(os.path.join(path, export_name) + '.pick', 'w') as f: |
54 | subprocess.check_call( |
55 | [ |
56 | 'git', |
57 | - 'format-patch', |
58 | - '--stdout', |
59 | + 'rev-list', |
60 | + '--reverse', |
61 | '%s..%s' % (import_tag.name, upload_tag.name), |
62 | ], |
63 | env=env, |
64 | @@ -155,8 +157,8 @@ def import_single(repo, path, version, namespace='importer'): |
65 | :raises FileNotFoundError: if rich history cannot be found for this |
66 | version. |
67 | :raises BaseNotFoundError: if a base import tag cannot be found. |
68 | - :raises subprocess.CalledProcessError: if the underlying call to "git |
69 | - am" or "git tag" fail. |
70 | + :raises subprocess.CalledProcessError: if any of the underlying calls to |
71 | + "git cherry-pick" or "git tag" fail. |
72 | :returns: None. |
73 | """ |
74 | version_name = gitubuntu.git_repository.git_dep14_tag(version) |
75 | @@ -170,17 +172,17 @@ def import_single(repo, path, version, namespace='importer'): |
76 | except KeyError as e: |
77 | raise BaseNotFoundError() from e |
78 | |
79 | - with open(os.path.join(path, version_name) + '.mbox', 'r') as f: |
80 | + with open(os.path.join(path, version_name) + '.pick', 'r') as pick: |
81 | with repo.temporary_worktree(import_tag_name, 'rich-history-import'): |
82 | env = { |
83 | 'GIT_COMMITTER_NAME': spec.SYNTHESIZED_COMMITTER_NAME, |
84 | 'GIT_COMMITTER_EMAIL': spec.SYNTHESIZED_COMMITTER_EMAIL, |
85 | } |
86 | - subprocess.check_call( |
87 | - ['git', 'am'], |
88 | - stdin=f, |
89 | - env=env, |
90 | - ) |
91 | + for commit_string in pick: |
92 | + subprocess.check_call( |
93 | + ['git', 'cherry-pick', commit_string.strip()], |
94 | + env=env, |
95 | + ) |
96 | upload_tag_name = '%s/upload/%s' % (namespace, version_name) |
97 | subprocess.check_call( |
98 | [ |
99 | diff --git a/gitubuntu/rich_history_test.py b/gitubuntu/rich_history_test.py |
100 | index 5223f2d..1716546 100644 |
101 | --- a/gitubuntu/rich_history_test.py |
102 | +++ b/gitubuntu/rich_history_test.py |
103 | @@ -5,12 +5,12 @@ import pytest |
104 | |
105 | from gitubuntu.repo_builder import Blob, Commit, Placeholder, Repo, Tree |
106 | import gitubuntu.spec as spec |
107 | -from gitubuntu.test_fixtures import repo, repo_factory |
108 | +from gitubuntu.test_fixtures import repo |
109 | |
110 | import gitubuntu.rich_history as target |
111 | |
112 | |
113 | -def test_rich_history_preservation(tmpdir, repo_factory): |
114 | +def test_rich_history_preservation(tmpdir, repo): |
115 | """An export followed by an import should preserve rich history |
116 | |
117 | Given a minimal repository that should be able to have upload tags be |
118 | @@ -19,11 +19,8 @@ def test_rich_history_preservation(tmpdir, repo_factory): |
119 | the result should be a correctly reconstructed upload tag. |
120 | |
121 | :param py.path tmpdir: the pytest standard tmpdir fixture. |
122 | - :param repo_factory: our standard repo_factory fixture. |
123 | + :param repo: our standard repo fixture. |
124 | """ |
125 | - source_repo = next(repo_factory) |
126 | - dest_repo = next(repo_factory) |
127 | - |
128 | Repo( |
129 | commits=[ |
130 | Commit( |
131 | @@ -51,8 +48,9 @@ def test_rich_history_preservation(tmpdir, repo_factory): |
132 | 'importer/import/1': Placeholder('1'), |
133 | 'importer/upload/2': Placeholder('3'), |
134 | } |
135 | - ).write(source_repo.raw_repo) |
136 | - target.export_all(source_repo, str(tmpdir)) |
137 | + ).write(repo.raw_repo) |
138 | + target.export_all(repo, str(tmpdir)) |
139 | + repo.delete_tags_in_namespace('importer') |
140 | Repo( |
141 | commits=[ |
142 | Commit( |
143 | @@ -64,9 +62,9 @@ def test_rich_history_preservation(tmpdir, repo_factory): |
144 | ) |
145 | ], |
146 | tags={'importer/import/1': Placeholder('1')}, |
147 | - ).write(dest_repo.raw_repo) |
148 | - target.import_single(dest_repo, tmpdir, '2') |
149 | - new_tag = dest_repo.raw_repo.lookup_reference( |
150 | + ).write(repo.raw_repo) |
151 | + target.import_single(repo, tmpdir, '2') |
152 | + new_tag = repo.raw_repo.lookup_reference( |
153 | 'refs/tags/importer/upload/2', |
154 | ).peel(pygit2.Tag) |
155 | assert new_tag.tagger.name == spec.SYNTHESIZED_COMMITTER_NAME |
156 | @@ -75,8 +73,8 @@ def test_rich_history_preservation(tmpdir, repo_factory): |
157 | assert new_commit.committer.name == spec.SYNTHESIZED_COMMITTER_NAME |
158 | assert new_commit.committer.email == spec.SYNTHESIZED_COMMITTER_EMAIL |
159 | new_tree = new_commit.peel(pygit2.Tree) |
160 | - assert dest_repo.raw_repo.get(new_tree['a'].id).data == b'abc' |
161 | - assert dest_repo.raw_repo.get(new_tree['b'].id).data == b'b' |
162 | + assert repo.raw_repo.get(new_tree['a'].id).data == b'abc' |
163 | + assert repo.raw_repo.get(new_tree['b'].id).data == b'b' |
164 | |
165 | |
166 | def test_rich_history_preservation_multiple_parents(tmpdir, repo): |
167 | @@ -89,7 +87,7 @@ def test_rich_history_preservation_multiple_parents(tmpdir, repo): |
168 | code. |
169 | |
170 | :param py.path tmpdir: the pytest standard tmpdir fixture. |
171 | - :param repo_factory: our standard repo_factory fixture. |
172 | + :param repo: our standard repo fixture. |
173 | """ |
174 | Repo( |
175 | commits=[ |
176 | @@ -147,3 +145,55 @@ def test_rich_history_import_no_base(tmpdir, repo): |
177 | tmpdir.join('2.base').write("1\n") |
178 | with pytest.raises(target.BaseNotFoundError): |
179 | target.import_single(repo, str(tmpdir), '2') |
180 | + |
181 | + |
182 | +def test_rich_history_preservation_patch_in_message(tmpdir, repo): |
183 | + """Commit messages with patch-like contents should round trip |
184 | + |
185 | + If a commit message in rich history contains text in a patch-like |
186 | + format (such as a diff of some file), it should not prevent correct |
187 | + round-tripping. "git format-patch" followed by "git am" fails this |
188 | + test, for example. |
189 | + |
190 | + :param py.path tmpdir: the pytest standard tmpdir fixture. |
191 | + :param GitUbuntuRepository repo: our standard repo fixture. |
192 | + """ |
193 | + Repo( |
194 | + commits=[ |
195 | + Commit( |
196 | + name='1', |
197 | + message='a', |
198 | + tree=Tree({ |
199 | + 'a': Blob(b'a'), |
200 | + }), |
201 | + ), |
202 | + Commit( |
203 | + name='2', |
204 | + message='''First line |
205 | + |
206 | +Inline patch that isn't part of the real patch starts here |
207 | + |
208 | +--- a/foo |
209 | ++++ b/foo |
210 | +@@ -1 +1,2 @@ |
211 | + foo |
212 | ++bar |
213 | +''', |
214 | + tree=Tree({ |
215 | + 'a': Blob(b'ab'), |
216 | + }), |
217 | + parents=[Placeholder('1')], |
218 | + ), |
219 | + ], |
220 | + tags={ |
221 | + 'importer/import/1': Placeholder('1'), |
222 | + 'importer/upload/2': Placeholder('2'), |
223 | + } |
224 | + ).write(repo.raw_repo) |
225 | + target.export_all(repo, str(tmpdir)) |
226 | + repo.raw_repo.lookup_reference('refs/tags/importer/upload/2').delete() |
227 | + |
228 | + # We already test in test_rich_history_preservation() that the import works |
229 | + # correctly; this test solely checks that the following call does not raise |
230 | + # an exception, so no further assertion is required. |
231 | + target.import_single(repo, tmpdir, '2') |
232 | diff --git a/gitubuntu/test_fixtures.py b/gitubuntu/test_fixtures.py |
233 | index bbeb8f6..60a5457 100644 |
234 | --- a/gitubuntu/test_fixtures.py |
235 | +++ b/gitubuntu/test_fixtures.py |
236 | @@ -35,26 +35,6 @@ def repo(): |
237 | |
238 | |
239 | @pytest.fixture |
240 | -def repo_factory(): |
241 | - """A fixture that generates a series of repo fixtures |
242 | - |
243 | - This can be used by tests that require multiple independent repo fixtures. |
244 | - """ |
245 | - |
246 | - # See https://stackoverflow.com/q/21583833/478206 |
247 | - with contextlib.ExitStack() as stack: |
248 | - class Factory: |
249 | - def __iter__(self): |
250 | - return self |
251 | - |
252 | - def __next__(self): |
253 | - fixture_generator = repo() |
254 | - stack.callback(fixture_generator.close) |
255 | - return next(fixture_generator) |
256 | - yield Factory() |
257 | - |
258 | - |
259 | -@pytest.fixture |
260 | def pygit2_repo(): |
261 | """An empty pygit2 repository fixture in a temporary directory""" |
262 | with tempfile.TemporaryDirectory() as tmp_dir: |
PASSED: Continuous integration, rev:e30452d7304 5c9a44dae172dae 0029d79bbd58e8 /jenkins. ubuntu. com/server/ job/git- ubuntu- ci/512/
https:/
Executed test runs:
SUCCESS: VM Setup
SUCCESS: Build
SUCCESS: Unit Tests
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/git- ubuntu- ci/512/ /rebuild
https:/