tree.py (6792B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import re 6 import tempfile 7 8 from wptrunner import update as wptupdate 9 from wptrunner.update.tree import Commit, CommitMessage, get_unique_name 10 11 12 class HgTree(wptupdate.tree.HgTree): 13 def __init__(self, *args, **kwargs): 14 self.commit_cls = kwargs.pop("commit_cls", Commit) 15 wptupdate.tree.HgTree.__init__(self, *args, **kwargs) 16 17 # TODO: The extra methods for upstreaming patches from a 18 # hg checkout 19 20 21 class GitTree(wptupdate.tree.GitTree): 22 def __init__(self, *args, **kwargs): 23 """Extension of the basic GitTree with extra methods for 24 transfering patches""" 25 commit_cls = kwargs.pop("commit_cls", Commit) 26 wptupdate.tree.GitTree.__init__(self, *args, **kwargs) 27 self.commit_cls = commit_cls 28 29 def rev_from_hg(self, rev): 30 return self.git("cinnabar", "hg2git", rev).strip() 31 32 def rev_to_hg(self, rev): 33 return self.git("cinnabar", "git2hg", rev).strip() 34 35 def create_branch(self, name, ref=None): 36 """Create a named branch, 37 38 :param name: String representing the branch name. 39 :param ref: None to use current HEAD or rev that the branch should point to""" 40 41 args = [] 42 if ref is not None: 43 if hasattr(ref, "sha1"): 44 ref = ref.sha1 45 args.append(ref) 46 self.git("branch", name, *args) 47 48 def commits_by_message(self, message, path=None): 49 """List of commits with messages containing a given string. 50 51 :param message: The string that must be contained in the message. 52 :param path: Path to a file or directory the commit touches 53 """ 54 args = ["--pretty=format:%H", "--reverse", "-z", "--grep=%s" % message] 55 if path is not None: 56 args.append("--") 57 args.append(path) 58 data = self.git("log", *args) 59 return [self.commit_cls(self, sha1) for sha1 in data.split("\0")] 60 61 def log(self, base_commit=None, path=None): 62 """List commits touching a certian path from a given base commit. 63 64 :base_param commit: Commit object for the base commit from which to log 65 :param path: Path that the commits must touch 66 """ 67 args = ["--pretty=format:%H", "--reverse", "-z"] 68 if base_commit is not None: 69 args.append("%s.." % base_commit.sha1) 70 if path is not None: 71 args.append("--") 72 args.append(path) 73 data = self.git("log", *args) 74 return [self.commit_cls(self, sha1) for sha1 in data.split("\0") if sha1] 75 76 def import_patch(self, patch): 77 """Import a patch file into the tree and commit it 78 79 :param patch: a Patch object containing the patch to import 80 """ 81 82 with tempfile.NamedTemporaryFile() as f: 83 f.write(patch.diff) 84 f.flush() 85 f.seek(0) 86 self.git("apply", "--index", f.name) 87 self.git("commit", "-m", patch.message.text, "--author=%s" % patch.full_author) 88 89 def rebase(self, ref, continue_rebase=False): 90 """Rebase the current branch onto another commit. 91 92 :param ref: A Commit object for the commit to rebase onto 93 :param continue_rebase: Continue an in-progress rebase""" 94 if continue_rebase: 95 args = ["--continue"] 96 else: 97 if hasattr(ref, "sha1"): 98 ref = ref.sha1 99 args = [ref] 100 self.git("rebase", *args) 101 102 def push(self, remote, local_ref, remote_ref, force=False): 103 """Push local changes to a remote. 104 105 :param remote: URL of the remote to push to 106 :param local_ref: Local branch to push 107 :param remote_ref: Name of the remote branch to push to 108 :param force: Do a force push 109 """ 110 args = [] 111 if force: 112 args.append("-f") 113 args.extend([remote, "%s:%s" % (local_ref, remote_ref)]) 114 self.git("push", *args) 115 116 def unique_branch_name(self, prefix): 117 """Get an unused branch name in the local tree 118 119 :param prefix: Prefix to use at the start of the branch name""" 120 branches = [ 121 ref[len("refs/heads/") :] 122 for sha1, ref in self.list_refs() 123 if ref.startswith("refs/heads/") 124 ] 125 return get_unique_name(branches, prefix) 126 127 128 class Patch: 129 def __init__(self, author, email, message, diff): 130 self.author = author 131 self.email = email 132 if isinstance(message, CommitMessage): 133 self.message = message 134 else: 135 self.message = GeckoCommitMessage(message) 136 self.diff = diff 137 138 def __repr__(self): 139 return "<Patch (%s)>" % self.message.full_summary 140 141 @property 142 def full_author(self): 143 return "%s <%s>" % (self.author, self.email) 144 145 @property 146 def empty(self): 147 return bool(self.diff.strip()) 148 149 150 class GeckoCommitMessage(CommitMessage): 151 """Commit message following the Gecko conventions for identifying bug number 152 and reviewer""" 153 154 # c.f. http://hg.mozilla.org/hgcustom/version-control-tools/file/tip/hghooks/mozhghooks/commit-message.py # noqa E501 155 # which has the regexps that are actually enforced by the VCS hooks. These are 156 # slightly different because we need to parse out specific parts of the message rather 157 # than just enforce a general pattern. 158 159 _bug_re = re.compile( 160 r"^Bug (\d+)[^\w]*(?:Part \d+[^\w]*)?(.*?)\s*(?:r=(\w*))?$", re.IGNORECASE 161 ) 162 163 _backout_re = re.compile( 164 r"^(?:Back(?:ing|ed)\s+out)|Backout|(?:Revert|(?:ed|ing))", re.IGNORECASE 165 ) 166 _backout_sha1_re = re.compile(r"(?:\s|\:)(0-9a-f){12}") 167 168 def _parse_message(self): 169 CommitMessage._parse_message(self) 170 171 if self._backout_re.match(self.full_summary): 172 self.backouts = self._backout_re.findall(self.full_summary) 173 else: 174 self.backouts = [] 175 176 m = self._bug_re.match(self.full_summary) 177 if m is not None: 178 self.bug, self.summary, self.reviewer = m.groups() 179 else: 180 self.bug, self.summary, self.reviewer = None, self.full_summary, None 181 182 183 class GeckoCommit(Commit): 184 msg_cls = GeckoCommitMessage 185 186 def export_patch(self, path=None): 187 """Convert a commit in the tree to a Patch with the bug number and 188 reviewer stripped from the message""" 189 args = ["--binary", "--patch", "--format=format:", "%s" % (self.sha1,)] 190 if path is not None: 191 args.append("--") 192 args.append(path) 193 194 diff = self.git("show", *args) 195 196 return Patch(self.author, self.email, self.message, diff)