[FIX] runbot_merge: format_patch if --no-prefix

Turns out you can configure format-patch with `--no-prefix` and some
people (*cough cough* mat) have that in their standard setup, so the
assumption of needing to strip 1 level of prefix does not necessarily
hold.

Also fix a few more issues:

- some people (*cough cough* still mat) also use `-n` by default,
  which adds the series sequence (`n/m`) even for a single patch,
  handle that correctly
- logging patch application errors is pretty useful when patching
  fails and I'm trying to get the information via logs, do that
- especially when I decide to add error messages to tracking *but
  forgot to show the chatter by default*, fix that as well

The commit-based patcher worked first try, and patch-based would have
worked too if not for those meddling kids. In the future it might be a
good idea to reify the stripping level (`-p`) on the patch object
though, and maybe provide a computed preview of the list of files to
patch, so issues are easier for the operator to diagnose.
This commit is contained in:
Xavier Morel 2024-11-18 12:37:44 +01:00
parent a12e593fba
commit 5441ba12ae
3 changed files with 85 additions and 32 deletions

View File

@ -28,8 +28,8 @@ _logger = logging.getLogger(__name__)
FILE_PATTERN = re.compile(r""" FILE_PATTERN = re.compile(r"""
# paths with spaces don't work well as the path can be followed by a timestamp # paths with spaces don't work well as the path can be followed by a timestamp
# (in an unspecified format?) # (in an unspecified format?)
---\x20a/(?P<file_from>\S+)(:?\s.*)?\n ---\x20(?P<prefix_a>a/)?(?P<file_from>\S+)(:?\s.*)?\n
\+\+\+\x20b/(?P<file_to>\S+)(:?\s.*)?\n \+\+\+\x20(?P<prefix_b>b/)?(?P<file_to>\S+)(:?\s.*)?\n
@@\x20-(\d+(,\d+)?)\x20\+(\d+(,\d+)?)\x20@@ # trailing garbage @@\x20-(\d+(,\d+)?)\x20\+(\d+(,\d+)?)\x20@@ # trailing garbage
""", re.VERBOSE) """, re.VERBOSE)
@ -98,7 +98,7 @@ def parse_format_patch(p: Patch) -> ParseResult:
name, email = parseaddr(m['from']) name, email = parseaddr(m['from'])
author = (name, email, m['date']) author = (name, email, m['date'])
msg = m['subject'].removeprefix('[PATCH] ') msg = re.sub(r'^\[PATCH( \d+/\d+)?\] ', '', m['subject'])
body, _, rest = m.get_content().partition('---\n') body, _, rest = m.get_content().partition('---\n')
if body: if body:
msg += '\n\n' + body msg += '\n\n' + body
@ -225,12 +225,16 @@ class Patch(models.Model):
patch.repository.name, patch.repository.name,
) )
try: try:
c = patch._apply_commit(r) if patch.commit else patch._apply_patch(r) if patch.commit:
c = patch._apply_commit(r)
else:
c = patch._apply_patch(r)
except Exception as e: except Exception as e:
if isinstance(e, PatchFailure): if isinstance(e, PatchFailure):
subject = "Unable to apply patch" subject = "Unable to apply patch"
else: else:
subject = "Unknown error while trying to apply patch" subject = "Unknown error while trying to apply patch"
_logger.error("%s:\n%s", subject, str(e))
patch.message_post(body=plaintext2html(e), subject=subject) patch.message_post(body=plaintext2html(e), subject=subject)
continue continue
# `.` is the local "remote", so this updates target to c # `.` is the local "remote", so this updates target to c
@ -276,10 +280,17 @@ 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 = dict.fromkeys( files = {}
(m['file_to'] for m in FILE_PATTERN.finditer(p.patch)), def reader(_r, f):
lambda _r, f: pathlib.Path(tmpdir, f).read_text(encoding="utf-8"), return pathlib.Path(tmpdir, f).read_text(encoding="utf-8")
)
prefix = 0
for m in FILE_PATTERN.finditer(p.patch):
if not prefix and m['prefix_a'] and m['prefix_b']:
prefix = 1
files[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)
@ -289,7 +300,7 @@ class Patch(models.Model):
tempfile.TemporaryDirectory() as tmpdir: tempfile.TemporaryDirectory() as tmpdir:
tf.extractall(tmpdir) tf.extractall(tmpdir)
patch = subprocess.run( patch = subprocess.run(
['patch', '-p1', '-d', tmpdir], ['patch', f'-p{prefix}', '-d', tmpdir],
input=p.patch, input=p.patch,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,

View File

@ -23,6 +23,51 @@ index 000000000000..000000000000 100644
+2 +2
""" """
FORMAT_PATCH_XMO = """\
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
---
b | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/b b/b
index 000000000000..000000000000 100644
--- a/b
+++ b/b
@@ -1,1 +1,1 @@
-1
+2
--
2.46.2
"""
# slightly different format than the one I got, possibly because older?
FORMAT_PATCH_MAT = """\
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 1/1] [I18N] whop
whop whop
---
b | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git b b
index 000000000000..000000000000 100644
--- b
+++ b
@@ -1,1 +1,1 @@
-1
+2
--
2.34.1
"""
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _setup(repo): def _setup(repo):
@ -148,41 +193,34 @@ def test_apply_udiff(env, project, repo, users):
assert not p.active assert not p.active
def test_apply_format_patch(env, project, repo, users): @pytest.mark.parametrize('patch', [
pytest.param(FORMAT_PATCH_XMO, id='xmo'),
pytest.param(FORMAT_PATCH_MAT, id='mat'),
])
def test_apply_format_patch(env, project, repo, users, patch):
p = env['runbot_merge.patch'].create({ p = env['runbot_merge.patch'].create({
'target': project.branch_ids.id, 'target': project.branch_ids.id,
'repository': project.repo_ids.id, 'repository': project.repo_ids.id,
'patch': """\ 'patch': 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] whop
whop whop
---
b | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/b b/b
index 000000000000..000000000000 100644
--- a/b
+++ b/b
@@ -1,1 +1,1 @@
-1
+2
--
2.46.2
""",
}) })
env.run_crons() env.run_crons()
bot = env['res.users'].browse((1,))
assert p.message_ids[::-1].mapped(lambda m: (
m.author_id.display_name,
m.body,
list(map(read_tracking_value, m.tracking_value_ids)),
)) == [
(p.create_uid.partner_id.display_name, '<p>Unstaged direct-application patch created</p>', []),
(bot.partner_id.display_name, "", [('active', 1, 0)]),
]
HEAD = repo.commit('master') HEAD = repo.commit('master')
assert repo.read_tree(HEAD) == { assert repo.read_tree(HEAD) == {
'a': '2', 'a': '2',
'b': '2\n', 'b': '2\n',
} }
assert HEAD.message == "whop\n\nwhop whop" assert HEAD.message == "[I18N] whop\n\nwhop whop"
assert HEAD.author['name'] == "3 Discos Down" assert HEAD.author['name'] == "3 Discos Down"
assert HEAD.author['email'] == "bar@example.org" assert HEAD.author['email'] == "bar@example.org"
assert not p.active assert not p.active

View File

@ -143,6 +143,10 @@
</group> </group>
</group> </group>
</sheet> </sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form> </form>
</field> </field>
</record> </record>