Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F161693621
D57845.id180626.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
10 KB
Referenced Files
None
Subscribers
None
D57845.id180626.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D57845: tools: Add git-mfc
Attached
Detach File
Event Timeline
Log In to Comment