| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # |
| # Copyright 2017 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """This is a tool for picking patches from upstream and applying them.""" |
| |
| """echo""" "This is a python script! Don't interpret it with bash." # pylint: disable=pointless-string-statement |
| """exit""" # pylint: disable=pointless-string-statement |
| |
| # pylint: disable=wrong-import-position |
| import argparse |
| from collections import OrderedDict |
| import configparser |
| import functools |
| import mailbox |
| import os |
| import pprint |
| import re |
| import signal |
| import socket |
| import ssl |
| import subprocess |
| import sys |
| import textwrap |
| import urllib.request |
| import xmlrpc.client |
| |
| |
| errprint = functools.partial(print, file=sys.stderr) |
| |
| # pylint: disable=line-too-long |
| # Note: Do not include trailing / in any of these |
| UPSTREAM_URLS = ( |
| # Acceptable Linux URLs |
| "git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git", |
| "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git", |
| "https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git", |
| # Acceptable Linux Firmware URLs |
| "git://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git", |
| "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git", |
| "https://kernel.googlesource.com/pub/scm/linux/kernel/git/firmware/linux-firmware.git", |
| # Upstream for various other projects |
| "git://w1.fi/srv/git/hostap.git", |
| "git://git.kernel.org/pub/scm/bluetooth/bluez.git", |
| "https://github.com/andersson/qrtr.git", |
| "https://review.coreboot.org/flashrom.git", |
| ) |
| |
| PATCHWORK_URLS = ( |
| "https://patchwork.kernel.org", |
| "https://patchwork.ozlabs.org", |
| "https://patchwork.freedesktop.org", |
| ) |
| |
| COMMIT_MESSAGE_WIDTH = 75 |
| |
| _PWCLIENTRC = os.path.expanduser("~/.pwclientrc") |
| |
| |
| def _git(args, stdin=None, encoding="utf-8", no_stderr=False): |
| """Calls a git subcommand. |
| |
| Similar to subprocess.check_output. |
| |
| Args: |
| args: subcommand + args passed to 'git'. |
| stdin: a string or bytes (depending on encoding) that will be passed |
| to the git subcommand. |
| encoding: either 'utf-8' (default) or None. Override it to None if |
| you want both stdin and stdout to be raw bytes. |
| no_stderr: If True, we'll eat stderr |
| |
| Returns: |
| the stdout of the git subcommand, same type as stdin. The output is |
| also run through strip to make sure there's no extra whitespace. |
| |
| Raises: |
| subprocess.CalledProcessError: when return code is not zero. |
| The exception has a .returncode attribute. |
| """ |
| return subprocess.run( |
| ["git"] + args, |
| encoding=encoding, |
| input=stdin, |
| stdout=subprocess.PIPE, |
| stderr=(subprocess.PIPE if no_stderr else None), |
| check=True, |
| ).stdout.strip() |
| |
| |
| def _git_returncode(*args, **kwargs): |
| """Same as _git, but return returncode instead of stdout. |
| |
| Similar to subprocess.call. |
| |
| Never raises subprocess.CalledProcessError. |
| """ |
| try: |
| _git(*args, **kwargs) |
| return 0 |
| except subprocess.CalledProcessError as e: |
| return e.returncode |
| |
| |
| def _get_conflicts(): |
| """Report conflicting files.""" |
| resolutions = ("DD", "AU", "UD", "UA", "DU", "AA", "UU") |
| conflicts = [] |
| output = _git(["status", "--porcelain", "--untracked-files=no"]) |
| for line in output.splitlines(): |
| if not line: |
| continue |
| resolution, name = line.split(None, 1) |
| if resolution in resolutions: |
| conflicts.append(" " + name) |
| if not conflicts: |
| return "" |
| return "\nConflicts:\n%s\n" % "\n".join(conflicts) |
| |
| |
| def _find_upstream_remote(urls): |
| """Find a remote pointing to an upstream repository.""" |
| for remote in _git(["remote"]).splitlines(): |
| try: |
| if _git(["remote", "get-url", remote]).rstrip("/") in urls: |
| return remote |
| except subprocess.CalledProcessError: |
| # Kinda weird, get-url failing on an item that git just gave us. |
| continue |
| return None |
| |
| |
| def _pause_for_merge(conflicts): |
| """Pause and go in the background till user resolves the conflicts.""" |
| |
| git_root = _git(["rev-parse", "--show-toplevel"]) |
| previous_head_hash = _git(["rev-parse", "HEAD"]) |
| |
| paths = ( |
| os.path.join(git_root, ".git", "rebase-apply"), |
| os.path.join(git_root, ".git", "CHERRY_PICK_HEAD"), |
| ) |
| for path in paths: |
| if os.path.exists(path): |
| errprint('Found "%s".' % path) |
| errprint(conflicts) |
| errprint( |
| "Please resolve the conflicts and restart the " |
| "shell job when done. Kill this job if you " |
| "aborted the conflict." |
| ) |
| os.kill(os.getpid(), signal.SIGTSTP) |
| |
| # Check the conflicts actually got resolved. Otherwise we'll end up |
| # modifying the wrong commit message and probably confusing people. |
| while previous_head_hash == _git(["rev-parse", "HEAD"]): |
| errprint( |
| "Error: no new commit has been made. Did you forget to run " |
| "`git am --continue` or `git cherry-pick --continue`?" |
| ) |
| errprint( |
| "Please create a new commit and restart the shell job (or kill" |
| " it if you aborted the conflict)." |
| ) |
| os.kill(os.getpid(), signal.SIGTSTP) |
| |
| |
| def _get_pw_url(project): |
| """Retrieve the patchwork server URL from .pwclientrc. |
| |
| Args: |
| project: patchwork project name; if None, we retrieve the default |
| from pwclientrc |
| """ |
| config = configparser.ConfigParser() |
| config.read([_PWCLIENTRC]) |
| |
| if project is None: |
| try: |
| project = config.get("options", "default") |
| except (configparser.NoSectionError, configparser.NoOptionError) as e: |
| errprint( |
| "Error: no default patchwork project found in %s. (%r)" |
| % (_PWCLIENTRC, e) |
| ) |
| sys.exit(1) |
| |
| if not config.has_option(project, "url"): |
| errprint("Error: patchwork URL not found for project '%s'" % project) |
| sys.exit(1) |
| |
| url = config.get(project, "url") |
| # Strip trailing 'xmlrpc' and/or trailing slash. |
| return re.sub("/(xmlrpc/)?$", "", url) |
| |
| |
| def _wrap_commit_line(prefix, content): |
| line = prefix + content |
| indent = " " * len(prefix) |
| |
| ret = textwrap.fill(line, COMMIT_MESSAGE_WIDTH, subsequent_indent=indent) |
| return ret[len(prefix) :] |
| |
| |
| def _get_patch_from_message_id(args, message_id): |
| for (url_template, mbox_suffix) in [ |
| ("https://lore.kernel.org/r/%s", "raw"), |
| # hostap project (and others) are here, but not kernel.org. |
| ("https://marc.info/?i=%s", "&q=mbox"), |
| # public-inbox comes last as a "default"; it has a nice error page |
| # pointing to other redirectors, even if it doesn't have what |
| # you're looking for directly. |
| ("https://public-inbox.org/git/%s", None), |
| ]: |
| this_url = url_template % message_id |
| if args["debug"]: |
| print("Probing archive for message at: %s" % this_url) |
| try: |
| opener = urllib.request.urlopen(this_url) |
| |
| if (opener.url != this_url) and mbox_suffix: |
| # To actually get the patch from some servers we need to: |
| # - Let it redirect us to an actual list it's tracking. |
| # - Add a suffix |
| opener = urllib.request.urlopen(opener.url + mbox_suffix) |
| elif opener.url == this_url and "marc.info" in url_template: |
| # On "marc.info" if there are more than one hit then it _won't_ |
| # redirect us and instead will show a disambiguation page. |
| # We'll use "re" to scrape. This is a bit fragile but |
| # marc.info doesn't seem to have any API to help us with this. |
| data = opener.read().decode('utf-8') |
| mo = re.search(r'<a href="\?l=([^&]*)\&m=([^"]*)', data) |
| if mo is not None: |
| l, m = mo.groups() |
| errprint("Warning: multiple hits on marc.info; picking %s" % l) |
| new_url = "https://marc.info/?l=%s&m=%s&q=mbox" % (l, m) |
| opener = urllib.request.urlopen(new_url) |
| |
| except urllib.error.HTTPError as e: |
| # Skip all HTTP errors. We can expect 404 for archives that |
| # don't have this MessageId, or 300 for public-inbox ("not |
| # found, but try these other redirects"). It's less clear what |
| # to do with transitory (or is it permanent?) server failures. |
| if args["debug"]: |
| print("Skipping URL %s, error: %s" % (this_url, e)) |
| continue |
| # Success! |
| if args["debug"]: |
| print("Found at %s" % this_url) |
| return this_url, opener.read() |
| errprint( |
| "WARNING: couldn't find working MessageId URL; " |
| 'defaulting to "%s"' % this_url |
| ) |
| return this_url, None |
| |
| |
| def _pick_fromlist(url, patch_id, args, message_id=None): |
| if args["tag"] is None: |
| args["tag"] = "FROMLIST: " |
| |
| if patch_id is not None: |
| try: |
| opener = urllib.request.urlopen( |
| "%s/patch/%s/mbox" % (url, patch_id) |
| ) |
| except urllib.error.HTTPError as e: |
| errprint("Error: could not download patch: %s" % e) |
| sys.exit(1) |
| patch_contents = opener.read() |
| |
| if not patch_contents: |
| errprint("Error: No patch content found") |
| sys.exit(1) |
| |
| message_id = mailbox.Message(patch_contents)["Message-Id"] |
| message_id = re.sub("^<|>$", "", message_id.strip()) |
| alt_url = None |
| elif message_id is not None: |
| alt_url, patch_contents = _get_patch_from_message_id(args, message_id) |
| if patch_contents is None: |
| errprint("Error: could not find patch based on message id") |
| sys.exit(1) |
| else: |
| errprint("Error: Need a message ID or a patchwork ID") |
| sys.exit(1) |
| |
| if args["source_line"] is None: |
| if alt_url is None: |
| alt_url, _ = _get_patch_from_message_id(args, message_id) |
| |
| if patch_id is not None: |
| args["source_line"] = "(am from %s/patch/%s/)" % (url, patch_id) |
| args["source_line"] += "\n(also found at %s)" % alt_url |
| else: |
| args["source_line"] = "(am from %s)" % alt_url |
| |
| # Auto-snarf the Change-Id if it was encoded into the Message-Id. |
| mo = re.match(r".*(I[a-f0-9]{40})@changeid$", message_id) |
| if mo and args["changeid"] is None: |
| args["changeid"] = mo.group(1) |
| |
| if args["replace"]: |
| _git(["reset", "--hard", "HEAD~1"]) |
| |
| return _git_returncode(["am", "-3"], stdin=patch_contents, encoding=None) |
| |
| |
| def _match_patchwork(match, args): |
| """Match location: pw://### or pw://PROJECT/###.""" |
| pw_project = match.group(2) |
| patch_id = match.group(3) |
| |
| if args["debug"]: |
| print( |
| "_match_patchwork: pw_project=%s, patch_id=%s" |
| % (pw_project, patch_id) |
| ) |
| |
| url = _get_pw_url(pw_project) |
| return _pick_fromlist(url, patch_id, args) |
| |
| |
| def _match_msgid(match, args): |
| """Match location: msgid://MSGID.""" |
| msgid = match.group(1) |
| |
| if args["debug"]: |
| print("_match_msgid: message_id=%s" % (msgid)) |
| |
| url = None |
| for url in PATCHWORK_URLS: |
| rpc = xmlrpc.client.ServerProxy(url + "/xmlrpc/") |
| try: |
| # Patchwork requires the brackets so force it |
| res = rpc.patch_list({"msgid": "<" + msgid + ">"}) |
| except ssl.SSLCertVerificationError: |
| errprint('Error: server "%s" gave an SSL error, skipping' % url) |
| continue |
| except socket.gaierror as e: |
| errprint( |
| 'Error: server "%s" gave socket error "%s", skipping' % (url, e) |
| ) |
| if res: |
| patch_id = res[0]["id"] |
| break |
| else: |
| # We can't find a patch ID for this message but we won't give up. |
| # Some patches might not be on any (known) patchwork servers but |
| # can still be found on mailing list archives. |
| patch_id = None |
| errprint( |
| """Warning: could not find msgid "%s" |
| on a known patchwork instance. This could be a Patchwork issue (dropped or |
| delayed patch?), or this script may need updated to point at a relevant |
| Patchwork server.""" |
| % msgid |
| ) |
| |
| return _pick_fromlist(url, patch_id, args, msgid) |
| |
| |
| def _upstream(commit, urls, args): |
| if args["debug"]: |
| print("_upstream: commit=%s" % commit) |
| |
| # Confirm an upstream remote is setup. |
| remote = _find_upstream_remote(urls) |
| if not remote: |
| errprint("Error: need a valid upstream remote") |
| sys.exit(1) |
| |
| branches = ["main", "master"] |
| for branch in branches: |
| remote_ref = "%s/%s" % (remote, branch) |
| try: |
| _git( |
| ["merge-base", "--is-ancestor", commit, remote_ref], |
| no_stderr=True, |
| ) |
| except subprocess.CalledProcessError: |
| continue |
| break |
| else: |
| errprint( |
| "Error: Commit not in %s, branches: %s" |
| % (remote, ", ".join(branches)) |
| ) |
| sys.exit(1) |
| |
| if args["source_line"] is None: |
| commit = _git(["rev-parse", commit]) |
| args["source_line"] = "(cherry picked from commit %s)" % (commit) |
| if args["tag"] is None: |
| args["tag"] = "UPSTREAM: " |
| |
| if args["replace"]: |
| _git(["reset", "--hard", "HEAD~1"]) |
| |
| return _git_returncode(["cherry-pick", commit]) |
| |
| |
| def _match_upstream(match, args): |
| """Match location: linux://HASH and upstream://HASH.""" |
| commit = match.group(1) |
| return _upstream(commit, urls=UPSTREAM_URLS, args=args) |
| |
| |
| def _match_fromgit(match, args): |
| """Match location: git://remote/branch/HASH.""" |
| remote = match.group(2) |
| branch = match.group(3) |
| commit = match.group(4) |
| |
| if args["debug"]: |
| print( |
| "_match_fromgit: remote=%s branch=%s commit=%s" |
| % (remote, branch, commit) |
| ) |
| |
| try: |
| _git( |
| ["merge-base", "--is-ancestor", commit, "%s/%s" % (remote, branch)] |
| ) |
| except subprocess.CalledProcessError: |
| errprint("Error: Commit not in %s/%s" % (remote, branch)) |
| sys.exit(1) |
| |
| url = _git(["remote", "get-url", remote]) |
| |
| if args["source_line"] is None: |
| commit = _git(["rev-parse", commit]) |
| args["source_line"] = "(cherry picked from commit %s\n %s %s)" % ( |
| commit, |
| url, |
| branch, |
| ) |
| if args["tag"] is None: |
| args["tag"] = "FROMGIT: " |
| |
| if args["replace"]: |
| _git(["reset", "--hard", "HEAD~1"]) |
| |
| return _git_returncode(["cherry-pick", commit]) |
| |
| |
| def _match_gitfetch(match, args): |
| """Match location: (git|https)://repoURL#branch/HASH.""" |
| remote = match.group(1) |
| branch = match.group(3) |
| commit = match.group(4) |
| |
| if args["debug"]: |
| print( |
| "_match_gitfetch: remote=%s branch=%s commit=%s" |
| % (remote, branch, commit) |
| ) |
| |
| try: |
| _git(["fetch", remote, branch]) |
| except subprocess.CalledProcessError: |
| errprint("Error: Branch not in %s" % remote) |
| sys.exit(1) |
| |
| url = remote |
| |
| if args["source_line"] is None: |
| commit = _git(["rev-parse", commit]) |
| args["source_line"] = "(cherry picked from commit %s\n %s %s)" % ( |
| commit, |
| url, |
| branch, |
| ) |
| if args["tag"] is None: |
| args["tag"] = "FROMGIT: " |
| |
| if args["replace"]: |
| _git(["reset", "--hard", "HEAD~1"]) |
| |
| return _git_returncode(["cherry-pick", commit]) |
| |
| |
| def _match_gitweb(match, args): |
| """Match location: https://repoURL/commit/?h=branch&id=HASH. or https://repoURL/commit/?id=HASH.""" |
| remote = match.group(1) |
| branch = match.group(3) |
| commit = match.group(4) |
| |
| if args["debug"]: |
| print( |
| "_match_gitweb: remote=%s branch=%s commit=%s" |
| % (remote, branch, commit) |
| ) |
| |
| try: |
| if branch: |
| _git(["fetch", remote, branch]) |
| else: |
| _git(["fetch", remote]) |
| except subprocess.CalledProcessError: |
| errprint("Error: Branch not in %s" % remote) |
| sys.exit(1) |
| |
| url = remote |
| |
| if args["source_line"] is None: |
| commit = _git(["rev-parse", commit]) |
| args["source_line"] = "(cherry picked from commit %s\n %s %s)" % ( |
| commit, |
| url, |
| branch, |
| ) |
| if args["tag"] is None: |
| args["tag"] = "FROMGIT: " |
| |
| if args["replace"]: |
| _git(["reset", "--hard", "HEAD~1"]) |
| |
| return _git_returncode(["cherry-pick", commit]) |
| |
| |
| def _remove_dup_bugs(bugs): |
| """Remove the duplicated bugs from a string keeping the original order.""" |
| |
| # Standardize all the spacing around bugs |
| bugs = re.sub(r"\s*,\s*", ", ", bugs) |
| |
| # Create a list of bugs |
| bugs = bugs.split(", ") |
| |
| # Remove duplicates keeping order |
| bugs = list(OrderedDict.fromkeys(bugs).keys()) |
| |
| # Convert into a string again |
| bugs = ", ".join(bugs) |
| |
| return bugs |
| |
| |
| def main(args): |
| """This is the main entrypoint for fromupstream. |
| |
| Args: |
| args: sys.argv[1:] |
| |
| Returns: |
| An int return code. |
| """ |
| parser = argparse.ArgumentParser() |
| |
| parser.add_argument( |
| "--bug", "-b", action="append", default=[], type=str, help="BUG= line" |
| ) |
| parser.add_argument( |
| "--test", "-t", action="append", default=[], type=str, help="TEST= line" |
| ) |
| parser.add_argument( |
| "--crbug", |
| action="append", |
| default=[], |
| type=int, |
| help="BUG=chromium: line", |
| ) |
| parser.add_argument( |
| "--buganizer", action="append", default=[], type=int, help="BUG=b: line" |
| ) |
| parser.add_argument( |
| "--changeid", "-c", help="Overrides the gerrit generated Change-Id line" |
| ) |
| parser.add_argument("--cqdepend", type=str, help="Cq-Depend: line") |
| parser.add_argument("--upstream-task", "-u", help="Set UPSTREAM-TASK") |
| |
| parser.add_argument( |
| "--replace", |
| "-r", |
| action="store_true", |
| help="Replaces the HEAD commit with this one, taking " |
| "its properties(BUG, TEST, Change-Id). Useful for " |
| "updating commits.", |
| ) |
| parser.add_argument("--nosignoff", dest="signoff", action="store_false") |
| parser.add_argument( |
| "--debug", "-d", action="store_true", help="Prints more verbose logs." |
| ) |
| |
| parser.add_argument("--tag", help="Overrides the tag from the title") |
| parser.add_argument( |
| "--source", |
| "-s", |
| dest="source_line", |
| type=str, |
| help="Overrides the source line, last line, ex: " |
| "(am from http://....)", |
| ) |
| parser.add_argument( |
| "locations", |
| nargs="+", |
| help="Patchwork ID (pw://### or pw://PROJECT/###, " |
| "where PROJECT is defined in ~/.pwclientrc; if no " |
| "PROJECT is specified, the default is retrieved from " |
| "~/.pwclientrc), " |
| "Message-ID (msgid://MSGID), " |
| "linux commit like linux://HASH, " |
| "upstream commit like upstream://HASH, or " |
| "git reference like git://remote/branch/HASH or " |
| "git://repoURL#branch/HASH or " |
| "https://repoURL#branch/HASH or " |
| "'https://repoURL/commit/?h=branch&id=HASH'", |
| ) |
| |
| args = vars(parser.parse_args(args)) |
| |
| cq_depends = [args["cqdepend"]] if args["cqdepend"] else [] |
| |
| bugs = args["bug"] |
| bugs += ["b:%d" % x for x in args["buganizer"]] |
| bugs += ["chromium:%d" % x for x in args["crbug"]] |
| bugs = ", ".join(bugs) |
| bugs = _remove_dup_bugs(bugs) |
| if bugs: |
| bug_lines = [ |
| x.strip(" ,") for x in _wrap_commit_line("BUG=", bugs).split("\n") |
| ] |
| else: |
| bug_lines = [] |
| |
| test_lines = [_wrap_commit_line("TEST=", x) for x in args["test"]] |
| |
| upstream_task = args.get("upstream_task") |
| if upstream_task and not upstream_task.startswith("b:"): |
| upstream_task = "b:%s" % upstream_task |
| |
| if args["replace"]: |
| old_commit_message = _git(["show", "-s", "--format=%B", "HEAD"]) |
| |
| # It is possible that multiple Change-Ids are in the commit message |
| # (due to cherry picking). We only want to pull out the first one. |
| changeid_match = re.search( |
| "^Change-Id: (.*)$", old_commit_message, re.MULTILINE |
| ) |
| if args["changeid"] is None and changeid_match: |
| args["changeid"] = changeid_match.group(1) |
| |
| if not cq_depends: |
| cq_depends = re.findall( |
| r"^Cq-Depend:\s+(.*)$", old_commit_message, re.MULTILINE |
| ) |
| |
| if not bug_lines: |
| bug_lines = re.findall( |
| r"^BUG=(.*)$", old_commit_message, re.MULTILINE |
| ) |
| |
| if not test_lines: |
| # Note: use (?=...) to avoid to consume the source string |
| test_lines = re.findall( |
| r""" |
| ^TEST=(.*?) # Match start from TEST= until |
| \n # (to remove the tailing newlines) |
| (?=^$| # a blank line |
| ^Cq-Depend:| # or Cq-Depend: |
| ^Change-Id:| # or Change-Id: |
| ^BUG=| # or following BUG= |
| ^TEST=| # or another TEST= |
| ^UPSTREAM-TASK=) # or UPSTREAM-TASK |
| """, |
| old_commit_message, |
| re.MULTILINE | re.DOTALL | re.VERBOSE, |
| ) |
| |
| upstream_task_match = re.search( |
| "^UPSTREAM-TASK=(.*)$", old_commit_message, re.MULTILINE |
| ) |
| if upstream_task is None and upstream_task_match: |
| upstream_task = upstream_task_match.group(1) |
| |
| if not bug_lines or not test_lines: |
| parser.error( |
| "BUG=/TEST= lines are required; --replace can help " |
| "automate, or set via --bug/--test" |
| ) |
| |
| if args["debug"]: |
| pprint.pprint(args) |
| |
| re_matches = ( |
| (re.compile(r"^pw://(([^/]+)/)?(.+)"), _match_patchwork), |
| (re.compile(r"^msgid://<?([^>]*)>?"), _match_msgid), |
| (re.compile(r"^linux://([0-9a-f]+)"), _match_upstream), |
| (re.compile(r"^upstream://([0-9a-f]+)"), _match_upstream), |
| ( |
| re.compile(r"^(from)?git://([^/\#]+)/([^#]+)/([0-9a-f]+)$"), |
| _match_fromgit, |
| ), |
| (re.compile(r"^((git|https)://.+)#(.+)/([0-9a-f]+)$"), _match_gitfetch), |
| ( |
| re.compile(r"^(https://.+)/commit/\?(h=(.+)\&)?id=([0-9a-f]+)$"), |
| _match_gitweb, |
| ), |
| ) |
| |
| # Backup user provided parameters |
| user_source_line = args["source_line"] |
| user_tag = args["tag"] |
| user_changeid = args["changeid"] |
| |
| for location in args["locations"]: |
| # Restore user parameters |
| args["source_line"] = user_source_line |
| args["tag"] = user_tag |
| args["changeid"] = user_changeid |
| |
| if args["debug"]: |
| print("location=%s" % location) |
| |
| for reg, handler in re_matches: |
| match = reg.match(location) |
| if match: |
| ret = handler(match, args) |
| break |
| else: |
| errprint('Don\'t know what "%s" means.' % location) |
| sys.exit(1) |
| |
| if ret != 0: |
| conflicts = _get_conflicts() |
| if args["tag"] == "UPSTREAM: ": |
| args["tag"] = "BACKPORT: " |
| else: |
| args["tag"] = "BACKPORT: " + args["tag"] |
| _pause_for_merge(conflicts) |
| else: |
| conflicts = "" |
| |
| # extract commit message |
| commit_message = _git(["show", "-s", "--format=%B", "HEAD"]) |
| |
| # Remove stray Change-Id, most likely from merge resolution |
| commit_message = re.sub(r"Change-Id:.*\n?", "", commit_message) |
| |
| # Note the source location before tagging anything else |
| commit_message += "\n" + args["source_line"] |
| |
| # add automatic Change ID, BUG, and TEST (and maybe signoff too) so |
| # next commands know where to work on |
| commit_message += "\n" |
| commit_message += conflicts |
| if upstream_task: |
| commit_message += "\nUPSTREAM-TASK=%s" % upstream_task |
| commit_message += "\n" |
| commit_message += "\n".join("BUG=%s" % bug for bug in bug_lines) |
| commit_message += "\n" |
| commit_message += "\n".join("TEST=%s" % t for t in test_lines) |
| |
| extra = [] |
| if args["signoff"]: |
| signoff = "Signed-off-by: %s <%s>" % ( |
| _git(["config", "user.name"]), |
| _git(["config", "user.email"]), |
| ) |
| if not signoff in commit_message.splitlines(): |
| extra += ["-s"] |
| _git(["commit"] + extra + ["--amend", "-F", "-"], stdin=commit_message) |
| |
| # re-extract commit message |
| commit_message = _git(["show", "-s", "--format=%B", "HEAD"]) |
| |
| # If we see a "Link: " that seems to point to a Message-Id with an |
| # automatic Change-Id we'll snarf it out. |
| mo = re.search( |
| r"^Link:.*(I[a-f0-9]{40})@changeid", commit_message, re.MULTILINE |
| ) |
| if mo and args["changeid"] is None: |
| args["changeid"] = mo.group(1) |
| |
| # replace changeid if needed |
| if args["changeid"] is not None: |
| commit_message = re.sub( |
| r"(Change-Id: )(\w+)", |
| r"\1%s" % args["changeid"], |
| commit_message, |
| ) |
| args["changeid"] = None |
| |
| if cq_depends: |
| commit_message = re.sub( |
| r"(Change-Id: \w+)", |
| r"%s\n\1" % "\n".join("Cq-Depend: %s" % c for c in cq_depends), |
| commit_message, |
| ) |
| |
| # decorate it that it's from outside |
| commit_message = args["tag"] + commit_message |
| |
| # commit everything |
| _git(["commit", "--amend", "-F", "-"], stdin=commit_message) |
| |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |