Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F145904251
D49013.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
11 KB
Referenced Files
None
Subscribers
None
D49013.diff
View Options
diff --git a/tools/tools/git/mfc-helper b/tools/tools/git/mfc-helper
new file mode 100755
--- /dev/null
+++ b/tools/tools/git/mfc-helper
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+
+#
+# SPDX-License-Identifier: BSD-2-Clause
+# Copyright (c) 2025 Klara, Inc.
+#
+
+#
+# A script which helps with MFCing commits.
+#
+# $ mfc-helper [-ln] [-b <upstream branch>] [-r <upstream remote>] [<commit1> .. <commitN>]
+#
+# Given a list of revisions (which may be specified as ranges, e.g., "A~..B"),
+# this script will print a list of commits which need to be cherry-picked in
+# order to complete the MFC. This includes the commits themselves, as well as
+# any commits from the upstream branch which are tagged as fixing one or more
+# of the commits in the list.
+#
+# If no revisions are specified, the script will look at the current branch
+# and find all commits which have been cherry-picked from the upstream branch
+# but are missing their fixup commits.
+#
+# By default, the upstream branch is freebsd/main.
+#
+
+import argparse
+import datetime
+import git
+import os
+import re
+import sys
+
+
+def err(code, msg):
+ print(os.path.basename(sys.argv[0]) + ': ' + msg, file=sys.stderr)
+ sys.exit(code)
+
+
+def warn(msg):
+ print(os.path.basename(sys.argv[0]) + ': ' + msg, file=sys.stderr)
+
+
+def fixes(repo, commit):
+ """
+ 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. In other cases we just ignore the tag, as it
+ refers to a GitHub object (and we don't care about those) or a Subversion
+ revision (old enough that we don't care anymore). This list ought not to
+ grow over time, but that's a bit tricky to enforce.
+ """
+ special = {
+ # Malformed/typoed/incorrect tags.
+ "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"],
+ # Tags that refer to some external repo.
+ "9b56dfd27c64fcaf2dfbaa1eb3e2bd2b163fa56c": [],
+ "ab92cab02633580f763a38a329a5b25050bb4fbf": [],
+ "28fdb212adc0431fff683749a1307038e25ff58e": [],
+ "811912c46b5886f1aa3bb7a51a6ec1270bc947a8": [],
+ "813d981e1e78daffde4b2a05df35d054fcb4343f": [],
+ "ecf2a89a997ad4a14339b6a2f544e44b422620a0": [],
+ "f6517a7e69c10c6057d6c990a9f3ea22a2b62398": [],
+ # Ambiguous hash.
+ "a5770eb54f7d13717098b5c34cc2dd51d2772021": ["86c06ff8864bc0e2233980c741b689714000850d"],
+ "b84d0aaa4e64fb95b105d0d38f6295fec7a82110": [],
+ "b586c66baf4824d175d051b3f5b06588c9aa2bc8": [],
+ }
+ if commit.hexsha in special:
+ return [repo.commit(c) for c in special[commit.hexsha]]
+
+ # If the commit is old enough, just forget about it.
+ 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 + ':'):
+ # Get rid of the tag.
+ line = ' '.join(line.split(':')[1:]).strip()
+ # Does it appear to start with a commit hash?
+ m = regexp.match(line)
+ if not m:
+ continue
+ fixed.append(repo.commit(m.group(1)))
+ 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.
+ """
+ # The user is MFCing specific commits. Which ones?
+ tomfc = []
+ for rev in commits:
+ # If the revision contains "..", treat it as a range, otherwise it's
+ # just a single commit. This is gross but I can't find a better way.
+ 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")
+
+ # When iterating over commits in the upstream branch, we can stop once
+ # we've visited every commit that we're MFCing.
+ tovisit = tomfc.copy()
+
+ # Now go through the upstream branch and find all commits which fix at
+ # least one of the commits we're MFCing.
+ 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:
+ # Insert the fixup immediately after the commit. This gives
+ # the correct order if there are multiple fixups. Note that
+ # fixups can have fixups.
+ 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 origin(repo, commit):
+ """
+ Given a commit, return the set of commits from the upstream branch from which it
+ was cherry-picked, using the explicit annotations added by `git cherry-pick -x`.
+ """
+ special = {
+ # stable/14
+ "aab45924bdb10338654e25ab9ecec106b7eb368b": ["c57c02ebf7bcc9b02a0dc11711e8d8a6960ad34b"],
+ "11dde2c8b7156a7d2072589c22c2f3c0de6880d8": ["62af5b9dc6205289a0ace964d060fba64e71ef28"],
+ "19f7b2bbc4e4fec33b7e8d546dd05a79533ca8e4": ["d0cbb1930e82a53b07b1091402ff14cdfe7a4898"],
+ "d034ff89b84f8470eb5bbcbb65c64c762e07f0bd": ["ac2156c139f8f685b84a71a7f0f164d6cccc7656"],
+ "fce2a3509a65a374820dc889929f8e8f5dbd1707": ["a9722e5ae8519a9a28e950dbd8d489178e9bc27e"],
+ "5946b0c6cbc77e6c5f62f5f7e635c6036e14f4d0": [],
+ "06bb8e1dab004ccb283f7a20fe84aa1326baf6b7": [],
+ "e5fadc41b48045d8978993d6c4ac72c64542b470": ["e20971500194d2f7299e9d01ca3b20e9bc6b4009"],
+ "3ba946aebf747e12da4fb22ee0e45ee0e3a233ee": ["c4380d5b9383e2a062840573b36ab643b65b2610"],
+ "404b91dac415c5c9126fb4201145049c4d3dfbba": ["93b7818226cf5270646725805b4a8c17a1ad3761",
+ "a04ca1c229195c7089b878a94fbe76505ea693b5"],
+ # 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:
+ return [repo.commit(c) for c in special[commit.hexsha]]
+
+ commits = []
+ for line in commit.message.splitlines():
+ m = re.match(r'^\(cherry picked from commit ([0-9a-f]{40})', line)
+ if m:
+ try:
+ toadd = repo.commit(m.group(1))
+ except git.exc.BadName:
+ warn(f"Commit {commit.hexsha} refers to unknown object {m.group(1)}")
+ commits.append(toadd)
+ return commits
+
+
+def dangling(repo, upstream):
+ """
+ Find commits in the current branch which have been cherry-picked from
+ upstream but are missing fixup commits.
+ """
+ # Find the upstream commits corresponding to all cherry-picks in this
+ # branch.
+ base = repo.merge_base('HEAD', upstream, all=True)[-1]
+ dangling = []
+ picked = []
+ for commit in repo.iter_commits(base.hexsha + '..HEAD'):
+ picked.extend([ref.hexsha for ref in origin(repo, commit)])
+ # 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):
+ if fix.hexsha not in fixups:
+ fixups[fix.hexsha] = []
+ fixups[fix.hexsha].append(commit.hexsha)
+ # Now, 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:
+ dangling.append((repo.commit(f), repo.commit(commit)))
+
+ return dangling
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-b', type=str, help='Upstream branch', default='main')
+ parser.add_argument('-l', action='store_true', help='List commits only')
+ parser.add_argument('-n', action='store_true', help='Do not fetch remote')
+ parser.add_argument('-r', type=str, help='Upstream remote', default='freebsd')
+ parser.add_argument('commits', nargs='*', help='Commits to MFC')
+ args = parser.parse_args()
+
+ branch = args.r + '/' + args.b
+
+ repo = git.Repo()
+ if not args.n:
+ repo.remotes[args.r].fetch(args.b)
+
+ if len(args.commits) > 0:
+ # The user wants to see the MFC closure of a set of commits.
+ tomfc = mfcclosure(repo, branch, args.commits)
+ for commit in tomfc:
+ if args.l:
+ print(commit.hexsha)
+ else:
+ print(commit.hexsha, commit.summary)
+ else:
+ tomfc = dangling(repo, branch)
+ for missing, commit in tomfc:
+ if args.l:
+ print(missing.hexsha)
+ else:
+ print(f'{missing.hexsha} ({missing.summary}) required by {commit.hexsha} ({commit.summary})')
+
+
+if __name__ == '__main__':
+ main()
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Feb 26, 11:51 PM (16 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
29009090
Default Alt Text
D49013.diff (11 KB)
Attached To
Mode
D49013: tools/git: Add a script which can process fixup tags
Attached
Detach File
Event Timeline
Log In to Comment