mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +07:00
[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:
parent
54337aeccc
commit
1097c5f19e
@ -263,12 +263,17 @@ class Repo:
|
|||||||
f, _, local = f.rpartition("/")
|
f, _, local = f.rpartition("/")
|
||||||
# tree to update, `{tree}:` works as an alias for tree
|
# tree to update, `{tree}:` works as an alias for tree
|
||||||
lstree = repo.ls_tree(f"{tree}:{f}").stdout.splitlines(keepends=False)
|
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
|
# tab before name is critical to the format
|
||||||
f"{mode} {typ} {oid if name == local else sha}\t{name}\n"
|
new_tree.append(f"{mode} {typ} {sha}\t{name}\n")
|
||||||
for mode, typ, sha, name in map(methodcaller("split", None, 3), lstree)
|
if not seen:
|
||||||
)
|
new_tree.append(f"100644 blob {oid}\t{local}\n")
|
||||||
oid = repo.with_config(input=new_tree, check=True).mktree().stdout.strip()
|
oid = repo.with_config(input="".join(new_tree), check=True).mktree().stdout.strip()
|
||||||
tree = oid
|
tree = oid
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ overhead, or FBI backdoors oh wait forget about that last one.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
@ -179,7 +180,7 @@ class Patch(models.Model):
|
|||||||
p.committer = f"{name} <{email}>"
|
p.committer = f"{name} <{email}>"
|
||||||
p.commitdate = date
|
p.commitdate = date
|
||||||
p.file_ids = File.concat(*(
|
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)
|
for m in FILE_PATTERN.finditer(p.patch)
|
||||||
))
|
))
|
||||||
p.message = r.message
|
p.message = r.message
|
||||||
@ -233,7 +234,7 @@ class Patch(models.Model):
|
|||||||
has_files = False
|
has_files = False
|
||||||
for m in FILE_PATTERN.finditer(patch.patch):
|
for m in FILE_PATTERN.finditer(patch.patch):
|
||||||
has_files = True
|
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.")
|
raise ValidationError("Only patches updating a file in place are supported, not creation, removal, or renaming.")
|
||||||
if not has_files:
|
if not has_files:
|
||||||
raise ValidationError("Patches should have files they patch, found none.")
|
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:
|
def _apply_patch(self, r: git.Repo) -> str:
|
||||||
p = self._parse_patch()
|
p = self._parse_patch()
|
||||||
files = {}
|
|
||||||
def reader(_r, f):
|
def reader(_r, f):
|
||||||
return pathlib.Path(tmpdir, f).read_text(encoding="utf-8")
|
return pathlib.Path(tmpdir, f).read_text(encoding="utf-8")
|
||||||
|
|
||||||
prefix = 0
|
prefix = 0
|
||||||
|
read = set()
|
||||||
|
patched = {}
|
||||||
for m in FILE_PATTERN.finditer(p.patch):
|
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
|
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)
|
archiver = r.stdout(True)
|
||||||
# if the parent is checked then we can't get rid of the kwarg and popen doesn't support it
|
# 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._config.pop('check', None)
|
||||||
archiver.runner = subprocess.Popen
|
archiver.runner = subprocess.Popen
|
||||||
with archiver.archive(self.target.name, *files) as out, \
|
with contextlib.ExitStack() as stack,\
|
||||||
tarfile.open(fileobj=out.stdout, mode='r|') as tf,\
|
|
||||||
tempfile.TemporaryDirectory() as tmpdir:
|
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 = subprocess.run(
|
||||||
['patch', f'-p{prefix}', '--directory', tmpdir, '--verbose'],
|
['patch', f'-p{prefix}', '--directory', tmpdir, '--verbose'],
|
||||||
input=p.patch,
|
input=p.patch,
|
||||||
@ -349,7 +357,7 @@ class Patch(models.Model):
|
|||||||
)
|
)
|
||||||
if patch.returncode:
|
if patch.returncode:
|
||||||
raise PatchFailure("\n---------\n".join(filter(None, [p.patch, patch.stdout.strip(), patch.stderr.strip()])))
|
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')\
|
sha = r.stdout().with_config(encoding='utf-8')\
|
||||||
.show('--no-patch', '--pretty=%H', self.target.name)\
|
.show('--no-patch', '--pretty=%H', self.target.name)\
|
||||||
|
@ -15,7 +15,7 @@ Date: 2021-04-24T17:09:14Z
|
|||||||
whop whop
|
whop whop
|
||||||
|
|
||||||
diff --git a/b b/b
|
diff --git a/b b/b
|
||||||
index 000000000000..000000000000 100644
|
index d00491fd7e5b..0cfbf08886fc 100644
|
||||||
--- a/b
|
--- a/b
|
||||||
+++ b/b
|
+++ b/b
|
||||||
@@ -1,1 +1,1 @@
|
@@ -1,1 +1,1 @@
|
||||||
@ -35,7 +35,7 @@ whop whop
|
|||||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
diff --git a/b b/b
|
diff --git a/b b/b
|
||||||
index 000000000000..000000000000 100644
|
index d00491fd7e5b..0cfbf08886fc 100644
|
||||||
--- a/b
|
--- a/b
|
||||||
+++ b/b
|
+++ b/b
|
||||||
@@ -1,1 +1,1 @@
|
@@ -1,1 +1,1 @@
|
||||||
@ -47,7 +47,7 @@ index 000000000000..000000000000 100644
|
|||||||
|
|
||||||
# slightly different format than the one I got, possibly because older?
|
# slightly different format than the one I got, possibly because older?
|
||||||
FORMAT_PATCH_MAT = """\
|
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>
|
From: 3 Discos Down <bar@example.org>
|
||||||
Date: Sat, 24 Apr 2021 17:09:14 +0000
|
Date: Sat, 24 Apr 2021 17:09:14 +0000
|
||||||
Subject: [PATCH 1/1] [I18N] whop
|
Subject: [PATCH 1/1] [I18N] whop
|
||||||
@ -58,7 +58,7 @@ whop whop
|
|||||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
diff --git b b
|
diff --git b b
|
||||||
index 000000000000..000000000000 100644
|
index d00491fd7e5b..0cfbf08886fc 100644
|
||||||
--- b
|
--- b
|
||||||
+++ b
|
+++ b
|
||||||
@@ -1,1 +1,1 @@
|
@@ -1,1 +1,1 @@
|
||||||
@ -268,3 +268,73 @@ def test_patch_conflict(env, project, repo, users):
|
|||||||
), (
|
), (
|
||||||
False, '', [('active', 1, 0)]
|
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"
|
||||||
|
Loading…
Reference in New Issue
Block a user