diff options
author | Peter Kjellerstedt <peter.kjellerstedt@axis.com> | 2024-02-19 02:28:32 +0100 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2024-02-19 16:03:22 +0000 |
commit | 4cfd0f7e4e2db19344677999572e5b71ae97dfc4 (patch) | |
tree | 07f0d397468ec6eeeddb4a2bba1e6f8a6320f915 /meta/lib | |
parent | dc2e09417d172ec3da01786e42ebe5e0734ee9ae (diff) | |
download | poky-4cfd0f7e4e2db19344677999572e5b71ae97dfc4.tar.gz |
lib/oe/patch: Use git notes to store the filenames for the patches
The old way of keeping track of the filenames for the patches that
correspond to the commits was to add a special comment line to the end
of the commit message, e.g., "%% original patch: <filename>", using a
temporary git hook. This method had some drawbacks, e.g.:
* It caused problems if one wanted to push the commits upstream as the
comment line had to be manually removed.
* The comment line would end up in patches if someone used git
format-path rather than devtool finish to generate the patches.
* The comment line could interfere with global Git hooks used to
validate the format of the Git commit message.
* When regenerating patches with `devtool finish --force-patch-refresh`,
the process typically resulted in adding empty lines to the end of the
commit messages in the updated patches.
A better way of keeping track of the patch filenames is to use Git
notes. This way the commit messages remain unaffected, but the
information is still shown when, e.g., doing `git log`. A special Git
notes space, refs/notes/devtool, is used to not intefere with the
default Git notes. It is configured to be shown in, e.g., `git log` and
to survive rewrites (i.e., `git commit --amend` and `git rebase`).
Since there is no longer any need for a temporary Git hook, the code
that manipulated the .git/hooks directory has also been removed. To
avoid potential problems due to global Git hooks, --no-verify was added
to the `git commit` command.
To not cause troubles for those who have done `devtool modify` for a
recipe with the old solution and then do `devtool finish` with the new
solution, the code will fall back to look for the old strings in the
commit message if no Git note can be found.
While not technically motivated like above, the way to keep track of
ignored commits is also changed to use Git notes to avoid having
different methods to store similar information.
(From OE-Core rev: f5e6183b9557477bef74024a587de0bfcc2b7c0d)
Signed-off-by: Peter Kjellerstedt <peter.kjellerstedt@axis.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/lib')
-rw-r--r-- | meta/lib/oe/patch.py | 109 | ||||
-rw-r--r-- | meta/lib/oeqa/selftest/cases/devtool.py | 9 |
2 files changed, 75 insertions, 43 deletions
diff --git a/meta/lib/oe/patch.py b/meta/lib/oe/patch.py index 3ded5f3601..60a0cc8291 100644 --- a/meta/lib/oe/patch.py +++ b/meta/lib/oe/patch.py | |||
@@ -294,8 +294,9 @@ class PatchTree(PatchSet): | |||
294 | self.Pop(all=True) | 294 | self.Pop(all=True) |
295 | 295 | ||
296 | class GitApplyTree(PatchTree): | 296 | class GitApplyTree(PatchTree): |
297 | patch_line_prefix = '%% original patch' | 297 | notes_ref = "refs/notes/devtool" |
298 | ignore_commit_prefix = '%% ignore' | 298 | original_patch = 'original patch' |
299 | ignore_commit = 'ignore' | ||
299 | 300 | ||
300 | def __init__(self, dir, d): | 301 | def __init__(self, dir, d): |
301 | PatchTree.__init__(self, dir, d) | 302 | PatchTree.__init__(self, dir, d) |
@@ -452,7 +453,7 @@ class GitApplyTree(PatchTree): | |||
452 | # Prepare git command | 453 | # Prepare git command |
453 | cmd = ["git"] | 454 | cmd = ["git"] |
454 | GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail) | 455 | GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail) |
455 | cmd += ["commit", "-F", tmpfile] | 456 | cmd += ["commit", "-F", tmpfile, "--no-verify"] |
456 | # git doesn't like plain email addresses as authors | 457 | # git doesn't like plain email addresses as authors |
457 | if author and '<' in author: | 458 | if author and '<' in author: |
458 | cmd.append('--author="%s"' % author) | 459 | cmd.append('--author="%s"' % author) |
@@ -461,14 +462,52 @@ class GitApplyTree(PatchTree): | |||
461 | return (tmpfile, cmd) | 462 | return (tmpfile, cmd) |
462 | 463 | ||
463 | @staticmethod | 464 | @staticmethod |
465 | def addNote(repo, ref, key, value=None): | ||
466 | note = key + (": %s" % value if value else "") | ||
467 | notes_ref = GitApplyTree.notes_ref | ||
468 | runcmd(["git", "config", "notes.rewriteMode", "ignore"], repo) | ||
469 | runcmd(["git", "config", "notes.displayRef", notes_ref, notes_ref], repo) | ||
470 | runcmd(["git", "config", "notes.rewriteRef", notes_ref, notes_ref], repo) | ||
471 | runcmd(["git", "notes", "--ref", notes_ref, "append", "-m", note, ref], repo) | ||
472 | |||
473 | @staticmethod | ||
474 | def removeNote(repo, ref, key): | ||
475 | notes = GitApplyTree.getNotes(repo, ref) | ||
476 | notes = {k: v for k, v in notes.items() if k != key and not k.startswith(key + ":")} | ||
477 | runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "remove", "--ignore-missing", ref], repo) | ||
478 | for note, value in notes.items(): | ||
479 | GitApplyTree.addNote(repo, ref, note, value) | ||
480 | |||
481 | @staticmethod | ||
482 | def getNotes(repo, ref): | ||
483 | import re | ||
484 | |||
485 | note = None | ||
486 | try: | ||
487 | note = runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "show", ref], repo) | ||
488 | prefix = "" | ||
489 | except CmdError: | ||
490 | note = runcmd(['git', 'show', '-s', '--format=%B', ref], repo) | ||
491 | prefix = "%% " | ||
492 | |||
493 | note_re = re.compile(r'^%s(.*?)(?::\s*(.*))?$' % prefix) | ||
494 | notes = dict() | ||
495 | for line in note.splitlines(): | ||
496 | m = note_re.match(line) | ||
497 | if m: | ||
498 | notes[m.group(1)] = m.group(2) | ||
499 | |||
500 | return notes | ||
501 | |||
502 | @staticmethod | ||
464 | def commitIgnored(subject, dir=None, files=None, d=None): | 503 | def commitIgnored(subject, dir=None, files=None, d=None): |
465 | if files: | 504 | if files: |
466 | runcmd(['git', 'add'] + files, dir) | 505 | runcmd(['git', 'add'] + files, dir) |
467 | message = "%s\n\n%s" % (subject, GitApplyTree.ignore_commit_prefix) | ||
468 | cmd = ["git"] | 506 | cmd = ["git"] |
469 | GitApplyTree.gitCommandUserOptions(cmd, d=d) | 507 | GitApplyTree.gitCommandUserOptions(cmd, d=d) |
470 | cmd += ["commit", "-m", message, "--no-verify"] | 508 | cmd += ["commit", "-m", subject, "--no-verify"] |
471 | runcmd(cmd, dir) | 509 | runcmd(cmd, dir) |
510 | GitApplyTree.addNote(dir, "HEAD", GitApplyTree.ignore_commit) | ||
472 | 511 | ||
473 | @staticmethod | 512 | @staticmethod |
474 | def extractPatches(tree, startcommits, outdir, paths=None): | 513 | def extractPatches(tree, startcommits, outdir, paths=None): |
@@ -484,18 +523,20 @@ class GitApplyTree(PatchTree): | |||
484 | out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.join(tree, name)) | 523 | out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.join(tree, name)) |
485 | if out: | 524 | if out: |
486 | for srcfile in out.split(): | 525 | for srcfile in out.split(): |
487 | outfile = os.path.basename(srcfile) | 526 | # This loop, which is used to remove any line that |
527 | # starts with "%% original patch", is kept for backwards | ||
528 | # compatibility. If/when that compatibility is dropped, | ||
529 | # it can be replaced with code to just read the first | ||
530 | # line of the patch file to get the SHA-1, and the code | ||
531 | # below that writes the modified patch file can be | ||
532 | # replaced with a simple file move. | ||
488 | for encoding in ['utf-8', 'latin-1']: | 533 | for encoding in ['utf-8', 'latin-1']: |
489 | patchlines = [] | 534 | patchlines = [] |
490 | try: | 535 | try: |
491 | with open(srcfile, 'r', encoding=encoding, newline='') as f: | 536 | with open(srcfile, 'r', encoding=encoding, newline='') as f: |
492 | for line in f: | 537 | for line in f: |
493 | if line.startswith(GitApplyTree.patch_line_prefix): | 538 | if line.startswith("%% " + GitApplyTree.original_patch): |
494 | outfile = line.split()[-1].strip() | ||
495 | continue | 539 | continue |
496 | if line.startswith(GitApplyTree.ignore_commit_prefix): | ||
497 | outfile = None | ||
498 | break | ||
499 | patchlines.append(line) | 540 | patchlines.append(line) |
500 | except UnicodeDecodeError: | 541 | except UnicodeDecodeError: |
501 | continue | 542 | continue |
@@ -503,11 +544,16 @@ class GitApplyTree(PatchTree): | |||
503 | else: | 544 | else: |
504 | raise PatchError('Unable to find a character encoding to decode %s' % srcfile) | 545 | raise PatchError('Unable to find a character encoding to decode %s' % srcfile) |
505 | 546 | ||
506 | if outfile: | 547 | sha1 = patchlines[0].split()[1] |
507 | bb.utils.mkdirhier(os.path.join(outdir, name)) | 548 | notes = GitApplyTree.getNotes(os.path.join(tree, name), sha1) |
508 | with open(os.path.join(outdir, name, outfile), 'w') as of: | 549 | if GitApplyTree.ignore_commit in notes: |
509 | for line in patchlines: | 550 | continue |
510 | of.write(line) | 551 | outfile = notes.get(GitApplyTree.original_patch, os.path.basename(srcfile)) |
552 | |||
553 | bb.utils.mkdirhier(os.path.join(outdir, name)) | ||
554 | with open(os.path.join(outdir, name, outfile), 'w') as of: | ||
555 | for line in patchlines: | ||
556 | of.write(line) | ||
511 | finally: | 557 | finally: |
512 | shutil.rmtree(tempdir) | 558 | shutil.rmtree(tempdir) |
513 | 559 | ||
@@ -555,28 +601,11 @@ class GitApplyTree(PatchTree): | |||
555 | 601 | ||
556 | return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) | 602 | return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
557 | 603 | ||
558 | # Add hooks which add a pointer to the original patch file name in the commit message | ||
559 | reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip() | 604 | reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip() |
560 | if not reporoot: | 605 | if not reporoot: |
561 | raise Exception("Cannot get repository root for directory %s" % self.dir) | 606 | raise Exception("Cannot get repository root for directory %s" % self.dir) |
562 | gitdir = (runcmd("git rev-parse --absolute-git-dir".split(), self.dir) or '').strip() | 607 | |
563 | if not gitdir: | 608 | patch_applied = True |
564 | raise Exception("Cannot get gitdir for directory %s" % self.dir) | ||
565 | hooks_dir = os.path.join(gitdir, 'hooks') | ||
566 | hooks_dir_backup = hooks_dir + '.devtool-orig' | ||
567 | if os.path.lexists(hooks_dir_backup): | ||
568 | raise Exception("Git hooks backup directory already exists: %s" % hooks_dir_backup) | ||
569 | if os.path.lexists(hooks_dir): | ||
570 | shutil.move(hooks_dir, hooks_dir_backup) | ||
571 | os.mkdir(hooks_dir) | ||
572 | commithook = os.path.join(hooks_dir, 'commit-msg') | ||
573 | applyhook = os.path.join(hooks_dir, 'applypatch-msg') | ||
574 | with open(commithook, 'w') as f: | ||
575 | # NOTE: the formatting here is significant; if you change it you'll also need to | ||
576 | # change other places which read it back | ||
577 | f.write('echo "\n%s: $PATCHFILE" >> $1' % GitApplyTree.patch_line_prefix) | ||
578 | os.chmod(commithook, 0o755) | ||
579 | shutil.copy2(commithook, applyhook) | ||
580 | try: | 609 | try: |
581 | patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file']) | 610 | patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file']) |
582 | if self._need_dirty_check(): | 611 | if self._need_dirty_check(): |
@@ -587,7 +616,7 @@ class GitApplyTree(PatchTree): | |||
587 | pass | 616 | pass |
588 | else: | 617 | else: |
589 | if output: | 618 | if output: |
590 | # The tree is dirty, not need to try to apply patches with git anymore | 619 | # The tree is dirty, no need to try to apply patches with git anymore |
591 | # since they fail, fallback directly to patch | 620 | # since they fail, fallback directly to patch |
592 | output = PatchTree._applypatch(self, patch, force, reverse, run) | 621 | output = PatchTree._applypatch(self, patch, force, reverse, run) |
593 | output += self._commitpatch(patch, patchfilevar) | 622 | output += self._commitpatch(patch, patchfilevar) |
@@ -620,10 +649,12 @@ class GitApplyTree(PatchTree): | |||
620 | output = PatchTree._applypatch(self, patch, force, reverse, run) | 649 | output = PatchTree._applypatch(self, patch, force, reverse, run) |
621 | output += self._commitpatch(patch, patchfilevar) | 650 | output += self._commitpatch(patch, patchfilevar) |
622 | return output | 651 | return output |
652 | except: | ||
653 | patch_applied = False | ||
654 | raise | ||
623 | finally: | 655 | finally: |
624 | shutil.rmtree(hooks_dir) | 656 | if patch_applied: |
625 | if os.path.lexists(hooks_dir_backup): | 657 | GitApplyTree.addNote(self.dir, "HEAD", GitApplyTree.original_patch, os.path.basename(patch['file'])) |
626 | shutil.move(hooks_dir_backup, hooks_dir) | ||
627 | 658 | ||
628 | 659 | ||
629 | class QuiltTree(PatchSet): | 660 | class QuiltTree(PatchSet): |
diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index c4dcdb4550..d37848bdef 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py | |||
@@ -989,9 +989,10 @@ class DevtoolModifyTests(DevtoolBase): | |||
989 | self.assertIn(tempdir, result.output) | 989 | self.assertIn(tempdir, result.output) |
990 | # Check git repo | 990 | # Check git repo |
991 | self._check_src_repo(tempdir) | 991 | self._check_src_repo(tempdir) |
992 | # Check that the patch is correctly applied | 992 | # Check that the patch is correctly applied. |
993 | # last commit message in the tree must contain | 993 | # The last commit message in the tree must contain the following note: |
994 | # %% original patch: <patchname> | 994 | # Notes (devtool): |
995 | # original patch: <patchname> | ||
995 | # .. | 996 | # .. |
996 | patchname = None | 997 | patchname = None |
997 | for uri in src_uri: | 998 | for uri in src_uri: |
@@ -999,7 +1000,7 @@ class DevtoolModifyTests(DevtoolBase): | |||
999 | patchname = uri.replace("file://", "").partition('.patch')[0] + '.patch' | 1000 | patchname = uri.replace("file://", "").partition('.patch')[0] + '.patch' |
1000 | self.assertIsNotNone(patchname) | 1001 | self.assertIsNotNone(patchname) |
1001 | result = runCmd('git -C %s log -1' % tempdir) | 1002 | result = runCmd('git -C %s log -1' % tempdir) |
1002 | self.assertIn("%%%% original patch: %s" % patchname, result.output) | 1003 | self.assertIn("Notes (devtool):\n original patch: %s" % patchname, result.output) |
1003 | 1004 | ||
1004 | # Configure the recipe to check that the git dependencies are correctly patched in cargo config | 1005 | # Configure the recipe to check that the git dependencies are correctly patched in cargo config |
1005 | bitbake('-c configure %s' % testrecipe) | 1006 | bitbake('-c configure %s' % testrecipe) |