diff --git a/runbot_merge/git.py b/runbot_merge/git.py index 8f2732a8..36c5a1ab 100644 --- a/runbot_merge/git.py +++ b/runbot_merge/git.py @@ -263,12 +263,17 @@ class Repo: f, _, local = f.rpartition("/") # tree to update, `{tree}:` works as an alias for tree lstree = repo.ls_tree(f"{tree}:{f}").stdout.splitlines(keepends=False) - new_tree = "".join( + new_tree = [] + seen = False + for mode, typ, sha, name in map(methodcaller("split", None, 3), lstree): + if name == local: + sha = oid + seen = True # tab before name is critical to the format - f"{mode} {typ} {oid if name == local else sha}\t{name}\n" - for mode, typ, sha, name in map(methodcaller("split", None, 3), lstree) - ) - oid = repo.with_config(input=new_tree, check=True).mktree().stdout.strip() + new_tree.append(f"{mode} {typ} {sha}\t{name}\n") + if not seen: + new_tree.append(f"100644 blob {oid}\t{local}\n") + oid = repo.with_config(input="".join(new_tree), check=True).mktree().stdout.strip() tree = oid return tree diff --git a/runbot_merge/models/patcher.py b/runbot_merge/models/patcher.py index 83e47d93..ef11588b 100644 --- a/runbot_merge/models/patcher.py +++ b/runbot_merge/models/patcher.py @@ -6,6 +6,7 @@ overhead, or FBI backdoors oh wait forget about that last one. """ from __future__ import annotations +import contextlib import logging import pathlib import re @@ -179,7 +180,7 @@ class Patch(models.Model): p.committer = f"{name} <{email}>" p.commitdate = date p.file_ids = File.concat(*( - File.new({'name': m['file_from']}) + File.new({'name': m['file_to']}) for m in FILE_PATTERN.finditer(p.patch) )) p.message = r.message @@ -233,7 +234,7 @@ class Patch(models.Model): has_files = False for m in FILE_PATTERN.finditer(patch.patch): has_files = True - if m['file_from'] != m['file_to']: + if m['file_from'] != m['file_to'] and m['file_from'] != '/dev/null': raise ValidationError("Only patches updating a file in place are supported, not creation, removal, or renaming.") if not has_files: raise ValidationError("Patches should have files they patch, found none.") @@ -321,25 +322,32 @@ class Patch(models.Model): def _apply_patch(self, r: git.Repo) -> str: p = self._parse_patch() - files = {} def reader(_r, f): return pathlib.Path(tmpdir, f).read_text(encoding="utf-8") prefix = 0 + read = set() + patched = {} for m in FILE_PATTERN.finditer(p.patch): - if not prefix and m['prefix_a'] and m['prefix_b']: + if not prefix and (m['prefix_a'] or m['file_from'] == '/dev/null') and m['prefix_b']: prefix = 1 - files[m['file_to']] = reader + if m['file_from'] != '/dev/null': + read.add(m['file_from']) + patched[m['file_to']] = reader archiver = r.stdout(True) # if the parent is checked then we can't get rid of the kwarg and popen doesn't support it archiver._config.pop('check', None) archiver.runner = subprocess.Popen - with archiver.archive(self.target.name, *files) as out, \ - tarfile.open(fileobj=out.stdout, mode='r|') as tf,\ + with contextlib.ExitStack() as stack,\ tempfile.TemporaryDirectory() as tmpdir: - tf.extractall(tmpdir) + # if there's no file to *update*, `archive` will extract the entire + # tree which is unnecessary + if read: + out = stack.enter_context(archiver.archive(self.target.name, *read)) + tf = stack.enter_context(tarfile.open(fileobj=out.stdout, mode='r|')) + tf.extractall(tmpdir) patch = subprocess.run( ['patch', f'-p{prefix}', '--directory', tmpdir, '--verbose'], input=p.patch, @@ -349,7 +357,7 @@ class Patch(models.Model): ) if patch.returncode: raise PatchFailure("\n---------\n".join(filter(None, [p.patch, patch.stdout.strip(), patch.stderr.strip()]))) - new_tree = r.update_tree(self.target.name, files) + new_tree = r.update_tree(self.target.name, patched) sha = r.stdout().with_config(encoding='utf-8')\ .show('--no-patch', '--pretty=%H', self.target.name)\ diff --git a/runbot_merge/tests/test_patching.py b/runbot_merge/tests/test_patching.py index 46dc516f..9b57041c 100644 --- a/runbot_merge/tests/test_patching.py +++ b/runbot_merge/tests/test_patching.py @@ -15,7 +15,7 @@ Date: 2021-04-24T17:09:14Z whop whop diff --git a/b b/b -index 000000000000..000000000000 100644 +index d00491fd7e5b..0cfbf08886fc 100644 --- a/b +++ b/b @@ -1,1 +1,1 @@ @@ -35,7 +35,7 @@ whop whop 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b b/b -index 000000000000..000000000000 100644 +index d00491fd7e5b..0cfbf08886fc 100644 --- a/b +++ b/b @@ -1,1 +1,1 @@ @@ -47,7 +47,7 @@ index 000000000000..000000000000 100644 # slightly different format than the one I got, possibly because older? FORMAT_PATCH_MAT = """\ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From 3000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: 3 Discos Down Date: Sat, 24 Apr 2021 17:09:14 +0000 Subject: [PATCH 1/1] [I18N] whop @@ -58,7 +58,7 @@ whop whop 1 file changed, 1 insertion(+), 1 deletion(-) diff --git b b -index 000000000000..000000000000 100644 +index d00491fd7e5b..0cfbf08886fc 100644 --- b +++ b @@ -1,1 +1,1 @@ @@ -268,3 +268,73 @@ def test_patch_conflict(env, project, repo, users): ), ( False, '', [('active', 1, 0)] )] + +CREATE_FILE_FORMAT_PATCH = """\ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: 3 Discos Down +Date: Sat, 24 Apr 2021 17:09:14 +0000 +Subject: [PATCH] [I18N] whop + +whop whop +--- + x | 1 + + 1 file changed, 1 insertion(+) + create mode 100644 b + +diff --git a/x b/x +new file mode 100644 +index 000000000000..d00491fd7e5b +--- /dev/null ++++ b/x +@@ -0,0 +1 @@ ++1 +-- +2.48.1 +""" + +CREATE_FILE_SHOW = """\ +commit 0000000000000000000000000000000000000000 +Author: 3 Discos Down +Date: 2021-04-24T17:09:14Z + + [I18N] whop + + whop whop + +diff --git a/x b/x +new file mode 100644 +index 000000000000..d00491fd7e5b +--- /dev/null ++++ b/x +@@ -0,0 +1 @@ ++1 +""" + +@pytest.mark.parametrize('patch', [ + pytest.param(CREATE_FILE_SHOW, id='show'), + pytest.param(CREATE_FILE_FORMAT_PATCH, id='format-patch'), +]) +def test_apply_creation(env, project, repo, users, patch): + assert repo.read_tree(repo.commit('master')) == { + 'a': '2', + 'b': '1\n', + } + + env['runbot_merge.patch'].create({ + 'target': project.branch_ids.id, + 'repository': project.repo_ids.id, + 'patch': patch, + }) + # trying to check the list of files doesn't work, even using web_read + + env.run_crons() + + HEAD = repo.commit('master') + assert repo.read_tree(HEAD) == { + 'a': '2', + 'b': '1\n', + 'x': '1\n', + } + assert HEAD.message == "[I18N] whop\n\nwhop whop" + assert HEAD.author['name'] == "3 Discos Down" + assert HEAD.author['email'] == "bar@example.org"