diff options
Diffstat (limited to 'python/wpt/importer/tree.py')
-rw-r--r-- | python/wpt/importer/tree.py | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/python/wpt/importer/tree.py b/python/wpt/importer/tree.py new file mode 100644 index 00000000000..ec38e9b481e --- /dev/null +++ b/python/wpt/importer/tree.py @@ -0,0 +1,201 @@ +# 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 https://mozilla.org/MPL/2.0/. + +from distutils.spawn import find_executable +import re +import subprocess +import sys +import tempfile + +from wptrunner import update as wptupdate +from wptrunner.update.tree import Commit, CommitMessage, get_unique_name + + +class GitTree(wptupdate.tree.GitTree): + def __init__(self, *args, **kwargs): + """Extension of the basic GitTree with extra methods for + transfering patches""" + commit_cls = kwargs.pop("commit_cls", Commit) + wptupdate.tree.GitTree.__init__(self, *args, **kwargs) + self.commit_cls = commit_cls + + def create_branch(self, name, ref=None): + """Create a named branch, + + :param name: String representing the branch name. + :param ref: None to use current HEAD or rev that the branch should point to""" + + args = [] + if ref is not None: + if hasattr(ref, "sha1"): + ref = ref.sha1 + args.append(ref) + self.git("branch", name, *args) + + def commits_by_message(self, message, path=None): + """List of commits with messages containing a given string. + + :param message: The string that must be contained in the message. + :param path: Path to a file or directory the commit touches + """ + args = ["--pretty=format:%H", "--reverse", "-z", "--grep=%s" % message] + if path is not None: + args.append("--") + args.append(path) + data = self.git("log", *args) + return [self.commit_cls(self, sha1) for sha1 in data.split("\0")] + + def log(self, base_commit=None, path=None): + """List commits touching a certian path from a given base commit. + + :base_param commit: Commit object for the base commit from which to log + :param path: Path that the commits must touch + """ + args = ["--pretty=format:%H", "--reverse", "-z", "--no-merges"] + if base_commit is not None: + args.append("%s.." % base_commit.sha1) + if path is not None: + args.append("--") + args.append(path) + data = self.git("log", *args) + return [self.commit_cls(self, sha1) for sha1 in data.split("\0") if sha1] + + def import_patch(self, patch, strip_count): + """Import a patch file into the tree and commit it + + :param patch: a Patch object containing the patch to import + """ + + with tempfile.NamedTemporaryFile() as f: + f.write(patch.diff) + f.flush() + f.seek(0) + self.git("apply", "--index", f.name, "-p", str(strip_count)) + self.git("commit", "-m", patch.message.text, "--author=%s" % patch.full_author) + + def rebase(self, ref, continue_rebase=False): + """Rebase the current branch onto another commit. + + :param ref: A Commit object for the commit to rebase onto + :param continue_rebase: Continue an in-progress rebase""" + if continue_rebase: + args = ["--continue"] + else: + if hasattr(ref, "sha1"): + ref = ref.sha1 + args = [ref] + self.git("rebase", *args) + + def push(self, remote, local_ref, remote_ref, force=False): + """Push local changes to a remote. + + :param remote: URL of the remote to push to + :param local_ref: Local branch to push + :param remote_ref: Name of the remote branch to push to + :param force: Do a force push + """ + args = [] + if force: + args.append("-f") + args.extend([remote, "%s:%s" % (local_ref, remote_ref)]) + self.git("push", *args) + + def unique_branch_name(self, prefix): + """Get an unused branch name in the local tree + + :param prefix: Prefix to use at the start of the branch name""" + branches = [ref[len("refs/heads/"):] for sha1, ref in self.list_refs() + if ref.startswith("refs/heads/")] + return get_unique_name(branches, prefix) + + +class Patch(object): + def __init__(self, author, email, message, merge_message, diff): + self.author = author + self.email = email + self.merge_message = merge_message + if isinstance(message, CommitMessage): + self.message = message + else: + self.message = GeckoCommitMessage(message) + self.diff = diff + + def __repr__(self): + return "<Patch (%s)>" % self.message.full_summary + + @property + def full_author(self): + return "%s <%s>" % (self.author, self.email) + + @property + def empty(self): + return bool(self.diff.strip()) + + +class GeckoCommitMessage(CommitMessage): + """Commit message following the Gecko conventions for identifying bug number + and reviewer""" + + # c.f. http://hg.mozilla.org/hgcustom/version-control-tools/file/tip/hghooks/mozhghooks/commit-message.py + # which has the regexps that are actually enforced by the VCS hooks. These are + # slightly different because we need to parse out specific parts of the message rather + # than just enforce a general pattern. + + _bug_re = re.compile(r"^Bug (\d+)[^\w]*(?:Part \d+[^\w]*)?(.*?)\s*(?:r=(\w*))?$", + re.IGNORECASE) + _merge_re = re.compile(r"^Auto merge of #(\d+) - [^:]+:[^,]+, r=(.+)$", re.IGNORECASE) + + _backout_re = re.compile(r"^(?:Back(?:ing|ed)\s+out)|Backout|(?:Revert|(?:ed|ing))", + re.IGNORECASE) + _backout_sha1_re = re.compile(r"(?:\s|\:)(0-9a-f){12}") + + def _parse_message(self): + CommitMessage._parse_message(self) + + if self._backout_re.match(self.full_summary): + self.backouts = self._backout_re.findall(self.full_summary) + else: + self.backouts = [] + + m = self._merge_re.match(self.full_summary) + if m is not None: + self.bug, self.reviewer = m.groups() + self.summary = self.full_summary + else: + m = self._bug_re.match(self.full_summary) + if m is not None: + self.bug, self.summary, self.reviewer = m.groups() + else: + self.bug, self.summary, self.reviewer = None, self.full_summary, None + + +class GeckoCommit(Commit): + msg_cls = GeckoCommitMessage + + def __init__(self, tree, sha1, is_merge=False): + Commit.__init__(self, tree, sha1) + if not is_merge: + args = ["-c", sha1] + try: + merge_rev = self.git("when-merged", *args).strip() + except subprocess.CalledProcessError as exn: + if not find_executable('git-when-merged'): + print('Please add the `when-merged` git command to your PATH ' + '(https://github.com/mhagger/git-when-merged/).') + sys.exit(1) + raise exn + self.merge = GeckoCommit(tree, merge_rev, True) + + def export_patch(self, path=None): + """Convert a commit in the tree to a Patch with the bug number and + reviewer stripped from the message""" + args = ["--binary", self.sha1] + if path is not None: + args.append("--") + args.append(path) + + diff = self.git("show", *args) + + merge_message = self.merge.message if self.merge else None + return Patch(self.author, self.email, self.message, merge_message, diff) |