зеркало из https://github.com/mozilla/gecko-dev.git
256 строки
8.3 KiB
Python
256 строки
8.3 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, # You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
import logging
|
|
|
|
from mach.decorators import (
|
|
CommandArgument,
|
|
CommandProvider,
|
|
Command,
|
|
)
|
|
|
|
from mozbuild.base import MachCommandBase
|
|
|
|
import mozpack.path as mozpath
|
|
|
|
import json
|
|
|
|
GITHUB_ROOT = "https://github.com/"
|
|
PR_REPOSITORIES = {
|
|
"webrender": {
|
|
"github": "servo/webrender",
|
|
"path": "gfx/wr",
|
|
"bugzilla_product": "Core",
|
|
"bugzilla_component": "Graphics: WebRender",
|
|
},
|
|
"webgpu": {
|
|
"github": "gfx-rs/wgpu",
|
|
"path": "gfx/wgpu",
|
|
"bugzilla_product": "Core",
|
|
"bugzilla_component": "Graphics: WebGPU",
|
|
},
|
|
"debugger": {
|
|
"github": "firefox-devtools/debugger",
|
|
"path": "devtools/client/debugger",
|
|
"bugzilla_product": "DevTools",
|
|
"bugzilla_component": "Debugger",
|
|
},
|
|
}
|
|
|
|
|
|
@CommandProvider
|
|
class PullRequestImporter(MachCommandBase):
|
|
@Command(
|
|
"import-pr",
|
|
category="misc",
|
|
description="Import a pull request from Github to the local repo.",
|
|
)
|
|
@CommandArgument(
|
|
"-b", "--bug-number", help="Bug number to use in the commit messages."
|
|
)
|
|
@CommandArgument(
|
|
"-t",
|
|
"--bugzilla-token",
|
|
help="Bugzilla API token used to file a new bug if no bug number is "
|
|
"provided.",
|
|
)
|
|
@CommandArgument(
|
|
"-r", "--reviewer", help="Reviewer nick to apply to commit messages."
|
|
)
|
|
@CommandArgument(
|
|
"pull_request",
|
|
help="URL to the pull request to import (e.g. "
|
|
"https://github.com/servo/webrender/pull/3665).",
|
|
)
|
|
def import_pr(
|
|
self, pull_request, bug_number=None, bugzilla_token=None, reviewer=None
|
|
):
|
|
import requests
|
|
|
|
pr_number = None
|
|
repository = None
|
|
for r in PR_REPOSITORIES.values():
|
|
if pull_request.startswith(GITHUB_ROOT + r["github"] + "/pull/"):
|
|
# sanitize URL, dropping anything after the PR number
|
|
pr_number = int(re.search("/pull/([0-9]+)", pull_request).group(1))
|
|
pull_request = GITHUB_ROOT + r["github"] + "/pull/" + str(pr_number)
|
|
repository = r
|
|
break
|
|
|
|
if repository is None:
|
|
self.log(
|
|
logging.ERROR,
|
|
"unrecognized_repo",
|
|
{},
|
|
"The pull request URL was not recognized; add it to the list of "
|
|
"recognized repos in PR_REPOSITORIES in %s" % __file__,
|
|
)
|
|
sys.exit(1)
|
|
|
|
self.log(
|
|
logging.INFO,
|
|
"import_pr",
|
|
{"pr_url": pull_request},
|
|
"Attempting to import {pr_url}",
|
|
)
|
|
dirty = [
|
|
f
|
|
for f in self.repository.get_changed_files(mode="all")
|
|
if f.startswith(repository["path"])
|
|
]
|
|
if dirty:
|
|
self.log(
|
|
logging.ERROR,
|
|
"dirty_tree",
|
|
repository,
|
|
"Local {path} tree is dirty; aborting!",
|
|
)
|
|
sys.exit(1)
|
|
target_dir = mozpath.join(self.topsrcdir, os.path.normpath(repository["path"]))
|
|
|
|
if bug_number is None:
|
|
if bugzilla_token is None:
|
|
self.log(
|
|
logging.WARNING,
|
|
"no_token",
|
|
{},
|
|
"No bug number or bugzilla API token provided; bug number will not "
|
|
"be added to commit messages.",
|
|
)
|
|
else:
|
|
bug_number = self._file_bug(bugzilla_token, repository, pr_number)
|
|
elif bugzilla_token is not None:
|
|
self.log(
|
|
logging.WARNING,
|
|
"too_much_bug",
|
|
{},
|
|
"Providing a bugzilla token is unnecessary when a bug number is provided. "
|
|
"Using bug number; ignoring token.",
|
|
)
|
|
|
|
pr_patch = requests.get(pull_request + ".patch")
|
|
pr_patch.raise_for_status()
|
|
for patch in self._split_patches(
|
|
pr_patch.content, bug_number, pull_request, reviewer
|
|
):
|
|
self.log(
|
|
logging.INFO,
|
|
"commit_msg",
|
|
patch,
|
|
"Processing commit [{commit_summary}] by [{author}] at [{date}]",
|
|
)
|
|
patch_cmd = subprocess.Popen(
|
|
["patch", "-p1", "-s"], stdin=subprocess.PIPE, cwd=target_dir
|
|
)
|
|
patch_cmd.stdin.write(patch["diff"].encode("utf-8"))
|
|
patch_cmd.stdin.close()
|
|
patch_cmd.wait()
|
|
if patch_cmd.returncode != 0:
|
|
self.log(
|
|
logging.ERROR,
|
|
"commit_fail",
|
|
{},
|
|
'Error applying diff from commit via "patch -p1 -s". Aborting...',
|
|
)
|
|
sys.exit(patch_cmd.returncode)
|
|
self.repository.commit(
|
|
patch["commit_msg"], patch["author"], patch["date"], [target_dir]
|
|
)
|
|
self.log(logging.INFO, "commit_pass", {}, "Committed successfully.")
|
|
|
|
def _file_bug(self, token, repo, pr_number):
|
|
import requests
|
|
|
|
bug = requests.post(
|
|
"https://bugzilla.mozilla.org/rest/bug?api_key=%s" % token,
|
|
json={
|
|
"product": repo["bugzilla_product"],
|
|
"component": repo["bugzilla_component"],
|
|
"summary": "Land %s#%s in mozilla-central"
|
|
% (repo["github"], pr_number),
|
|
"version": "unspecified",
|
|
},
|
|
)
|
|
bug.raise_for_status()
|
|
self.log(logging.DEBUG, "new_bug", {}, bug.content)
|
|
bugnumber = json.loads(bug.content)["id"]
|
|
self.log(
|
|
logging.INFO, "new_bug", {"bugnumber": bugnumber}, "Filed bug {bugnumber}"
|
|
)
|
|
return bugnumber
|
|
|
|
def _split_patches(self, patchfile, bug_number, pull_request, reviewer):
|
|
INITIAL = 0
|
|
HEADERS = 1
|
|
STAT_AND_DIFF = 2
|
|
|
|
patch = b""
|
|
state = INITIAL
|
|
for line in patchfile.splitlines():
|
|
if state == INITIAL:
|
|
if line.startswith(b"From "):
|
|
state = HEADERS
|
|
elif state == HEADERS:
|
|
patch += line + b"\n"
|
|
if line == b"---":
|
|
state = STAT_AND_DIFF
|
|
elif state == STAT_AND_DIFF:
|
|
if line.startswith(b"From "):
|
|
yield self._parse_patch(patch, bug_number, pull_request, reviewer)
|
|
patch = b""
|
|
state = HEADERS
|
|
else:
|
|
patch += line + b"\n"
|
|
if len(patch) > 0:
|
|
yield self._parse_patch(patch, bug_number, pull_request, reviewer)
|
|
return
|
|
|
|
def _parse_patch(self, patch, bug_number, pull_request, reviewer):
|
|
import email
|
|
from email import (
|
|
header,
|
|
policy,
|
|
)
|
|
|
|
parse_policy = policy.compat32.clone(max_line_length=None)
|
|
parsed_mail = email.message_from_bytes(patch, policy=parse_policy)
|
|
|
|
def header_as_unicode(key):
|
|
decoded = header.decode_header(parsed_mail[key])
|
|
return str(header.make_header(decoded))
|
|
|
|
author = header_as_unicode("From")
|
|
date = header_as_unicode("Date")
|
|
commit_summary = header_as_unicode("Subject")
|
|
email_body = parsed_mail.get_payload(decode=True).decode("utf-8")
|
|
(commit_body, diff) = ("\n" + email_body).rsplit("\n---\n", 1)
|
|
|
|
bug_prefix = ""
|
|
if bug_number is not None:
|
|
bug_prefix = "Bug %s - " % bug_number
|
|
commit_summary = re.sub(r"^\[PATCH[0-9 /]*\] ", bug_prefix, commit_summary)
|
|
if reviewer is not None:
|
|
commit_summary += " r=" + reviewer
|
|
|
|
commit_msg = commit_summary + "\n"
|
|
if len(commit_body) > 0:
|
|
commit_msg += commit_body + "\n"
|
|
commit_msg += "\n[import_pr] From " + pull_request + "\n"
|
|
|
|
patch_obj = {
|
|
"author": author,
|
|
"date": date,
|
|
"commit_summary": commit_summary,
|
|
"commit_msg": commit_msg,
|
|
"diff": diff,
|
|
}
|
|
return patch_obj
|