summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPeter Kjellerstedt <peter.kjellerstedt@axis.com>2024-02-19 02:28:32 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2024-02-19 16:03:22 +0000
commit4cfd0f7e4e2db19344677999572e5b71ae97dfc4 (patch)
tree07f0d397468ec6eeeddb4a2bba1e6f8a6320f915
parentdc2e09417d172ec3da01786e42ebe5e0734ee9ae (diff)
downloadpoky-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>
-rw-r--r--meta/lib/oe/patch.py109
-rw-r--r--meta/lib/oeqa/selftest/cases/devtool.py9
-rw-r--r--scripts/lib/devtool/standard.py15
-rw-r--r--scripts/lib/devtool/upgrade.py24
4 files changed, 103 insertions, 54 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
296class GitApplyTree(PatchTree): 296class 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
629class QuiltTree(PatchSet): 660class 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)
diff --git a/scripts/lib/devtool/standard.py b/scripts/lib/devtool/standard.py
index 6d7fd17fbd..7972b4f822 100644
--- a/scripts/lib/devtool/standard.py
+++ b/scripts/lib/devtool/standard.py
@@ -937,14 +937,13 @@ def modify(args, config, basepath, workspace):
937 seen_patches = [] 937 seen_patches = []
938 for branch in branches: 938 for branch in branches:
939 branch_patches[branch] = [] 939 branch_patches[branch] = []
940 (stdout, _) = bb.process.run('git log devtool-base..%s' % branch, cwd=srctree) 940 (stdout, _) = bb.process.run('git rev-list devtool-base..%s' % branch, cwd=srctree)
941 for line in stdout.splitlines(): 941 for sha1 in stdout.splitlines():
942 line = line.strip() 942 notes = oe.patch.GitApplyTree.getNotes(srctree, sha1.strip())
943 if line.startswith(oe.patch.GitApplyTree.patch_line_prefix): 943 origpatch = notes.get(oe.patch.GitApplyTree.original_patch)
944 origpatch = line[len(oe.patch.GitApplyTree.patch_line_prefix):].split(':', 1)[-1].strip() 944 if origpatch and origpatch not in seen_patches:
945 if not origpatch in seen_patches: 945 seen_patches.append(origpatch)
946 seen_patches.append(origpatch) 946 branch_patches[branch].append(origpatch)
947 branch_patches[branch].append(origpatch)
948 947
949 # Need to grab this here in case the source is within a subdirectory 948 # Need to grab this here in case the source is within a subdirectory
950 srctreebase = srctree 949 srctreebase = srctree
diff --git a/scripts/lib/devtool/upgrade.py b/scripts/lib/devtool/upgrade.py
index ef58523dc8..fa5b8ef3c7 100644
--- a/scripts/lib/devtool/upgrade.py
+++ b/scripts/lib/devtool/upgrade.py
@@ -261,7 +261,7 @@ def _extract_new_source(newpv, srctree, no_patch, srcrev, srcbranch, branch, kee
261 revs = {} 261 revs = {}
262 for path in paths: 262 for path in paths:
263 (stdout, _) = _run('git rev-parse HEAD', cwd=path) 263 (stdout, _) = _run('git rev-parse HEAD', cwd=path)
264 revs[os.path.relpath(path,srctree)] = stdout.rstrip() 264 revs[os.path.relpath(path, srctree)] = stdout.rstrip()
265 265
266 if no_patch: 266 if no_patch:
267 patches = oe.recipeutils.get_recipe_patches(crd) 267 patches = oe.recipeutils.get_recipe_patches(crd)
@@ -272,17 +272,35 @@ def _extract_new_source(newpv, srctree, no_patch, srcrev, srcbranch, branch, kee
272 _run('git checkout devtool-patched -b %s' % branch, cwd=path) 272 _run('git checkout devtool-patched -b %s' % branch, cwd=path)
273 (stdout, _) = _run('git branch --list devtool-override-*', cwd=path) 273 (stdout, _) = _run('git branch --list devtool-override-*', cwd=path)
274 branches_to_rebase = [branch] + stdout.split() 274 branches_to_rebase = [branch] + stdout.split()
275 target_branch = revs[os.path.relpath(path, srctree)]
276
277 # There is a bug (or feature?) in git rebase where if a commit with
278 # a note is fully rebased away by being part of an old commit, the
279 # note is still attached to the old commit. Avoid this by making
280 # sure all old devtool related commits have a note attached to them
281 # (this assumes git config notes.rewriteMode is set to ignore).
282 (stdout, _) = __run('git rev-list devtool-base..%s' % target_branch)
283 for rev in stdout.splitlines():
284 if not oe.patch.GitApplyTree.getNotes(path, rev):
285 oe.patch.GitApplyTree.addNote(path, rev, "dummy")
286
275 for b in branches_to_rebase: 287 for b in branches_to_rebase:
276 logger.info("Rebasing {} onto {}".format(b, revs[os.path.relpath(path,srctree)])) 288 logger.info("Rebasing {} onto {}".format(b, target_branch))
277 _run('git checkout %s' % b, cwd=path) 289 _run('git checkout %s' % b, cwd=path)
278 try: 290 try:
279 _run('git rebase %s' % revs[os.path.relpath(path, srctree)], cwd=path) 291 _run('git rebase %s' % target_branch, cwd=path)
280 except bb.process.ExecutionError as e: 292 except bb.process.ExecutionError as e:
281 if 'conflict' in e.stdout: 293 if 'conflict' in e.stdout:
282 logger.warning('Command \'%s\' failed:\n%s\n\nYou will need to resolve conflicts in order to complete the upgrade.' % (e.command, e.stdout.rstrip())) 294 logger.warning('Command \'%s\' failed:\n%s\n\nYou will need to resolve conflicts in order to complete the upgrade.' % (e.command, e.stdout.rstrip()))
283 _run('git rebase --abort', cwd=path) 295 _run('git rebase --abort', cwd=path)
284 else: 296 else:
285 logger.warning('Command \'%s\' failed:\n%s' % (e.command, e.stdout)) 297 logger.warning('Command \'%s\' failed:\n%s' % (e.command, e.stdout))
298
299 # Remove any dummy notes added above.
300 (stdout, _) = __run('git rev-list devtool-base..%s' % target_branch)
301 for rev in stdout.splitlines():
302 oe.patch.GitApplyTree.removeNote(path, rev, "dummy")
303
286 _run('git checkout %s' % branch, cwd=path) 304 _run('git checkout %s' % branch, cwd=path)
287 305
288 if tmpsrctree: 306 if tmpsrctree: