Merge ~racb/git-ubuntu:fix-rich-history-unescaped-patch into git-ubuntu:master

Proposed by Robie Basak
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)
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

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:e30452d73045c9a44dae172dae0029d79bbd58e8
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/512/
Executed test runs:
    SUCCESS: VM Setup
    SUCCESS: Build
    SUCCESS: Unit Tests
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/512//rebuild

review: Approve (continuous-integration)
Revision history for this message
Bryce Harrington (bryce) wrote :

8f84439e71276f94c56837872b97d3d9432bc33d
  - Test refactoring, to make it use a single repo rather than source
    and dest ones.
  - Drops the repo_fixture
  - LGTM
  √ Testsuite result: passed

1c52d81028861d18a43d09d18e926bbeb969fd2d
  - 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

c2555935fcb8fc6285d60bcf5daa0599d7f6b0ec
  - 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

e30452d73045c9a44dae172dae0029d79bbd58e8
  - 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.

review: Approve
Revision history for this message
Robie Basak (racb) wrote :

Thanks. Agreed with the docstring change. I have updated the patchset and pushed. Now awaiting CI result before landing.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:0376feae802c37ba6e71b7eca899230c32bc7904
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/513/
Executed test runs:
    SUCCESS: VM Setup
    SUCCESS: Build
    SUCCESS: Unit Tests
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/513//rebuild

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/doc/STYLE.md b/doc/STYLE.md
2index 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.
18diff --git a/gitubuntu/rich_history.py b/gitubuntu/rich_history.py
19index 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 [
99diff --git a/gitubuntu/rich_history_test.py b/gitubuntu/rich_history_test.py
100index 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')
232diff --git a/gitubuntu/test_fixtures.py b/gitubuntu/test_fixtures.py
233index 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:

Subscribers

People subscribed via source and target branches