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
diff --git a/doc/STYLE.md b/doc/STYLE.md
index 8a54d95..eb1e166 100644
--- a/doc/STYLE.md
+++ b/doc/STYLE.md
@@ -101,12 +101,3 @@ We use [pytest fixtures](https://docs.pytest.org/en/latest/fixture.html)
101to help us with this. Use of pytest's built-in `tmpdir` fixture is very101to help us with this. Use of pytest's built-in `tmpdir` fixture is very
102common, as is `repo` and `pygit2_repo` as defined in102common, as is `repo` and `pygit2_repo` as defined in
103`gitubuntu.test_fixtures`.103`gitubuntu.test_fixtures`.
104
105Sometimes a test needs multiple instances of these fixtures.
106`gitubuntu.test_fixtures.repo_factory` achieves this for the `repo`
107fixture, and
108`gitubuntu.rich_history_test::test_rich_history_preservation` is an
109example of its use. A similar pattern may be used if multiple instances
110of other fixtures become necessary. In time it may make sense to
111refactor this into a generic "make a factory out of this fixture"
112function.
diff --git a/gitubuntu/rich_history.py b/gitubuntu/rich_history.py
index ac178fb..c474f3f 100644
--- a/gitubuntu/rich_history.py
+++ b/gitubuntu/rich_history.py
@@ -63,11 +63,13 @@ def export_upload_tag(repo, path, upload_tag, commit_map):
63 """Export a single upload tag for later reconstruction63 """Export a single upload tag for later reconstruction
6464
65 If successful, two files will be created in the output directory: one with65 If successful, two files will be created in the output directory: one with
66 extension '.base', and one with extension '.mbox'. The base name of these66 extension '.base', and one with extension '.pick'. The base name of these
67 files is the version string from the upload tag. The '.base' file will67 files is the version string from the upload tag. The '.base' file will
68 contain a single LF-terminated string which is the version string from the68 contain a single LF-terminated string which is the version string from the
69 import tag on which the upload tag patchset is based. The '.mbox' file will69 import tag on which the upload tag patchset is based. The '.pick' file will
70 contain the patchset as exported using 'git format-patch --stdout'.70 contain the patchset described as a series of commit hash strings that can
71 be picked in order, one on each line, as described by 'git rev-list
72 --reverse'.
7173
72 Note that the upload and import tag names are already escaped according to74 Note that the upload and import tag names are already escaped according to
73 dep14, so the output filenames will have their version parts escaped75 dep14, so the output filenames will have their version parts escaped
@@ -81,7 +83,7 @@ def export_upload_tag(repo, path, upload_tag, commit_map):
81 :raises BaseNotFoundError: if a base import tag cannot be found.83 :raises BaseNotFoundError: if a base import tag cannot be found.
82 :raises MultipleParentError: if multiple parents for a commit are found.84 :raises MultipleParentError: if multiple parents for a commit are found.
83 :raises subprocess.CalledProcessError: if the underlying call to "git85 :raises subprocess.CalledProcessError: if the underlying call to "git
84 format-patch" fails.86 rev-list" fails.
85 :returns: None.87 :returns: None.
86 """88 """
87 import_tag = get_upload_tag_base(repo, upload_tag, commit_map)89 import_tag = get_upload_tag_base(repo, upload_tag, commit_map)
@@ -96,12 +98,12 @@ def export_upload_tag(repo, path, upload_tag, commit_map):
96 initial_env=repo._initial_env,98 initial_env=repo._initial_env,
97 update_env=repo.env,99 update_env=repo.env,
98 )100 )
99 with open(os.path.join(path, export_name) + '.mbox', 'w') as f:101 with open(os.path.join(path, export_name) + '.pick', 'w') as f:
100 subprocess.check_call(102 subprocess.check_call(
101 [103 [
102 'git',104 'git',
103 'format-patch',105 'rev-list',
104 '--stdout',106 '--reverse',
105 '%s..%s' % (import_tag.name, upload_tag.name),107 '%s..%s' % (import_tag.name, upload_tag.name),
106 ],108 ],
107 env=env,109 env=env,
@@ -155,8 +157,8 @@ def import_single(repo, path, version, namespace='importer'):
155 :raises FileNotFoundError: if rich history cannot be found for this157 :raises FileNotFoundError: if rich history cannot be found for this
156 version.158 version.
157 :raises BaseNotFoundError: if a base import tag cannot be found.159 :raises BaseNotFoundError: if a base import tag cannot be found.
158 :raises subprocess.CalledProcessError: if the underlying call to "git160 :raises subprocess.CalledProcessError: if any of the underlying calls to
159 am" or "git tag" fail.161 "git cherry-pick" or "git tag" fail.
160 :returns: None.162 :returns: None.
161 """163 """
162 version_name = gitubuntu.git_repository.git_dep14_tag(version)164 version_name = gitubuntu.git_repository.git_dep14_tag(version)
@@ -170,17 +172,17 @@ def import_single(repo, path, version, namespace='importer'):
170 except KeyError as e:172 except KeyError as e:
171 raise BaseNotFoundError() from e173 raise BaseNotFoundError() from e
172174
173 with open(os.path.join(path, version_name) + '.mbox', 'r') as f:175 with open(os.path.join(path, version_name) + '.pick', 'r') as pick:
174 with repo.temporary_worktree(import_tag_name, 'rich-history-import'):176 with repo.temporary_worktree(import_tag_name, 'rich-history-import'):
175 env = {177 env = {
176 'GIT_COMMITTER_NAME': spec.SYNTHESIZED_COMMITTER_NAME,178 'GIT_COMMITTER_NAME': spec.SYNTHESIZED_COMMITTER_NAME,
177 'GIT_COMMITTER_EMAIL': spec.SYNTHESIZED_COMMITTER_EMAIL,179 'GIT_COMMITTER_EMAIL': spec.SYNTHESIZED_COMMITTER_EMAIL,
178 }180 }
179 subprocess.check_call(181 for commit_string in pick:
180 ['git', 'am'],182 subprocess.check_call(
181 stdin=f,183 ['git', 'cherry-pick', commit_string.strip()],
182 env=env,184 env=env,
183 )185 )
184 upload_tag_name = '%s/upload/%s' % (namespace, version_name)186 upload_tag_name = '%s/upload/%s' % (namespace, version_name)
185 subprocess.check_call(187 subprocess.check_call(
186 [188 [
diff --git a/gitubuntu/rich_history_test.py b/gitubuntu/rich_history_test.py
index 5223f2d..1716546 100644
--- a/gitubuntu/rich_history_test.py
+++ b/gitubuntu/rich_history_test.py
@@ -5,12 +5,12 @@ import pytest
55
6from gitubuntu.repo_builder import Blob, Commit, Placeholder, Repo, Tree6from gitubuntu.repo_builder import Blob, Commit, Placeholder, Repo, Tree
7import gitubuntu.spec as spec7import gitubuntu.spec as spec
8from gitubuntu.test_fixtures import repo, repo_factory8from gitubuntu.test_fixtures import repo
99
10import gitubuntu.rich_history as target10import gitubuntu.rich_history as target
1111
1212
13def test_rich_history_preservation(tmpdir, repo_factory):13def test_rich_history_preservation(tmpdir, repo):
14 """An export followed by an import should preserve rich history14 """An export followed by an import should preserve rich history
1515
16 Given a minimal repository that should be able to have upload tags be16 Given a minimal repository that should be able to have upload tags be
@@ -19,11 +19,8 @@ def test_rich_history_preservation(tmpdir, repo_factory):
19 the result should be a correctly reconstructed upload tag.19 the result should be a correctly reconstructed upload tag.
2020
21 :param py.path tmpdir: the pytest standard tmpdir fixture.21 :param py.path tmpdir: the pytest standard tmpdir fixture.
22 :param repo_factory: our standard repo_factory fixture.22 :param repo: our standard repo fixture.
23 """23 """
24 source_repo = next(repo_factory)
25 dest_repo = next(repo_factory)
26
27 Repo(24 Repo(
28 commits=[25 commits=[
29 Commit(26 Commit(
@@ -51,8 +48,9 @@ def test_rich_history_preservation(tmpdir, repo_factory):
51 'importer/import/1': Placeholder('1'),48 'importer/import/1': Placeholder('1'),
52 'importer/upload/2': Placeholder('3'),49 'importer/upload/2': Placeholder('3'),
53 }50 }
54 ).write(source_repo.raw_repo)51 ).write(repo.raw_repo)
55 target.export_all(source_repo, str(tmpdir))52 target.export_all(repo, str(tmpdir))
53 repo.delete_tags_in_namespace('importer')
56 Repo(54 Repo(
57 commits=[55 commits=[
58 Commit(56 Commit(
@@ -64,9 +62,9 @@ def test_rich_history_preservation(tmpdir, repo_factory):
64 )62 )
65 ],63 ],
66 tags={'importer/import/1': Placeholder('1')},64 tags={'importer/import/1': Placeholder('1')},
67 ).write(dest_repo.raw_repo)65 ).write(repo.raw_repo)
68 target.import_single(dest_repo, tmpdir, '2')66 target.import_single(repo, tmpdir, '2')
69 new_tag = dest_repo.raw_repo.lookup_reference(67 new_tag = repo.raw_repo.lookup_reference(
70 'refs/tags/importer/upload/2',68 'refs/tags/importer/upload/2',
71 ).peel(pygit2.Tag)69 ).peel(pygit2.Tag)
72 assert new_tag.tagger.name == spec.SYNTHESIZED_COMMITTER_NAME70 assert new_tag.tagger.name == spec.SYNTHESIZED_COMMITTER_NAME
@@ -75,8 +73,8 @@ def test_rich_history_preservation(tmpdir, repo_factory):
75 assert new_commit.committer.name == spec.SYNTHESIZED_COMMITTER_NAME73 assert new_commit.committer.name == spec.SYNTHESIZED_COMMITTER_NAME
76 assert new_commit.committer.email == spec.SYNTHESIZED_COMMITTER_EMAIL74 assert new_commit.committer.email == spec.SYNTHESIZED_COMMITTER_EMAIL
77 new_tree = new_commit.peel(pygit2.Tree)75 new_tree = new_commit.peel(pygit2.Tree)
78 assert dest_repo.raw_repo.get(new_tree['a'].id).data == b'abc'76 assert repo.raw_repo.get(new_tree['a'].id).data == b'abc'
79 assert dest_repo.raw_repo.get(new_tree['b'].id).data == b'b'77 assert repo.raw_repo.get(new_tree['b'].id).data == b'b'
8078
8179
82def test_rich_history_preservation_multiple_parents(tmpdir, repo):80def test_rich_history_preservation_multiple_parents(tmpdir, repo):
@@ -89,7 +87,7 @@ def test_rich_history_preservation_multiple_parents(tmpdir, repo):
89 code.87 code.
9088
91 :param py.path tmpdir: the pytest standard tmpdir fixture.89 :param py.path tmpdir: the pytest standard tmpdir fixture.
92 :param repo_factory: our standard repo_factory fixture.90 :param repo: our standard repo fixture.
93 """91 """
94 Repo(92 Repo(
95 commits=[93 commits=[
@@ -147,3 +145,55 @@ def test_rich_history_import_no_base(tmpdir, repo):
147 tmpdir.join('2.base').write("1\n")145 tmpdir.join('2.base').write("1\n")
148 with pytest.raises(target.BaseNotFoundError):146 with pytest.raises(target.BaseNotFoundError):
149 target.import_single(repo, str(tmpdir), '2')147 target.import_single(repo, str(tmpdir), '2')
148
149
150def test_rich_history_preservation_patch_in_message(tmpdir, repo):
151 """Commit messages with patch-like contents should round trip
152
153 If a commit message in rich history contains text in a patch-like
154 format (such as a diff of some file), it should not prevent correct
155 round-tripping. "git format-patch" followed by "git am" fails this
156 test, for example.
157
158 :param py.path tmpdir: the pytest standard tmpdir fixture.
159 :param GitUbuntuRepository repo: our standard repo fixture.
160 """
161 Repo(
162 commits=[
163 Commit(
164 name='1',
165 message='a',
166 tree=Tree({
167 'a': Blob(b'a'),
168 }),
169 ),
170 Commit(
171 name='2',
172 message='''First line
173
174Inline patch that isn't part of the real patch starts here
175
176--- a/foo
177+++ b/foo
178@@ -1 +1,2 @@
179 foo
180+bar
181''',
182 tree=Tree({
183 'a': Blob(b'ab'),
184 }),
185 parents=[Placeholder('1')],
186 ),
187 ],
188 tags={
189 'importer/import/1': Placeholder('1'),
190 'importer/upload/2': Placeholder('2'),
191 }
192 ).write(repo.raw_repo)
193 target.export_all(repo, str(tmpdir))
194 repo.raw_repo.lookup_reference('refs/tags/importer/upload/2').delete()
195
196 # We already test in test_rich_history_preservation() that the import works
197 # correctly; this test solely checks that the following call does not raise
198 # an exception, so no further assertion is required.
199 target.import_single(repo, tmpdir, '2')
diff --git a/gitubuntu/test_fixtures.py b/gitubuntu/test_fixtures.py
index bbeb8f6..60a5457 100644
--- a/gitubuntu/test_fixtures.py
+++ b/gitubuntu/test_fixtures.py
@@ -35,26 +35,6 @@ def repo():
3535
3636
37@pytest.fixture37@pytest.fixture
38def repo_factory():
39 """A fixture that generates a series of repo fixtures
40
41 This can be used by tests that require multiple independent repo fixtures.
42 """
43
44 # See https://stackoverflow.com/q/21583833/478206
45 with contextlib.ExitStack() as stack:
46 class Factory:
47 def __iter__(self):
48 return self
49
50 def __next__(self):
51 fixture_generator = repo()
52 stack.callback(fixture_generator.close)
53 return next(fixture_generator)
54 yield Factory()
55
56
57@pytest.fixture
58def pygit2_repo():38def pygit2_repo():
59 """An empty pygit2 repository fixture in a temporary directory"""39 """An empty pygit2 repository fixture in a temporary directory"""
60 with tempfile.TemporaryDirectory() as tmp_dir:40 with tempfile.TemporaryDirectory() as tmp_dir:

Subscribers

People subscribed via source and target branches