# 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/.
import re
import tempfile
from wptrunner
import update
as wptupdate
from wptrunner.update.tree
import Commit, CommitMessage, get_unique_name
class HgTree(wptupdate.tree.HgTree):
def __init__(self, *args, **kwargs):
self.commit_cls = kwargs.pop(
"commit_cls", Commit)
wptupdate.tree.HgTree.__init__(self, *args, **kwargs)
# TODO: The extra methods for upstreaming patches from a
# hg checkout
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 rev_from_hg(self, rev):
return self.git(
"cinnabar",
"hg2git", rev).strip()
def rev_to_hg(self, rev):
return self.git(
"cinnabar",
"git2hg", rev).strip()
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"]
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):
"""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)
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, diff):
self.author = author
self.email = email
if isinstance(message, CommitMessage):
self.message = message
else:
self.message = GeckoCommitMessage(message)
self.diff = diff
def __repr__(self):
return "" % 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 # noqa E501
# 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
)
_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._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 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",
"--patch",
"--format=format:",
"%s" % (self.sha1,)]
if path
is not None:
args.append(
"--")
args.append(path)
diff = self.git(
"show", *args)
return Patch(self.author, self.email, self.message, diff)