diff --git a/tools/tools/git/git-ghpr b/tools/tools/git/git-ghpr new file mode 100755 --- /dev/null +++ b/tools/tools/git/git-ghpr @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# +# git-ghpr - Merge commits from github pull requests to main +# +# Copyright (c) 2023 M. Warner Losh +# +# SPX-License-Identifier: MIT +# +# Some code taken verbatim (or lightly tweaked) from git-publish by +# Stefan Hajnoczi licensed under MIT. + +import glob +import datetime +import locale +import optparse +import os +import re +import subprocess +import sys + +VERSION = '0.0.1' +#git-publish+ + +# Encoding for command-line arguments +CMDLINE_ENCODING = locale.getpreferredencoding() + +# Encoding for communicating with the Git executable +GIT_ENCODING = 'utf-8' + +# Encoding for files that GIT_EDITOR can edit +TEXTFILE_ENCODING = CMDLINE_ENCODING + +# As a git alias it is helpful to be a single file script with no external +# dependencies, so these git command-line wrappers are used instead of +# python-git. + +class GitError(Exception): + pass + +class GitHookError(Exception): + pass + +def popen_lines(cmd, **kwargs): + '''Communicate with a Popen object and return a list of lines for stdout and stderr''' + stdout, stderr = cmd.communicate(**kwargs) + stdout = re.split('\r\n|\n',stdout.decode(GIT_ENCODING))[:-1] + stderr = re.split('\r\n|\n',stderr.decode(GIT_ENCODING))[:-1] + return stdout, stderr + +def _git_check(*args): + '''Run a git command and return a list of lines, may raise GitError''' + cmdstr = 'git ' + ' '.join(('"%s"' % arg if ' ' in arg else arg) for arg in args) + if VERBOSE: + print(cmdstr) + cmd = subprocess.Popen(['git'] + list(args), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = popen_lines(cmd) + if cmd.returncode != 0: + raise GitError('ERROR: %s\n%s' % (cmdstr, '\n'.join(stderr))) + return stdout + +def _git(*args): + '''Run a git command and return a list of lines, ignore errors''' + try: + return _git_check(*args) + except GitError: + # ignore git command errors + return [] + +def _git_with_stderr(*args): + '''Run a git command and return a list of lines for stdout and stderr''' + if VERBOSE: + print('git ' + ' '.join(args)) + cmd = subprocess.Popen(['git'] + list(args), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = popen_lines(cmd) + return stdout, stderr, cmd.returncode + +def git_get_current_branch(): + git_dir = git_get_git_dir() + rebase_dir = os.path.join(git_dir, 'rebase-merge') + if os.path.exists(rebase_dir): + branch_path = os.path.join(rebase_dir, 'head-name') + prefix = 'refs/heads/' + # Path names are encoded in UTF-8 normalization form C. + with open(branch_path, encoding=GIT_ENCODING) as f: + branch = f.read().strip() + if branch.startswith(prefix): + return branch[len(prefix):] + return branch + else: + return _git_check('symbolic-ref', '--short', 'HEAD')[0] + +GIT_TOPLEVEL = None +def git_get_toplevel_dir(): + global GIT_TOPLEVEL + if GIT_TOPLEVEL is None: + GIT_TOPLEVEL = _git_check('rev-parse', '--show-toplevel')[0] + return GIT_TOPLEVEL + +GIT_DIR = None +def git_get_git_dir(): + global GIT_DIR + if GIT_DIR is None: + GIT_DIR = _git('rev-parse', '--git-dir')[0] + return GIT_DIR + +def git_branch_exists(branch): + '''Check if the given branch exists''' + try: + _git_check('rev-parse', '-q', '--verify', branch) + return True + except GitError: + return False + +def setup(): + '''Add git alias in ~/.gitconfig''' + path = os.path.abspath(sys.argv[0]) + ret = subprocess.call(['git', 'config', '--global', + 'alias.ghpr', '!' + path]) + if ret == 0: + print('You can now use \'git ghpr\' like a built-in git command.') + +#git-publish- + +def git_checkout(*args): + '''Return status of the tree''' + return _git_check('checkout', *args) + +def parse_args(): + + parser = optparse.OptionParser(version='%%prog %s' % VERSION, + usage='%prog [options] -- [common format-patch options]', + description='Do all the MFC tweaks needed when merging from main to stable/XX.', + epilog='Please report bugs to Warner Losh .') + parser.add_option('-b', '--branch', dest='branch', default='PR', + help='branch to use [defaults PR]') + parser.add_option('--pr', dest='pr', default=None, + help='Pull request to land') + parser.add_option('--setup', dest='setup', action='store_true', default=False, + help='add git alias in ~/.gitconfig') + parser.add_option('--upstream', dest='upstream', default='freebsd', + help='Upstream remote name, defaults to freebsd') + parser.add_option('-v', '--verbose', dest='verbose', + action='store_true', default=False, + help='show executed git commands (useful for troubleshooting)') + + return parser.parse_args() + +class FetchError(Exception): + pass + +def fetch(url, dst): + '''Try to fetch the specified URL''' + cmdstr = 'fetch ' + '-o ' + dst + ' ' + url + if VERBOSE: + print(cmdstr) + cmd = subprocess.Popen(['fetch', '-o', dst, url], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = popen_lines(cmd) + if cmd.returncode != 0: + raise FetchError('ERROR: %s\n%s' % (cmdstr, '\n'.join(stderr))) + return stdout + +def main(): + global VERBOSE + + options, args = parse_args() + VERBOSE = options.verbose + + # Keep this before any operations that call out to git(1) so that setup + # works when the current working directory is outside a git repo. + if options.setup: + setup() + return 0 + + try: + git_get_toplevel_dir() + except GitError: + print('Unable to find git directory, are you sure you are in a git repo?') + return 1 + + if options.pr is None: + print('--pr is mandatory') + return 1 + + branch = 'PR-%s' % options.pr + if git_branch_exists(branch): + print('Still lamely requires a unique branch, and %s exists' % options.branch) + return 1 + + git_checkout('-b', branch, 'main') + tmp = '/tmp/%s.patch' % options.pr + fetch('https://patch-diff.githubusercontent.com/raw/freebsd/freebsd-src/pull/%s.patch' % options.pr, + tmp) + _git_check('am', '--ignore-date', tmp) + + # Now, rebase this to main to add the trailers we need, but with an editor + # that s/-/ / in those trailers because git doesn't like trailers with + # spaces in the key value. + _git_check('rebase', '-i', 'main', + '--exec', + 'env EDITOR=$HOME/bin/git-fixup-editor git commit --amend --trailer "Reviewed-by: imp" --trailer "Pull-Request: https://github.com/freebsd/freebsd-src/pull/%s"' % options.pr, + ) + print('I claim PR %s is now a series of commits ready for closer review' % options.pr) + +if __name__ == '__main__': + sys.exit(main())