Page MenuHomeFreeBSD

D57845.id180626.diff
No OneTemporary

D57845.id180626.diff

diff --git a/tools/tools/git/git-mfc b/tools/tools/git/git-mfc
new file mode 100755
--- /dev/null
+++ b/tools/tools/git/git-mfc
@@ -0,0 +1,267 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: BSD-2-Clause
+# Copyright (c) 2025 Klara, Inc.
+#
+# git-mfc - Cherry-pick commits from main to a stable branch, automatically
+# including fixup commits identified by Fixes:/MFC-with:/MFC with: tags.
+#
+# Place this script in your PATH and invoke it as "git mfc <revision>...".
+#
+
+import argparse
+import datetime
+import os
+import re
+import subprocess
+import sys
+
+import git
+
+
+def err(code, msg):
+ print(os.path.basename(sys.argv[0]) + ': ' + msg, file=sys.stderr)
+ sys.exit(code)
+
+
+def fixes(repo, commit, warn=False):
+ """
+ Look at the commit's log message and return a list of commits that it fixes.
+
+ Hard-code a bunch of special cases in the src repo, where the fixes tag is
+ valid but malformed somehow.
+ """
+ special = {
+ "a693d17b9985a03bd9b5108e890d669005ab41eb": ["8a42005d1e491932666eb9f0be3e70ea1a28a3f7"],
+ "a04ca1c229195c7089b878a94fbe76505ea693b5": ["93b7818226cf5270646725805b4a8c17a1ad3761"],
+ "852088f6af6c5cd44542dde72aa5c3f4c4f9353c": ["b5fb9ae6872c499f1a02bec41f48b163a73a2aaa"],
+ "9c5d7e4a0c02bc45b61f565586da2abcc65d70fa": ["bec000c9c1ef409989685bb03ff0532907befb4a"],
+ "894cb08f0d3656fdb81f4d89085bedc4235f3cb6": ["5678d1d98a348f315453555377ccb28821a2ffcd"],
+ "cbddb2f02c7687d1039abcffd931e94e481c11a5": ["0118b0c8e58a438a931a5ce1bf8d7ae6208cc61b"],
+ "e9da71cd35d46ca13da4396d99e0af1703290e68": [],
+ "43cd6bbba053f02999d846ef297655ebec2d218b": ["38cbdae33b7c3f772845c06f52b86c0ddeab6a17"],
+ "94efe9f91be7f3aa501983716db5a4378282a734": ["7520b88860d7a79432e12ffcc47056844518bb62"],
+ "b332adfa96218148dfbb936a9c09d00484c868e3": ["7520b88860d7a79432e12ffcc47056844518bb62"],
+ "b911f504005df67f8c25f9b2f817c16588cd309c": ["801fb66a7e34c340f23d82f2b375feee4bd87df4"],
+ "3d37e7e5f540f513ab1d8fa61d9208c43b889401": ["fb9baa9b2045a193a3caf0a46b5cac5ef7a84b61"],
+ "0912408a281f203c43d0b3f73c38117336588342": ["9e33a616939fcff87f7539e3c41323deca5c74ce"],
+ "c036339ddf0cf80164f41ea31f1d8d27f4a068a9": ["a305b20ead13bb29880e15ff20c3bb83b5397a82"],
+ "9b56dfd27c64fcaf5dfbaa1eb3e2bd2b163fa56c": [],
+ "ab92cab02633580f763a38a329a5b25050bb4fbf": [],
+ "28fdb212adc0431fff683749a1307038e25ff58e": [],
+ "811912c46b5886f1aa3bb7a51a6ec1270bc947a8": [],
+ "813d981e1e78daffde4b2a05df35d054fcb4343f": [],
+ "ecf2a89a997ad4a14339b6a2f544e44b422620a0": [],
+ "f6517a7e69c10c6057d6c990a9f3ea22a2b62398": [],
+ "a5770eb54f7d13717098b5c34cc2dd51d2772021": ["86c06ff8864bc0e2233980c741b689714000850d"],
+ "b84d0aaa4e64fb95b105d0d38f6295fec7a82110": [],
+ "b586c66baf4824d175d051b3f5b06588c9aa2bc8": [],
+ }
+ if commit.hexsha in special:
+ return [repo.commit(c) for c in special[commit.hexsha]]
+
+ if commit.committed_date < datetime.datetime(2021, 1, 1).timestamp():
+ return []
+
+ regexp = re.compile(r'^([0-9a-f][0-9a-f]+)[, \(]?')
+
+ fixed = []
+ for line in commit.message.splitlines():
+ for tag, offset in (("Fixes", 1), ("MFC-with", 1), ("MFC with", 2)):
+ if line.startswith(tag + ':'):
+ line = ' '.join(line.split(':')[1:]).strip()
+ m = regexp.match(line)
+ if not m:
+ continue
+ try:
+ fixed.append(repo.commit(m.group(1)))
+ except (git.exc.BadName, ValueError):
+ if warn:
+ print(f"warning: {commit.hexsha[:12]}: {tag} tag "
+ f"references unknown commit {m.group(1)}",
+ file=sys.stderr)
+ return fixed
+
+
+def mfcclosure(repo, upstream, commits):
+ """
+ Given a set of commits to MFC from the upstream branch, return a list
+ of all commits in the original set plus those which fix up commits in
+ the original set.
+ """
+ tomfc = []
+ for rev in commits:
+ try:
+ if ".." in rev:
+ revlist = list(repo.iter_commits(rev))
+ revlist.reverse()
+ tomfc.extend(revlist)
+ else:
+ tomfc.append(repo.commit(rev))
+ except git.exc.BadName:
+ err(1, f"Revision '{rev}' is invalid")
+
+ tovisit = tomfc.copy()
+
+ for commit in repo.iter_commits(upstream):
+ if commit in tovisit:
+ tovisit.remove(commit)
+ if len(tovisit) == 0:
+ break
+ continue
+
+ for fix in fixes(repo, commit):
+ if fix in tomfc and commit not in tomfc:
+ i = tomfc.index(fix) + 1
+ tomfc[i:i] = mfcclosure(repo, upstream, [commit.hexsha])
+ else:
+ err(1, "the following commits are not in the upstream branch: " +
+ ', '.join([c.hexsha for c in tovisit]))
+ return tomfc
+
+
+def cherry_pick_origin(repo, commit, warn=False):
+ """
+ Given a commit, return the commit in the upstream branch which it was
+ cherry-picked from.
+ """
+ special = {
+ # stable/14
+ "aab45924bdb10338654e25ab9ecec106b7eb368b": "c57c02ebf7bcc9b02a0dc11711e8d8a6960ad34b",
+ "11dde2c8b7156a7d2072589c22c2f3c0de6880d8": "62af5b9dc6205289a0ace964d060fba64e71ef28",
+ "19f7b2bbc4e4fec33b7e8d546dd05a79533ca8e4": "d0cbb1930e82a53b07b1091402ff14cdfe7a4898",
+ "d034ff89b84f8470eb5bbcbb65c64c762e07f0bd": "ac2156c139f8f685b84a71a7f0f164d6cccc7656",
+ "fce2a3509a65a374820dc889929f8e8f5dbd1707": "a9722e5ae8519a9a28e950dbd8d489178e9bc27e",
+ "5946b0c6cbc77e6c5f62f5f7e635c6036e14f4d0": None,
+ "06bb8e1dab004ccb283f7a20fe84aa1326baf6b7": None,
+ "e5fadc41b48045d8978993d6c4ac72c64542b470": "e20971500194d2f7299e9d01ca3b20e9bc6b4009",
+ # stable/13
+ "fb4bc4c325eca15247a2e21ded0a70f92ec15488": "c57c02ebf7bcc9b02a0dc11711e8d8a6960ad34b",
+ "213406054e46b50d5df6d28b01a0a3410132b322": "62af5b9dc6205289a0ace964d060fba64e71ef28",
+ "eb6edafe8f484a69ff074018e5b93e481787923e": "d0cbb1930e82a53b07b1091402ff14cdfe7a4898",
+ "ced9fa71eaf95a27d5672c3419d7d9ca6c189168": "ac2156c139f8f685b84a71a7f0f164d6cccc7656",
+ "65bab39e140f97cace92a2923e50c6b654b02e22": "346483b1f10454c5617a25d5e136829f60fb1184",
+ "972637dc06a04432dc58e240b8ef3e9f538b98bb": "9209ea69bc03e7e9f678b2294da7a0317b5c9c5b",
+ "93a95ebbf7c8eb85aeb53a9fde329348992333e7": "6ccff5c0452c4bc2a4dd497e39a801ab8db8a021",
+ "9d31ae318711825d3a6ffa544d197708905435cf": "aef815e7873b006bd040ac1690425709635e32e7",
+ "a75324d674f5df10f1407080a49cfe933dbb06ec": "64ae2f785e0672286901c15045a24cbc533538d3",
+ "0729ba2f49c956789701aecb70f4f555181fd3a7": "91a8bed5a49eb2d1e4e096a4c68c108cebec8818",
+ "bec0d2c9c8413707b0fff8e65fb96aa53f149be3": "8d5c7813061dfa0b187500dfe3aeea7a28181c13",
+ }
+ if commit.hexsha in special:
+ sha = special[commit.hexsha]
+ return repo.commit(sha) if sha else None
+
+ lastline = commit.message.splitlines()[-1]
+ m = re.match(r'^\(cherry picked from commit ([0-9a-f]{40})', lastline)
+ if m:
+ try:
+ return repo.commit(m.group(1))
+ except (git.exc.BadName, ValueError):
+ if warn:
+ print(f"warning: {commit.hexsha[:12]}: cherry-picked from "
+ f"unknown commit {m.group(1)}", file=sys.stderr)
+
+
+def dangling(repo, upstream):
+ """
+ Find commits in the current branch which have been cherry-picked from
+ upstream but are missing fixup commits.
+ """
+ base = repo.merge_base('HEAD', upstream, all=True)[-1]
+ result = []
+ picked = []
+ for commit in repo.iter_commits(base.hexsha + '..HEAD'):
+ ref = cherry_pick_origin(repo, commit, warn=True)
+ if ref:
+ picked.append(ref.hexsha)
+ # Create a mapping of all upstream commits to their fixups.
+ fixups = {}
+ for commit in repo.iter_commits(base.hexsha + '..' + upstream):
+ for fix in fixes(repo, commit, warn=True):
+ if fix.hexsha not in fixups:
+ fixups[fix.hexsha] = []
+ fixups[fix.hexsha].append(commit.hexsha)
+ # Any commit that's been cherry-picked and has fixups that haven't
+ # been cherry-picked goes into our set.
+ for commit in picked:
+ for f in fixups.get(commit, []):
+ if f not in picked:
+ result.append((repo.commit(f), repo.commit(commit)))
+ return result
+
+
+def cherry_pick(commit):
+ """Cherry-pick a single commit with -x. Abort on failure."""
+ result = subprocess.run(
+ ['git', 'cherry-pick', '-x', commit.hexsha],
+ capture_output=False,
+ )
+ if result.returncode != 0:
+ print(f"\nCherry-pick of {commit.hexsha} ({commit.summary}) failed.",
+ file=sys.stderr)
+ print("Resolve the conflict and then re-run git mfc to continue,",
+ file=sys.stderr)
+ print("or run 'git cherry-pick --abort' to give up.", file=sys.stderr)
+ sys.exit(1)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ prog='git mfc',
+ description='Cherry-pick commits from main to a stable branch, '
+ 'automatically including fixup commits.',
+ )
+ parser.add_argument(
+ '-n', action='store_true',
+ help='List the commits that would be cherry-picked, but do not act',
+ )
+ parser.add_argument(
+ '--dangling', action='store_true',
+ help='Find cherry-picked commits in the current branch that are '
+ 'missing fixup commits from the origin branch',
+ )
+ parser.add_argument(
+ '--origin', type=str, default='main',
+ help='Upstream branch (default: main)',
+ )
+ parser.add_argument(
+ '-r', '--remote', type=str, default='anon',
+ help='Upstream remote (default: anon)',
+ )
+ parser.add_argument(
+ 'commits', nargs='*', metavar='revision',
+ help='Commits to MFC (hashes, ranges with "..", etc.)',
+ )
+ args = parser.parse_args()
+
+ if not args.dangling and len(args.commits) == 0:
+ parser.error('at least one revision is required (or use --dangling)')
+
+ upstream = args.remote + '/' + args.origin
+
+ repo = git.Repo()
+ if not args.n:
+ print(f"Fetching {upstream}...")
+ repo.remotes[args.remote].fetch(args.origin)
+
+ if args.dangling:
+ missing = dangling(repo, upstream)
+ for fixup, commit in missing:
+ print(f'{fixup.hexsha} ({fixup.summary}) fixes {commit.hexsha} ({commit.summary})')
+ return
+
+ tomfc = mfcclosure(repo, upstream, args.commits)
+
+ if args.n:
+ for commit in tomfc:
+ print(commit.hexsha, commit.summary)
+ return
+
+ for commit in tomfc:
+ print(f"Cherry-picking {commit.hexsha[:12]} {commit.summary}")
+ cherry_pick(commit)
+
+
+if __name__ == '__main__':
+ main()

File Metadata

Mime Type
text/plain
Expires
Tue, Jul 7, 12:23 AM (9 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34361722
Default Alt Text
D57845.id180626.diff (10 KB)

Event Timeline