Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F161712553
D57845.id180979.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
28 KB
Referenced Files
None
Subscribers
None
D57845.id180979.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,523 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2025 Klara, Inc.
+# Copyright (c) 2026 Mark Johnston <markj@FreeBSD.org>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# git-mfc - Helper command for MFCing commits to stable or release branches.
+#
+
+import argparse
+import datetime
+import os
+import re
+import subprocess
+import sys
+
+
+def err(code, msg):
+ print(os.path.basename(sys.argv[0]) + ': ' + msg, file=sys.stderr)
+ sys.exit(code)
+
+
+try:
+ import git
+except ImportError:
+ err(1, "gitpython module missing; install it with pip or the devel/py-gitpython package")
+
+
+def commit_summary(commit):
+ return f'{commit.hexsha} ("{commit.summary}")'
+
+
+def origin_branch():
+ """
+ Determine the default origin branch by parsing sys/conf/newvers.sh.
+ """
+ newvers = 'sys/conf/newvers.sh'
+ if not os.path.exists(newvers):
+ err(1, f"{newvers} not found; use --origin to specify the upstream branch")
+
+ revision = None
+ branch = None
+ with open(newvers) as f:
+ for line in f:
+ m = re.match(r'^REVISION="([^"]+)"', line)
+ if m:
+ revision = m.group(1)
+ m = re.match(r'^BRANCH="([^"]+)"', line)
+ if m:
+ branch = m.group(1)
+
+ if branch is None:
+ err(1, f"could not determine BRANCH from {newvers}")
+ if branch == "CURRENT":
+ err(1, "this is a CURRENT tree, we do not MFC to CURRENT")
+ if branch == "STABLE":
+ return "main"
+ if re.match(r'^RELEASE(-p\d+)?$', branch):
+ if revision is None:
+ err(1, f"could not determine REVISION from {newvers}")
+ major = revision.split('.')[0]
+ return f"stable/{major}"
+
+ err(1, f"unexpected BRANCH value '{branch}' in {newvers}")
+
+
+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 reverts(commit):
+ """
+ Look at the commit's log message and return the hash of the commit it
+ reverts, or None.
+ """
+ for line in commit.message.splitlines():
+ m = re.match(r'^This reverts commit ([0-9a-f]{40})\.', line)
+ if m:
+ return m.group(1)
+ return None
+
+
+def mfcclosure(repo, upstream, commits, _reverted=None):
+ """
+ Given a set of commits to MFC from the upstream branch, return a tuple
+ (tomfc, reverted) where tomfc is a list of all commits in the original
+ set plus those which fix up commits in the original set (excluding
+ reverted fixups), and reverted is the set of commit hashes that have
+ been reverted on the upstream branch.
+
+ Reverts are collected during the upstream walk, so no separate scan is
+ needed. Recursive calls share the same reverted set via _reverted.
+ """
+ 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")
+
+ toplevel = _reverted is None
+ if toplevel:
+ _reverted = set()
+
+ tovisit = tomfc.copy()
+
+ for commit in repo.iter_commits(upstream):
+ r = reverts(commit)
+ if r:
+ _reverted.add(r)
+
+ 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:
+ if commit.hexsha in _reverted:
+ print(f"warning: skipping {commit_summary(commit)}: "
+ f"fixup was reverted upstream", file=sys.stderr)
+ continue
+ i = tomfc.index(fix) + 1
+ sub, _ = mfcclosure(repo, upstream, [commit.hexsha],
+ _reverted=_reverted)
+ tomfc[i:i] = sub
+ else:
+ err(1, "the following commits are not in the upstream branch: " +
+ ', '.join([c.hexsha for c in tovisit]))
+ return (tomfc, _reverted)
+
+
+def mfc_origins(repo, commit, warn=False):
+ """
+ Given a commit, return a list of upstream commits that it was
+ cherry-picked from. This is usually a single commit, but may be
+ multiple if several upstream commits were squashed into one MFC.
+
+ Handle a bunch of special commits where the existing cherry-pick
+ metadata is incorrect.
+ """
+ special = {
+ # stable/15
+ "e224f2c1b98ba065942c83d322cdb8e3987d0c81": ["068fea0aa15bceb7b6b01687542b58ee81d1d887"],
+ "48578fbefa666abfcfe3a47269343febefae94f9": ["728ec0c094ce473ae17ebd1adb05f0959bf3a68e"],
+ "2bf7d850c53e55ffced055235de432055f0cb9e2": ["7e79bc8ce70693a892c443c42af5ec16a95ba466"],
+ "e554d44c3a7ffa8664e89f35aec80b917f69e6b3": ["561dc357c2f5892af3aa481a1020860b7ff473e0"],
+ # stable/14
+ "aab45924bdb10338654e25ab9ecec106b7eb368b": ["c57c02ebf7bcc9b02a0dc11711e8d8a6960ad34b"],
+ "11dde2c8b7156a7d2072589c22c2f3c0de6880d8": ["62af5b9dc6205289a0ace964d060fba64e71ef28"],
+ "19f7b2bbc4e4fec33b7e8d546dd05a79533ca8e4": ["d0cbb1930e82a53b07b1091402ff14cdfe7a4898"],
+ "d034ff89b84f8470eb5bbcbb65c64c762e07f0bd": ["ac2156c139f8f685b84a71a7f0f164d6cccc7656"],
+ "fce2a3509a65a374820dc889929f8e8f5dbd1707": ["a9722e5ae8519a9a28e950dbd8d489178e9bc27e"],
+ "5946b0c6cbc77e6c5f62f5f7e635c6036e14f4d0": [],
+ "06bb8e1dab004ccb283f7a20fe84aa1326baf6b7": [],
+ "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:
+ return [repo.commit(s) for s in special[commit.hexsha]]
+
+ origins = []
+ for line in commit.message.splitlines():
+ m = re.match(r'^\(cherry picked from commit ([0-9a-f]{40})', line)
+ if m:
+ try:
+ origins.append(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)
+ return origins
+
+
+def dangling(repo, upstream, author=None):
+ """
+ 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 = []
+ mine = []
+ for commit in repo.iter_commits(base.hexsha + '..HEAD'):
+ refs = mfc_origins(repo, commit, warn=True)
+ for ref in refs:
+ picked.append(ref.hexsha)
+ if author is None or author in commit.author.email:
+ mine.append(ref.hexsha)
+ # Create a mapping of all upstream commits to their fixups,
+ # tracking reverts as we go.
+ fixups = {}
+ reverted = set()
+ for commit in repo.iter_commits(base.hexsha + '..' + upstream):
+ r = reverts(commit)
+ if r:
+ reverted.add(r)
+ for fix in fixes(repo, commit, warn=True):
+ if fix.hexsha not in fixups:
+ fixups[fix.hexsha] = []
+ fixups[fix.hexsha].append(commit.hexsha)
+ # For each of the user's cherry-picks, find fixups that haven't been
+ # cherry-picked (by anyone) or reverted.
+ for commit in mine:
+ for f in fixups.get(commit, []):
+ if f not in picked and f not in reverted:
+ result.append((repo.commit(f), repo.commit(commit)))
+ return result
+
+
+def mfc_after(commit):
+ """
+ Parse "MFC after:" tags from a commit message and return the time delta,
+ or None if there is no parseable MFC-after tag.
+ """
+ units = {
+ 'day': 1, 'days': 1,
+ 'week': 7, 'weeks': 7,
+ 'month': 30, 'months': 30,
+ }
+ for line in commit.message.splitlines():
+ m = re.match(r'^MFC[ -]after:\s*(\d+)\s+(\w+)', line, re.IGNORECASE)
+ if m:
+ count = int(m.group(1))
+ unit = m.group(2).lower()
+ if unit in units:
+ return datetime.timedelta(days=count * units[unit])
+ return None
+
+
+def pending(repo, upstream, author=None, baking=False):
+ """
+ Find commits in the upstream branch that have an "MFC after:" tag, whose
+ waiting period has elapsed, and which have not been cherry-picked to the
+ current branch.
+
+ If baking is True, also include commits whose waiting period
+ has not yet elapsed.
+ """
+ base = repo.merge_base('HEAD', upstream, all=True)[-1]
+
+ # Collect the set of upstream commits already cherry-picked.
+ picked = set()
+ for commit in repo.iter_commits(base.hexsha + '..HEAD'):
+ for ref in mfc_origins(repo, commit):
+ picked.add(ref.hexsha)
+
+ results = {}
+
+ now = datetime.datetime.now(datetime.timezone.utc)
+ revlist = list(repo.iter_commits(base.hexsha + '..' + upstream))
+ revlist.reverse()
+ for commit in revlist:
+ r = reverts(commit)
+ if r and r in results:
+ # Don't report reverted commits.
+ del results[r]
+ continue
+
+ if commit.hexsha in picked:
+ # Don't report commits that have already been cherry-picked.
+ continue
+
+ delta = mfc_after(commit)
+ if delta is None:
+ # Not yet eligible for MFC.
+ continue
+
+ if author is not None and author not in commit.author.email:
+ # Don't report commits that don't match the requested author.
+ continue
+
+ committed = datetime.datetime.fromtimestamp(
+ commit.committed_date, tz=datetime.timezone.utc)
+ ready_date = committed + delta
+ ready = now >= ready_date
+
+ if not baking and not ready:
+ continue
+
+ results[commit.hexsha] = (commit, ready_date, ready)
+
+ return list(results.values())
+
+
+def cherry_pick(commit, edit=False):
+ """Cherry-pick a single commit with -x. Abort on failure."""
+ cmd = ['git', 'cherry-pick', '-x']
+ if edit:
+ cmd.append('-e')
+ cmd.append(commit.hexsha)
+ result = subprocess.run(cmd, 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(
+ '-e', '--edit', action='store_true',
+ help='Edit the commit message before committing',
+ )
+ parser.add_argument(
+ '-n', action='store_true',
+ help='List the commits that would be cherry-picked, but do not act',
+ )
+ parser.add_argument(
+ '-a', '--author', type=str, default=None,
+ help='Filter --pending/--dangling results by author (default: current user)',
+ )
+ parser.add_argument(
+ '--all', action='store_true',
+ help='With --pending/--dangling, show commits from all authors',
+ )
+ parser.add_argument(
+ '--baking', action='store_true',
+ help='With --pending, also show commits whose MFC-after period '
+ 'has not yet elapsed',
+ )
+ 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(
+ '-f', '--force', action='store_true',
+ help='Cherry-pick commits even if they appear to be already present',
+ )
+ parser.add_argument(
+ '--ignore-reverts', action='store_true',
+ help='Cherry-pick commits even if they were reverted upstream',
+ )
+ parser.add_argument(
+ '--pending', action='store_true',
+ help='Show upstream commits with MFC-after tags that are ready to '
+ 'be cherry-picked',
+ )
+ parser.add_argument(
+ '--origin', type=str, default=None,
+ help='Upstream branch (default: derived from sys/conf/newvers.sh)',
+ )
+ parser.add_argument(
+ '-r', '--remote', type=str, default=None,
+ help='Upstream remote (default: tracking remote of current branch)',
+ )
+ parser.add_argument(
+ 'commits', nargs='*', metavar='revision',
+ help='Commits to MFC (hashes, ranges with "..", etc.)',
+ )
+ args = parser.parse_args()
+
+ if args.dangling and args.pending:
+ err(1, 'usage error: --dangling and --pending are mutually exclusive')
+ if (args.dangling or args.pending) and len(args.commits) > 0:
+ err(1, 'usage error: revisions cannot be specified with --dangling or --pending')
+ if not args.dangling and not args.pending and len(args.commits) == 0:
+ err(1, 'usage error: at least one revision is required')
+
+ origin = args.origin if args.origin else origin_branch()
+
+ repo = git.Repo()
+ if args.remote:
+ remote = args.remote
+ else:
+ try:
+ tracking = repo.active_branch.tracking_branch()
+ remote = tracking.remote_name if tracking else 'freebsd'
+ except (TypeError, ValueError):
+ remote = 'freebsd'
+
+ upstream = remote + '/' + origin
+
+ if not args.n:
+ repo.remotes[remote].fetch(origin)
+
+ show_all = getattr(args, 'all')
+ if show_all:
+ author = None
+ elif args.author:
+ author = args.author
+ elif args.dangling or args.pending:
+ author = repo.config_reader().get_value('user', 'email', default=None)
+ if author is None:
+ err(1, "could not determine user email; use -a or --all")
+ else:
+ author = None
+
+ if args.dangling:
+ missing = dangling(repo, upstream, author=author)
+ for fixup, commit in missing:
+ print(f'{commit_summary(fixup)} fixes {commit_summary(commit)}')
+ elif args.pending:
+ results = pending(repo, upstream, author=author,
+ baking=args.baking)
+ for commit, ready_date, ready in results:
+ date_str = ready_date.strftime('%Y-%m-%d')
+ if ready:
+ status = f"ready since {date_str}"
+ else:
+ status = f"ready on {date_str}"
+ print(f'{commit_summary(commit)} ({status})')
+ else:
+ tomfc, reverted = mfcclosure(repo, upstream, args.commits)
+
+ # Check if any explicitly requested commits were reverted.
+ if not args.ignore_reverts:
+ for commit in tomfc:
+ if commit.hexsha in reverted:
+ err(1, f"{commit_summary(commit)} was reverted "
+ f"upstream; use --ignore-reverts to override")
+
+ # Find commits already cherry-picked to the current branch so we
+ # can skip them. This lets the user re-run the same command after
+ # resolving a conflict without re-applying earlier commits.
+ if not args.force:
+ already_picked = set()
+ base = repo.merge_base('HEAD', upstream, all=True)[-1]
+ for c in repo.iter_commits(base.hexsha + '..HEAD'):
+ for ref in mfc_origins(repo, c):
+ already_picked.add(ref.hexsha)
+ else:
+ already_picked = set()
+
+ if args.n:
+ for commit in tomfc:
+ if commit.hexsha in already_picked:
+ continue
+ print(commit.hexsha, commit.summary)
+ return
+
+ for commit in tomfc:
+ if commit.hexsha in already_picked:
+ print(f"Skipping {commit.hexsha[:12]} {commit.summary} "
+ f"(already cherry-picked)")
+ continue
+ print(f"Cherry-picking {commit.hexsha[:12]} {commit.summary}")
+ cherry_pick(commit, edit=args.edit)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/tools/git/git-mfc.1 b/tools/tools/git/git-mfc.1
new file mode 100644
--- /dev/null
+++ b/tools/tools/git/git-mfc.1
@@ -0,0 +1,309 @@
+.\"
+.\" Copyright (c) 2026 Mark Johnston <markj@FreeBSD.org>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.Dd June 26, 2026
+.Dt GIT-MFC 1
+.Os
+.Sh NAME
+.Nm "git mfc"
+.Nd cherry-pick commits from an upstream branch to a stable or release branch
+.Sh SYNOPSIS
+.Nm
+.Op Fl efn
+.Op Fl -ignore-reverts
+.Op Fl -origin Ar branch
+.Op Fl r Ar remote
+.Ar revision ...
+.Nm
+.Fl -pending
+.Op Fl a Ar author
+.Op Fl -all
+.Op Fl -baking
+.Op Fl -origin Ar branch
+.Op Fl r Ar remote
+.Nm
+.Fl -dangling
+.Op Fl a Ar author
+.Op Fl -all
+.Op Fl -origin Ar branch
+.Op Fl r Ar remote
+.Sh DESCRIPTION
+The
+.Nm
+utility helps with MFCing (merge from CURRENT) commits to
+.Fx
+stable and release branches.
+It must be run from a checkout of the branch being MFCed
+.Em to .
+The origin branch is determined automatically and can be
+overridden with the
+.Fl -origin
+option.
+.Pp
+There are three modes of operation:
+.Bl -enum
+.It
+In the default mode, one or more revisions are specified on the command line.
+Each revision is cherry-picked with
+.Fl x
+to the current branch.
+The origin branch is scanned for commits with
+.Dq Fixes: ,
+.Dq MFC-with: ,
+or
+.Dq MFC with:
+tags referencing any of the specified revisions, and those fixup commits
+are automatically included in the correct order.
+.Pp
+If a specified revision has been reverted on the origin branch,
+.Nm
+refuses to cherry-pick it unless
+.Fl -ignore-reverts
+is given.
+Fixup commits that have been reverted are skipped with a warning.
+.Pp
+Commits that have already been cherry-picked to the current branch are
+automatically skipped.
+This allows the same
+.Nm
+command to be re-run after resolving a conflict without re-applying
+earlier commits.
+Use
+.Fl f
+to override this behavior.
+.It
+In
+.Fl -pending
+mode, no revisions are specified.
+Instead,
+.Nm
+scans the origin branch for commits with
+.Dq MFC after:
+tags whose waiting period has elapsed and which have not already been
+cherry-picked to the current branch.
+Commits that have been reverted upstream are excluded.
+By default, only commits authored by the current user
+.Pq as determined by Xr git-config 1
+are shown.
+.It
+In
+.Fl -dangling
+mode, no revisions are specified.
+Instead,
+.Nm
+finds commits in the current branch that have been cherry-picked from the
+origin branch but are missing associated fixup commits.
+Fixup commits that have been reverted upstream are excluded.
+By default, only cherry-picks authored by the current user
+.Pq as determined by Xr git-config 1
+are considered.
+.El
+.Pp
+The following options are available:
+.Bl -tag -width "-a author"
+.It Fl a Ar author , Fl -author Ar author
+With
+.Fl -pending
+or
+.Fl -dangling ,
+filter results to commits whose author email contains the given string.
+By default, the value of
+.Va user.email
+from
+.Xr git-config 1
+is used.
+.It Fl -all
+With
+.Fl -pending
+or
+.Fl -dangling ,
+show commits from all authors instead of only the current user.
+.It Fl -dangling
+Find cherry-picked commits in the current branch that are missing fixup
+commits from the origin branch.
+Cannot be combined with
+.Fl -pending
+or revision arguments.
+.It Fl e , Fl -edit
+Open the commit message in an editor before committing each cherry-pick.
+Passed through to
+.Xr git-cherry-pick 1 .
+.It Fl f , Fl -force
+Cherry-pick commits even if they appear to have already been cherry-picked
+to the current branch.
+By default,
+.Nm
+skips commits that are already present.
+.It Fl -ignore-reverts
+Cherry-pick commits even if they have been reverted on the origin branch.
+Without this flag,
+.Nm
+exits with an error when a reverted commit is specified.
+.It Fl -baking
+With
+.Fl -pending ,
+also show commits whose
+.Dq MFC after:
+waiting period has not yet elapsed.
+These are annotated with the date they become ready.
+.It Fl n
+Dry run.
+List the commits that would be cherry-picked, but do not actually
+cherry-pick them.
+The remote is not fetched in this mode.
+.It Fl -origin Ar branch
+Specify the upstream branch name.
+The default is determined automatically from
+.Pa sys/conf/newvers.sh :
+.Dq main
+for STABLE branches and
+.Dq stable/ Ns Ar major
+for RELEASE branches.
+.It Fl -pending
+Show upstream commits with
+.Dq MFC after:
+tags that are ready to be cherry-picked.
+Cannot be combined with
+.Fl -dangling
+or revision arguments.
+.It Fl r Ar remote , Fl -remote Ar remote
+Specify the git remote to fetch from.
+The default is the currently checked-out branch's upstream remote.
+.El
+.Pp
+Before running any operation,
+.Nm
+ensures that the origin is up to date by fetching it from the specified remote.
+.Sh IMPLEMENTATION NOTES
+.Nm
+is implemented in Python and depends on the gitpython module.
+.Pp
+.Nm
+uses text strings in commit log messages to establish relationships between
+commits.
+It recognizes the following tags in commit log messages:
+.Bl -tag -width "MFC after:"
+.It Dq Fixes: Ar hash
+Indicates that this commit fixes the commit identified by
+.Ar hash .
+When MFCing the referenced commit,
+.Nm
+will automatically include this fixup commit.
+.It Dq MFC-with: Ar hash , Dq MFC with: Ar hash
+Equivalent to
+.Dq Fixes: ;
+indicates that this commit should be MFCed together with the referenced commit.
+.It Dq MFC after: Ar N Ar unit
+Indicates that this commit should be MFCed after the specified waiting period.
+Recognized units are
+.Dq day ,
+.Dq days ,
+.Dq week ,
+.Dq weeks ,
+.Dq month ,
+and
+.Dq months .
+Used by the
+.Fl -pending
+mode.
+.El
+.Pp
+.Nm
+also looks for strings of the form,
+.Dq This reverts commit Ar hash ,
+inserted by
+.Xr git-revert 1 ,
+and
+.Dq cherry picked from commit Ar hash ,
+inserted by
+.Xr git-cherry-pick 1 .
+.Sh EXAMPLES
+Cherry-pick a single commit to the current branch, automatically including
+any fixup commits:
+.Bd -literal -offset indent
+$ git mfc abc123
+.Ed
+.Pp
+Cherry-pick a range of commits:
+.Bd -literal -offset indent
+$ git mfc abc123~..def456
+.Ed
+.Pp
+Preview which commits would be cherry-picked without actually doing it:
+.Bd -literal -offset indent
+$ git mfc -n abc123
+.Ed
+.Pp
+Cherry-pick a commit, opening an editor to amend the commit message first:
+.Bd -literal -offset indent
+$ git mfc -e abc123
+.Ed
+.Pp
+List your commits that are ready for MFC:
+.Bd -literal -offset indent
+$ git mfc --pending
+.Ed
+.Pp
+List pending commits from all authors:
+.Bd -literal -offset indent
+$ git mfc --pending --all
+.Ed
+.Pp
+Include commits whose MFC-after period has not yet elapsed:
+.Bd -literal -offset indent
+$ git mfc --pending --baking
+.Ed
+.Pp
+List commits ready for MFC by a specific author:
+.Bd -literal -offset indent
+$ git mfc --pending -a markj@FreeBSD.org
+.Ed
+.Pp
+Find MFCs in the current branch that are missing fixup commits:
+.Bd -literal -offset indent
+$ git mfc --dangling
+.Ed
+.Pp
+Cherry-pick a commit that was reverted upstream:
+.Bd -literal -offset indent
+$ git mfc --ignore-reverts abc123
+.Ed
+.Pp
+Use a different remote and origin branch:
+.Bd -literal -offset indent
+$ git mfc -r freebsd --origin main abc123
+.Ed
+.Sh SEE ALSO
+.Xr git-cherry-pick 1 ,
+.Xr git-config 1 ,
+.Xr git-revert 1 ,
+.Xr development 7
+.Sh AUTHORS
+The
+.Nm
+utility was written by
+.An -nosplit
+.An Mark Johnston Aq Mt markj@FreeBSD.org .
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Jul 7, 4:26 AM (16 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34763935
Default Alt Text
D57845.id180979.diff (28 KB)
Attached To
Mode
D57845: tools: Add git-mfc
Attached
Detach File
Event Timeline
Log In to Comment