Page MenuHomeFreeBSD

D49013.diff
No OneTemporary

D49013.diff

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

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)

Event Timeline