[ADD] runbot_merge: support for file creation in patch

Had been left out of the original impl, but turns out it might be
useful e.g. when a new lang gets added to the i18n export.

Fixes #1042
This commit is contained in:
Xavier Morel 2025-01-29 13:29:53 +01:00
parent 54337aeccc
commit 1097c5f19e
3 changed files with 101 additions and 18 deletions

View File

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

View File

@ -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)\

View File

@ -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 <bar@example.org>
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 <bar@example.org>
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 <bar@example.org>
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"