Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / lib / patch.py
1 # Copyright (c) 2011-2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Module that handles the processing of patches to the source tree."""
6
7 from __future__ import print_function
8
9 import calendar
10 import os
11 import random
12 import re
13 import time
14
15 from chromite.cbuildbot import constants
16 from chromite.lib import cros_build_lib
17 from chromite.lib import git
18 from chromite.lib import gob_util
19
20
21 # We import mock so that we can identify mock.MagicMock instances in tests
22 # that use mock.
23 try:
24   import mock
25 except ImportError:
26   mock = None
27
28
29 _MAXIMUM_GERRIT_NUMBER_LENGTH = 6
30 _GERRIT_CHANGE_ID_PREFIX = 'I'
31 _GERRIT_CHANGE_ID_LENGTH = 40
32 _GERRIT_CHANGE_ID_TOTAL_LENGTH = (_GERRIT_CHANGE_ID_LENGTH +
33                                   len(_GERRIT_CHANGE_ID_PREFIX))
34 REPO_NAME_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_\-]*(/[a-zA-Z0-9_-]+)*$')
35 BRANCH_NAME_RE = re.compile(r'^(refs/heads/)?[a-zA-Z0-9_][a-zA-Z0-9_\-]*$')
36
37
38 def ParseSHA1(text, error_ok=True):
39   """Checks if |text| conforms to the SHA1 format and parses it.
40
41   Args:
42     text: The string to check.
43     error_ok: If set, do not raise an exception if |text| is not a
44       valid SHA1.
45
46   Returns:
47     If |text| is a valid SHA1, returns |text|.  Otherwise,
48     returns None when |error_ok| is set and raises an exception when
49     |error_ok| is False.
50   """
51   valid = git.IsSHA1(text)
52   if not error_ok and not valid:
53     raise ValueError('%s is not a valid SHA1', text)
54
55   return text if valid else None
56
57
58 def ParseGerritNumber(text, error_ok=True):
59   """Checks if |text| conforms to the Gerrit number format and parses it.
60
61   Args:
62     text: The string to check.
63     error_ok: If set, do not raise an exception if |text| is not a
64       valid Gerrit number.
65
66   Returns:
67     If |text| is a valid Gerrit number, returns |text|.  Otherwise,
68     returns None when |error_ok| is set and raises an exception when
69     |error_ok| is False.
70   """
71   valid = text.isdigit() and len(text) <= _MAXIMUM_GERRIT_NUMBER_LENGTH
72   if not error_ok and not valid:
73     raise ValueError('%s is not a valid Gerrit number', text)
74
75   return text if valid else None
76
77
78 def ParseChangeID(text, error_ok=True):
79   """Checks if |text| conforms to the change-ID format and parses it.
80
81   Change-ID is a string that starts with I/i. E.g.
82     I47ea30385af60ae4cc2acc5d1a283a46423bc6e1
83
84   Args:
85     text: The string to check.
86     error_ok: If set, do not raise an exception if |text| is not a
87       valid change-ID.
88
89   Returns:
90     If |text| is a valid change-ID, returns |text|.  Otherwise,
91     returns None when |error_ok| is set and raises an exception when
92     |error_ok| is False.
93   """
94   valid = (text.startswith(_GERRIT_CHANGE_ID_PREFIX) and
95            len(text) == _GERRIT_CHANGE_ID_TOTAL_LENGTH and
96            git.IsSHA1(text[len(_GERRIT_CHANGE_ID_PREFIX):].lower()))
97
98   if not error_ok and not valid:
99     raise ValueError('%s is not a valid change-ID', text)
100
101   return text if valid else None
102
103
104 def ParseFullChangeID(text, error_ok=True):
105   """Checks if |text| conforms to the full change-ID format and parses it.
106
107   Full change-ID format: project~branch~change-id. E.g.
108     chromiumos/chromite~master~I47ea30385af60ae4cc2acc5d1a283a46423bc6e1
109
110   Args:
111     text: The string to check.
112     error_ok: If set, do not raise an exception if |text| is not a
113       valid full change-ID.
114
115   Returns:
116     If |text| is a valid full change-ID, returns (project, branch,
117     change_id).  Otherwise, returns None when |error_ok| is set and
118     raises an exception when |error_ok| is False.
119   """
120   fields = text.split('~')
121   if not len(fields) == 3:
122     if not error_ok:
123       raise ValueError('%s is not a valid full change-ID', text)
124
125     return None
126
127   project, branch, change_id = fields
128   if (not REPO_NAME_RE.match(project) or
129       not BRANCH_NAME_RE.match(branch) or
130       not ParseChangeID(change_id)):
131     if not error_ok:
132       raise ValueError('%s is not a valid full change-ID', text)
133
134     return None
135
136   return project, branch, change_id
137
138
139 class PatchException(Exception):
140   """Base exception class all patch exception derive from."""
141
142   # Unless instances override it, default all exceptions to ToT.
143   inflight = False
144
145   def __init__(self, patch, message=None):
146     is_mock = mock is not None and isinstance(patch, mock.MagicMock)
147     if not isinstance(patch, GitRepoPatch) and not is_mock:
148       raise TypeError(
149           "Patch must be a GitRepoPatch derivative; got type %s: %r"
150           % (type(patch), patch))
151     Exception.__init__(self)
152     self.patch = patch
153     self.message = message
154     self.args = (patch,)
155     if message is not None:
156       self.args += (message,)
157
158   def ShortExplanation(self):
159     """Print a short explanation of why the patch failed.
160
161     Explanations here should be suitable for inclusion in a sentence
162     starting with the CL number. This is useful for writing nice error
163     messages about dependency errors.
164     """
165     return 'failed: %s' % (self.message,)
166
167   def __str__(self):
168     return '%s %s' % (self.patch.PatchLink(), self.ShortExplanation())
169
170
171 class ApplyPatchException(PatchException):
172   """Exception thrown if we fail to apply a patch."""
173
174   def __init__(self, patch, message=None, inflight=False, trivial=False,
175                files=()):
176     PatchException.__init__(self, patch, message=message)
177     self.inflight = inflight
178     self.trivial = trivial
179     self.files = files = tuple(files)
180     # Reset args; else serialization can break.
181     self.args = (patch, message, inflight, trivial, files)
182
183   def _StringifyInflight(self):
184     return 'the current patch series' if self.inflight else 'ToT'
185
186   def _StringifyFilenames(self):
187     """Stringify our list of filenames for presentation in Gerrit."""
188     # Prefix each filename with a hyphen so that Gerrit will format it as an
189     # unordered list.
190     return '\n\n'.join('- %s' % x for x in self.files)
191
192   def ShortExplanation(self):
193     s = 'conflicted with %s' % (self._StringifyInflight(),)
194     if self.trivial:
195       s += (' because file content merging is disabled for this '
196             'project.')
197     else:
198       s += '.'
199     if self.files:
200       s += ('\n\nThe conflicting files are amongst:\n\n'
201             '%s' % (self._StringifyFilenames(),))
202     if self.message:
203       s += '\n\n%s' % (self.message,)
204     return s
205
206
207 class EbuildConflict(ApplyPatchException):
208   """Exception thrown if two CLs delete the same ebuild."""
209
210   def __init__(self, patch, inflight, ebuilds):
211     ApplyPatchException.__init__(self, patch, inflight=inflight, files=ebuilds)
212     self.args = (patch, inflight, ebuilds)
213
214   def ShortExplanation(self):
215     return ('deletes an ebuild that is not present anymore. For this reason, '
216             'we refuse to merge your change.\n\n'
217             'When you rebase your change, please take into account that the '
218             'following ebuilds have been uprevved or deleted:\n\n'
219             '%s' % (self._StringifyFilenames()))
220
221
222 class PatchIsEmpty(ApplyPatchException):
223   """Exception thrown if we try to apply an empty patch"""
224
225   def ShortExplanation(self):
226     return 'had no changes after rebasing to %s.' % (
227         self._StringifyInflight(),)
228
229
230 class DependencyError(PatchException):
231   """Thrown when a change cannot be applied due to a failure in a dependency."""
232
233   def __init__(self, patch, error):
234     """Initialize the error object.
235
236     Args:
237       patch: The GitRepoPatch instance that this exception concerns.
238       error: A PatchException object that can be stringified to describe
239         the error.
240     """
241     PatchException.__init__(self, patch)
242     self.inflight = error.inflight
243     self.error = error
244     self.args = (patch, error,)
245
246   def ShortExplanation(self):
247     link = self.error.patch.PatchLink()
248     return ('depends on %s, which %s' % (link, self.error.ShortExplanation()))
249
250
251 class BrokenCQDepends(PatchException):
252   """Raised if a patch has a CQ-DEPEND line that is ill formated."""
253
254   def __init__(self, patch, text, msg=None):
255     PatchException.__init__(self, patch)
256     self.text = text
257     self.msg = msg
258     self.args = (patch, text, msg)
259
260   def ShortExplanation(self):
261     s = 'has a malformed CQ-DEPEND target: %s' % (self.text,)
262     if self.msg is not None:
263       s += '; %s' % (self.msg,)
264     return s
265
266
267 class BrokenChangeID(PatchException):
268   """Raised if a patch has an invalid or missing Change-ID."""
269
270   def __init__(self, patch, message, missing=False):
271     PatchException.__init__(self, patch)
272     self.message = message
273     self.missing = missing
274     self.args = (patch, message, missing)
275
276   def ShortExplanation(self):
277     return 'has a broken ChangeId: %s' % (self.message,)
278
279
280 class ChangeMatchesMultipleCheckouts(PatchException):
281   """Raised if the given change matches multiple checkouts."""
282
283   def ShortExplanation(self):
284     return ('matches multiple checkouts. Does the manifest check out the '
285             'same project and branch to different locations?')
286
287
288 class ChangeNotInManifest(PatchException):
289   """Raised if we try to apply a not-in-manifest change."""
290
291   def ShortExplanation(self):
292     return 'could not be found in the repo manifest.'
293
294
295 def MakeChangeId(unusable=False):
296   """Create a random Change-Id.
297
298   Args:
299     unusable: If set to True, return a Change-Id like string that gerrit
300       will explicitly fail on.  This is primarily used for internal ids,
301       as a fallback when a Change-Id could not be parsed.
302   """
303   s = "%x" % (random.randint(0, 2 ** 160),)
304   s = s.rjust(_GERRIT_CHANGE_ID_LENGTH, '0')
305   if unusable:
306     return 'Fake-ID %s' % s
307   return '%s%s' % (_GERRIT_CHANGE_ID_PREFIX, s)
308
309
310 class PatchCache(object):
311   """Dict-like object used for tracking a group of patches.
312
313   This is usable both for existence checks against given string
314   deps, and for change querying.
315   """
316
317   def __init__(self, initial=()):
318     self._dict = {}
319     self.Inject(*initial)
320
321   def Inject(self, *args):
322     """Inject a sequence of changes into this cache."""
323     for change in args:
324       self.InjectCustomKeys(change.LookupAliases(), change)
325
326   def InjectCustomKeys(self, keys, change):
327     """Inject a change w/ a list of keys.  Generally you want Inject instead.
328
329     Args:
330       keys: A list of keys to update.
331       change: The change to update the keys to.
332     """
333     for key in keys:
334       self._dict[str(key)] = change
335
336   def _GetAliases(self, value):
337     if hasattr(value, 'LookupAliases'):
338       return value.LookupAliases()
339     elif not isinstance(value, basestring):
340       # This isn't needed in production code; it however is
341       # rather useful to flush out bugs in test code.
342       raise ValueError("Value %r isn't a string" % (value,))
343     return [value]
344
345   def Remove(self, *args):
346     """Remove a change from this cache."""
347     for change in args:
348       for alias in self._GetAliases(change):
349         self._dict.pop(alias, None)
350
351   def __iter__(self):
352     return iter(set(self._dict.itervalues()))
353
354   def __getitem__(self, key):
355     """If the given key exists, return the Change, else None."""
356     for alias in self._GetAliases(key):
357       val = self._dict.get(alias)
358       if val is not None:
359         return val
360     return None
361
362   def __contains__(self, key):
363     return self[key] is not None
364
365   def copy(self):
366     """Return a copy of this cache."""
367     return self.__class__(list(self))
368
369
370 def StripPrefix(text):
371   """Strips the leading '*' for internal change names.
372
373   Args:
374     text: text to examine.
375
376   Returns:
377     A tuple of the corresponding remote and the stripped text.
378   """
379   remote = constants.EXTERNAL_REMOTE
380   prefix = constants.INTERNAL_CHANGE_PREFIX
381   if text.startswith(prefix):
382     text = text[len(prefix):]
383     remote = constants.INTERNAL_REMOTE
384
385   return remote, text
386
387
388 def AddPrefix(patch, text):
389   """Add the leading '*' to |text| if applicable.
390
391   Examines patch.remote and adds the prefix to text if applicable.
392
393   Args:
394     patch: A PatchQuery object to examine.
395     text: The text to add prefix to.
396
397   Returns:
398     |text| with an added prefix for internal patches; otherwise, returns text.
399   """
400   return '%s%s' % (constants.CHANGE_PREFIX[patch.remote], text)
401
402
403 def ParsePatchDep(text, no_change_id=False, no_sha1=False,
404                   no_full_change_id=False, no_gerrit_number=False):
405   """Parses a given patch dependency and convert it to a PatchQuery object.
406
407   Parse user-given dependency (e.g. from the CQ-DEPEND line in the
408   commit message) and returns a PatchQuery object with the relevant
409   information of the dependency.
410
411   Args:
412     text: The text to parse.
413     no_change_id: Do not allow change-ID.
414     no_sha1: Do not allow SHA1.
415     no_full_change_id: Do not allow full change-ID.
416     no_gerrit_number: Do not allow gerrit_number.
417
418   Retruns:
419     A PatchQuery object.
420   """
421   original_text = text
422   if not text:
423     raise ValueError("ParsePatchDep invoked with an empty value: %r"
424                      % (text,))
425   # Deal w/ CL: targets.
426   if text.upper().startswith("CL:"):
427     if not text.startswith("CL:"):
428       raise ValueError(
429           "ParsePatchDep: 'CL:' must be upper case: %r"
430           % (original_text,))
431     text = text[3:]
432
433   # Strip the prefix to determine the remote.
434   remote, text = StripPrefix(text)
435
436   parsed = ParseFullChangeID(text)
437   if parsed:
438     if no_full_change_id:
439       raise ValueError(
440           'ParsePatchDep: Full Change-ID is not allowed: %r.' % original_text)
441
442     project, branch, change_id = parsed
443     return PatchQuery(remote, project=project, tracking_branch=branch,
444                         change_id=change_id)
445
446   parsed = ParseChangeID(text)
447   if parsed:
448     if no_change_id:
449       raise ValueError(
450           'ParsePatchDep: Change-ID is not allowed: %r.' % original_text)
451
452     return PatchQuery(remote, change_id=parsed)
453
454   parsed = ParseGerritNumber(text)
455   if parsed:
456     if no_gerrit_number:
457       raise ValueError(
458           'ParsePatchDep: Gerrit number is not allowed: %r.' % original_text)
459
460     return PatchQuery(remote, gerrit_number=parsed)
461
462   parsed = ParseSHA1(text)
463   if parsed:
464     if no_sha1:
465       raise ValueError(
466           'ParsePatchDep: SHA1 is not allowed: %r.' % original_text)
467
468     return PatchQuery(remote, sha1=parsed)
469
470   raise ValueError('Cannot parse the dependency: %s' % original_text)
471
472
473 def GetPaladinDeps(commit_message):
474   """Get the paladin dependencies for the given |commit_message|."""
475   PALADIN_DEPENDENCY_RE = re.compile(r'^(CQ.?DEPEND.)(.*)$',
476                                      re.MULTILINE | re.IGNORECASE)
477   PATCH_RE = re.compile('[^, ]+')
478   EXPECTED_PREFIX = 'CQ-DEPEND='
479   matches = PALADIN_DEPENDENCY_RE.findall(commit_message)
480   dependencies = []
481   for prefix, match in matches:
482     if prefix != EXPECTED_PREFIX:
483       msg = 'Expected %r, but got %r' % (EXPECTED_PREFIX, prefix)
484       raise ValueError(msg)
485     for chunk in PATCH_RE.findall(match):
486       chunk = ParsePatchDep(chunk, no_sha1=True)
487       if chunk not in dependencies:
488         dependencies.append(chunk)
489   return dependencies
490
491
492 class PatchQuery(object):
493   """Store information about a patch.
494
495   This stores information about a patch used to query Gerrit and/or
496   our internal PatchCache. It is mostly used to describe a patch
497   dependency.
498
499   It is is intended to match a single patch. If a user specified a
500   non-full change id then it might match multiple patches. If a user
501   specified an invalid change id then it might not match any patches.
502   """
503   def __init__(self, remote, project=None, tracking_branch=None, change_id=None,
504                sha1=None, gerrit_number=None):
505     """Initializes a PatchQuery instance.
506
507     Args:
508       remote: The remote git instance path, defined in constants.CROS_REMOTES.
509       project: The name of the project that the patch applies to.
510       tracking_branch: The remote branch of the project the patch applies to.
511       change_id: The Gerrit Change-ID representing this patch.
512       sha1: The sha1 of the commit. This *must* be accurate
513       gerrit_number: The Gerrit number of the patch.
514     """
515     self.remote = remote
516     self.tracking_branch = None
517     if tracking_branch:
518       self.tracking_branch = os.path.basename(tracking_branch)
519     self.project = project
520     self.sha1 = None if sha1 is None else ParseSHA1(sha1)
521     self.change_id = None if change_id is None else ParseChangeID(change_id)
522     self.gerrit_number = (None if gerrit_number is None else
523                           ParseGerritNumber(gerrit_number))
524     self.id = self.full_change_id = None
525     self._SetFullChangeID()
526     # self.id is the only attribute with the internal prefix (*) if
527     # applicable. All other atttributes are strictly external format.
528     self._SetID()
529
530   def _SetFullChangeID(self):
531     """Set the unique full Change-ID if possible."""
532     if (self.project is not None and
533         self.tracking_branch is not None and
534         self.change_id is not None):
535       self.full_change_id = '%s~%s~%s' % (
536           self.project, self.tracking_branch, self.change_id)
537
538   def _SetID(self, override_value=None):
539     """Set the unique ID to be used internally, if possible."""
540     if override_value is not None:
541       self.id = override_value
542       return
543
544     if not self.full_change_id:
545       self._SetFullChangeID()
546
547     if self.full_change_id:
548       self.id = AddPrefix(self, self.full_change_id)
549
550     elif self.sha1:
551       # We assume sha1 is unique, but in rare cases (e.g. two branches with
552       # the same history) it is not. We don't handle that.
553       self.id = '%s%s' % (constants.CHANGE_PREFIX[self.remote], self.sha1)
554
555   def LookupAliases(self):
556     """Returns the list of lookup keys to query a PatchCache.
557
558     Each key has to be unique for the patch. If no unique key can be
559     generated yet (because of incomplete patch information), we'd
560     rather return None to avoid retrieving incorrect patch from the
561     cache.
562     """
563     l = []
564     if self.gerrit_number:
565       l.append(self.gerrit_number)
566
567     # Note that change-ID alone is not unique. Use full change-id here.
568     if self.full_change_id:
569       l.append(self.full_change_id)
570
571     # Note that in rare cases (two branches with the same history),
572     # the commit hash may not be unique. We don't handle that.
573     if self.sha1:
574       l.append(self.sha1)
575
576     return ['%s%s' % (constants.CHANGE_PREFIX[self.remote], x)
577             for x in l if x is not None]
578
579   def ToGerritQueryText(self):
580     """Generate a text used to query Gerrit.
581
582     This text may not be unique because the lack of information from
583     user-specified dependencies (crbug.com/354734). In which cases,
584     the Gerrit query would fail.
585     """
586     # Try to return a unique ID if possible.
587     if self.gerrit_number:
588       return self.gerrit_number
589     elif self.full_change_id:
590       return self.full_change_id
591     elif self.sha1:
592       # SHA1 may not not be unique, but we don't handle that here.
593       return self.sha1
594     elif self.change_id:
595       # Fall back to use Change-Id, which is not unique.
596       return self.change_id
597     else:
598       # We cannot query without at least one of the three fields. A
599       # special case is UploadedLocalPatch which has none of the
600       # above, but also is not used for query.
601       raise ValueError(
602           'We do not have enough information to generate a Gerrit query. '
603           'At least one of the following fields needs to be set: Change-Id, '
604           'Gerrit number, or sha1')
605
606   def __hash__(self):
607     """Returns a hash to be used in a set or a list."""
608     if self.id:
609       return hash(self.id)
610     else:
611       return hash((self.remote, self.project, self.tracking_branch,
612                   self.gerrit_number, self.change_id, self.sha1))
613
614   def __eq__(self, other):
615     """Defines when two PatchQuery objects are considered equal."""
616     # We allow comparing against a string to make testing easier.
617     if isinstance(other, basestring):
618       return self.id == other
619
620     if self.id is not None:
621       return self.id == other.id
622
623     return ((self.remote, self.project, self.tracking_branch,
624              self.gerrit_number, self.change_id, self.sha1) ==
625             (other.remote, other.project, other.tracking_branch,
626              other.gerrit_number, other.change_id, other.sha1))
627
628
629 class GitRepoPatch(PatchQuery):
630   """Representing a patch from a branch of a local or remote git repository."""
631
632   # Note the selective case insensitivity; gerrit allows only this.
633   # TOOD(ferringb): back VALID_CHANGE_ID_RE down to {8,40}, requires
634   # ensuring CQ's internals can do the translation (almost can now,
635   # but will fail in the case of a CQ-DEPEND on a change w/in the
636   # same pool).
637   pattern = (r'^'+ re.escape(_GERRIT_CHANGE_ID_PREFIX) + r'[0-9a-fA-F]{' +
638              re.escape(str(_GERRIT_CHANGE_ID_LENGTH)) + r'}$')
639   _STRICT_VALID_CHANGE_ID_RE = re.compile(pattern)
640   _GIT_CHANGE_ID_RE = re.compile(r'^Change-Id:[\t ]*(\w+)\s*$',
641                                  re.I | re.MULTILINE)
642
643   def __init__(self, project_url, project, ref, tracking_branch, remote,
644                sha1=None, change_id=None):
645     """Initialization of abstract Patch class.
646
647     Args:
648       project_url: The url of the git repo (can be local or remote) to pull the
649                    patch from.
650       project: See PatchQuery for documentation.
651       ref: The refspec to pull from the git repo.
652       tracking_branch: See PatchQuery for documentation.
653       remote: See PatchQuery for documentation.
654       sha1: The sha1 of the commit, if known. This *must* be accurate.  Can
655         be None if not yet known- in which case Fetch will update it.
656       change_id: See PatchQuery for documentation.
657     """
658     super(GitRepoPatch, self).__init__(remote, project=project,
659                                        tracking_branch=tracking_branch,
660                                        change_id = change_id,
661                                        sha1=sha1, gerrit_number=None)
662     self.project_url = project_url
663     self.commit_message = None
664     self._subject_line = None
665     self.ref = ref
666     self._is_fetched = set()
667
668   @property
669   def internal(self):
670     """Whether patch is to an internal cros project."""
671     return self.remote == constants.INTERNAL_REMOTE
672
673   def Fetch(self, git_repo):
674     """Fetch this patch into the given git repository.
675
676     FETCH_HEAD is implicitly reset by this operation.  Additionally,
677     if the sha1 of the patch was not yet known, it is pulled and stored
678     on this object and the git_repo is updated w/ the requested git
679     object.
680
681     While doing so, we'll load the commit message and Change-Id if not
682     already known.
683
684     Finally, if the sha1 is known and it's already available in the target
685     repository, this will skip the actual fetch operation (it's unneeded).
686
687     Args:
688       git_repo: The git repository to fetch this patch into.
689
690     Returns:
691       The sha1 of the patch.
692     """
693     git_repo = os.path.normpath(git_repo)
694     if git_repo in self._is_fetched:
695       return self.sha1
696
697     def _PullData(rev):
698       ret = git.RunGit(
699           git_repo, ['log', '--pretty=format:%H%x00%s%x00%B', '-n1', rev],
700           error_code_ok=True)
701       if ret.returncode != 0:
702         return None, None, None
703       output = ret.output.split('\0')
704       if len(output) != 3:
705         return None, None, None
706       return [unicode(x.strip(), 'ascii', 'ignore') for x in output]
707
708     sha1 = None
709     if self.sha1 is not None:
710       # See if we've already got the object.
711       sha1, subject, msg = _PullData(self.sha1)
712
713     if sha1 is None:
714       git.RunGit(git_repo, ['fetch', '-f', self.project_url, self.ref])
715       sha1, subject, msg = _PullData(self.sha1 or 'FETCH_HEAD')
716
717     sha1 = ParseSHA1(sha1, error_ok=False)
718
719     if self.sha1 is not None and sha1 != self.sha1:
720       # Even if we know the sha1, still do a sanity check to ensure we
721       # actually just fetched it.
722       raise PatchException(self,
723                            'Patch %s specifies sha1 %s, yet in fetching from '
724                            '%s we could not find that sha1.  Internal error '
725                            'most likely.' % (self, self.sha1, self.ref))
726     self.sha1 = sha1
727     self._EnsureId(msg)
728     self.commit_message = msg
729     self._subject_line = subject
730     self._is_fetched.add(git_repo)
731     return self.sha1
732
733   def GetDiffStatus(self, git_repo):
734     """Isolate the paths and modifications this patch induces.
735
736     Note that detection of file renaming is explicitly turned off.
737     This is intentional since the level of rename detection can vary
738     by user configuration, and trying to have our code specify the
739     minimum level is fairly messy from an API perspective.
740
741     Args:
742       git_repo: Git repository to operate upon.
743
744     Returns:
745       A dictionary of path -> modification_type tuples.  See
746       `git log --help`, specifically the --diff-filter section for details.
747     """
748
749     self.Fetch(git_repo)
750
751     try:
752       lines = git.RunGit(git_repo, ['diff', '--no-renames', '--name-status',
753                                     '%s^..%s' % (self.sha1, self.sha1)])
754     except cros_build_lib.RunCommandError as e:
755       # If we get a 128, that means git couldn't find the the parent of our
756       # sha1- meaning we're the first commit in the repository (there is no
757       # parent).
758       if e.result.returncode != 128:
759         raise
760       return {}
761     lines = lines.output.splitlines()
762     return dict(line.split('\t', 1)[::-1] for line in lines)
763
764   def CherryPick(self, git_repo, trivial=False, inflight=False,
765                  leave_dirty=False):
766     """Attempts to cherry-pick the given rev into branch.
767
768     Args:
769       git_repo: The git repository to operate upon.
770       trivial: Only allow trivial merges when applying change.
771       inflight: If true, changes are already applied in this branch.
772       leave_dirty: If True, if a CherryPick fails leaves partial commit behind.
773
774     Raises:
775       A ApplyPatchException if the request couldn't be handled.
776     """
777     # Note the --ff; we do *not* want the sha1 to change unless it
778     # has to.
779     cmd = ['cherry-pick', '--strategy', 'resolve', '--ff']
780     if trivial:
781       cmd += ['-X', 'trivial']
782     cmd.append(self.sha1)
783
784     reset_target = None if leave_dirty else 'HEAD'
785     try:
786       git.RunGit(git_repo, cmd)
787       reset_target = None
788       return
789     except cros_build_lib.RunCommandError as error:
790       ret = error.result.returncode
791       if ret not in (1, 2):
792         cros_build_lib.Error(
793             "Unknown cherry-pick exit code %s; %s",
794             ret, error)
795         raise ApplyPatchException(
796             self, inflight=inflight,
797             message=("Unknown exit code %s returned from cherry-pick "
798                      "command: %s" % (ret, error)))
799       elif ret == 1:
800         # This means merge resolution was fine, but there was content conflicts.
801         # If there are no conflicts, then this is caused by the change already
802         # being merged.
803         result = git.RunGit(git_repo,
804             ['diff', '--name-only', '--diff-filter=U'])
805
806         # Output is one line per filename.
807         conflicts = result.output.splitlines()
808         if not conflicts:
809           # No conflicts means the git repo is in a pristine state.
810           reset_target = None
811           raise PatchIsEmpty(self, inflight=inflight)
812
813         # Making it here means that it wasn't trivial, nor was it already
814         # applied.
815         assert not trivial
816         raise ApplyPatchException(self, inflight=inflight, files=conflicts)
817
818       # ret=2 handling, this deals w/ trivial conflicts; including figuring
819       # out if it was trivial induced or not.
820       assert trivial
821       # Here's the kicker; trivial conflicts can mask content conflicts.
822       # We would rather state if it's a content conflict since in solving the
823       # content conflict, the trivial conflict is solved.  Thus this
824       # second run, where we let the exception fly through if one occurs.
825       # Note that a trivial conflict means the tree is unmodified; thus
826       # no need for cleanup prior to this invocation.
827       reset_target = None
828       self.CherryPick(git_repo, trivial=False, inflight=inflight)
829       # Since it succeeded, we need to rewind.
830       reset_target = 'HEAD^'
831
832       raise ApplyPatchException(self, trivial=True, inflight=inflight)
833     finally:
834       if reset_target is not None:
835         git.RunGit(git_repo, ['reset', '--hard', reset_target],
836                    error_code_ok=True)
837
838   def Apply(self, git_repo, upstream, revision=None, trivial=False):
839     """Apply patch into a standalone git repo.
840
841     The git repo does not need to be part of a repo checkout.
842
843     Args:
844       git_repo: The git repository to operate upon.
845       revision: Revision to attach the tracking branch to.
846       upstream: The branch to base the patch on.
847       trivial: Only allow trivial merges when applying change.
848     """
849
850     self.Fetch(git_repo)
851
852     cros_build_lib.Info('Attempting to cherry-pick change %s', self)
853
854     # If the patch branch exists use it, otherwise create it and switch to it.
855     if git.DoesCommitExistInRepo(git_repo, constants.PATCH_BRANCH):
856       git.RunGit(git_repo, ['checkout', '-f', constants.PATCH_BRANCH])
857     else:
858       git.RunGit(git_repo,
859                  ['checkout', '-b', constants.PATCH_BRANCH, '-t', upstream])
860       if revision:
861         git.RunGit(git_repo, ['reset', '--hard', revision])
862
863     # Figure out if we're inflight.  At this point, we assume that the branch
864     # is checked out and rebased onto upstream.  If HEAD differs from upstream,
865     # then there are already other patches that have been applied.
866     upstream, head = [
867         git.RunGit(git_repo, ['rev-list', '-n1', x]).output.strip()
868         for x in (upstream, 'HEAD')]
869     inflight = (head != upstream)
870
871     self._FindEbuildConflicts(git_repo, upstream, inflight=inflight)
872
873     do_checkout = True
874     try:
875       self.CherryPick(git_repo, trivial=trivial, inflight=inflight)
876       do_checkout = False
877       return
878     except ApplyPatchException:
879       if not inflight:
880         raise
881       git.RunGit(git_repo, ['checkout', '-f', '--detach', upstream])
882
883       self.CherryPick(git_repo, trivial=trivial, inflight=False)
884       # Making it here means that it was an inflight issue; throw the original.
885       raise
886     finally:
887       # Ensure we're on the correct branch on the way out.
888       if do_checkout:
889         git.RunGit(git_repo, ['checkout', '-f', constants.PATCH_BRANCH],
890                    error_code_ok=True)
891
892   def ApplyAgainstManifest(self, manifest, trivial=False):
893     """Applies the patch against the specified manifest.
894
895       manifest: A ManifestCheckout object which is used to discern which
896         git repo to patch, what the upstream branch should be, etc.
897       trivial:  Only allow trivial merges when applying change.
898
899     Raises:
900       ApplyPatchException: If the patch failed to apply.
901     """
902     checkout = self.GetCheckout(manifest)
903     revision = checkout.get('revision')
904     # revision might be a branch which is written as it would appear on the
905     # remote. If so, rewrite it as a local reference to the remote branch.
906     # For example, refs/heads/master might become refs/remotes/cros/master.
907     if revision and not git.IsSHA1(revision):
908       revision = 'refs/remotes/%s/%s' % \
909           (checkout['remote'], git.StripRefs(revision))
910     upstream = checkout['tracking_branch']
911     self.Apply(checkout.GetPath(absolute=True), upstream, revision=revision,
912                trivial=trivial)
913
914   def GerritDependencies(self):
915     """Returns a list of Gerrit change numbers that this patch depends on.
916
917     Ordinary patches have no Gerrit-style dependencies since they're not
918     from Gerrit at all. See GerritPatch.GerritDependencies instead.
919     """
920     return []
921
922   def _EnsureId(self, commit_message):
923     """Ensure we have a usable Change-Id.  This will parse the Change-Id out
924     of the given commit message- if it cannot find one, it logs a warning
925     and creates a fake ID.
926
927     By its nature, that fake ID is useless- it's created to simplify
928     API usage for patch consumers. If CQ were to see and try operating
929     on one of these, it would fail for example.
930     """
931     if self.id is not None:
932       return self.id
933
934     try:
935       self.change_id = self._ParseChangeId(commit_message)
936     except BrokenChangeID:
937       cros_build_lib.Warning(
938           'Change %s, sha1 %s lacks a change-id in its commit '
939           'message.  CQ-DEPEND against this rev may not work, nor '
940           'will any gerrit querying.  Please add the appropriate '
941           'Change-Id into the commit message to resolve this.',
942           self, self.sha1)
943       self._SetID(self.sha1)
944     else:
945       self._SetID()
946
947   def _ParseChangeId(self, data):
948     """Parse a Change-Id out of a block of text.
949
950     Note that the returned content is *not* ran through FormatChangeId;
951     this is left up to the invoker.
952     """
953     # Grab just the last pararaph.
954     git_metadata = re.split(r'\n{2,}', data.rstrip())[-1]
955     change_id_match = self._GIT_CHANGE_ID_RE.findall(git_metadata)
956     if not change_id_match:
957       raise BrokenChangeID(self, 'Missing Change-Id in %s' % (data,),
958                            missing=True)
959
960     # Now, validate it.  This has no real effect on actual gerrit patches,
961     # but for local patches the validation is useful for general sanity
962     # enforcement.
963     change_id_match = change_id_match[-1]
964     # Note that since we're parsing it from basically a commit message,
965     # the gerrit standard format is required- no internal markings.
966     if not self._STRICT_VALID_CHANGE_ID_RE.match(change_id_match):
967       raise BrokenChangeID(self, change_id_match)
968
969     return ParseChangeID(change_id_match)
970
971   def PaladinDependencies(self, git_repo):
972     """Returns an ordered list of dependencies based on the Commit Message.
973
974     Parses the Commit message for this change looking for lines that follow
975     the format:
976
977     CQ-DEPEND=change_num+ e.g.
978
979     A commit which depends on a couple others.
980
981     BUG=blah
982     TEST=blah
983     CQ-DEPEND=10001,10002
984     """
985     dependencies = []
986     cros_build_lib.Debug('Checking for CQ-DEPEND dependencies for change %s',
987                          self)
988
989     # Only fetch the commit message if needed.
990     if self.commit_message is None:
991       self.Fetch(git_repo)
992
993     try:
994       dependencies = GetPaladinDeps(self.commit_message)
995     except ValueError as e:
996       raise BrokenCQDepends(self, str(e))
997
998     if dependencies:
999       cros_build_lib.Debug('Found %s Paladin dependencies for change %s',
1000                            dependencies, self)
1001     return dependencies
1002
1003   def _FindEbuildConflicts(self, git_repo, upstream, inflight=False):
1004     """Verify that there are no ebuild conflicts in the given |git_repo|.
1005
1006     When an ebuild is uprevved, git treats the uprev as a "delete" and an "add".
1007     If a developer writes a CL to delete an ebuild, and the CQ uprevs the ebuild
1008     in the mean time, the ebuild deletion is silently lost, because git does
1009     not flag the double-delete as a conflict. Instead the CQ attempts to test
1010     the CL and it ends up breaking the CQ.
1011
1012     Args:
1013       git_repo: The directory to examine.
1014       upstream: The upstream git revision.
1015       inflight: Whether we currently have patches applied to this repository.
1016     """
1017     ebuilds = [path for (path, mtype) in
1018                self.GetDiffStatus(git_repo).iteritems()
1019                if mtype == 'D' and path.endswith('.ebuild')]
1020
1021     conflicts = self._FindMissingFiles(git_repo, 'HEAD', ebuilds)
1022     if not conflicts:
1023       return
1024
1025     if inflight:
1026       # If we're inflight, test against ToT for an accurate error message.
1027       tot_conflicts = self._FindMissingFiles(git_repo, upstream, ebuilds)
1028       if tot_conflicts:
1029         inflight = False
1030         conflicts = tot_conflicts
1031
1032     raise EbuildConflict(self, inflight=inflight, ebuilds=conflicts)
1033
1034   def _FindMissingFiles(self, git_repo, tree_revision, files):
1035     """Return a list of the |files| that are missing in |tree_revision|.
1036
1037     Args:
1038       git_repo: Git repository to work in.
1039       tree_revision: Revision of the tree to use.
1040       files: Files to look for.
1041
1042     Returns:
1043       A list of the |files| that are missing in |tree_revision|.
1044     """
1045     if not files:
1046       return []
1047
1048     cmd = ['ls-tree', '--full-name', '--name-only', '-z', tree_revision, '--']
1049     output = git.RunGit(git_repo, cmd + files, error_code_ok=True).output
1050     existing_filenames = output.split('\0')[:-1]
1051     return [x for x in files if x not in existing_filenames]
1052
1053   def GetCheckout(self, manifest, strict=True):
1054     """Get the ProjectCheckout associated with this patch.
1055
1056     Args:
1057       manifest: A ManifestCheckout object.
1058       strict: If the change refers to a project/branch that is not in the
1059         manifest, raise a ChangeNotInManifest error.
1060
1061     Raises:
1062       ChangeMatchesMultipleCheckouts if there are multiple checkouts that
1063       match this change.
1064     """
1065     checkouts = manifest.FindCheckouts(self.project, self.tracking_branch,
1066                                        only_patchable=True)
1067     if len(checkouts) != 1:
1068       if len(checkouts) > 1:
1069         raise ChangeMatchesMultipleCheckouts(self)
1070       elif strict:
1071         raise ChangeNotInManifest(self)
1072       return None
1073
1074     return checkouts[0]
1075
1076   def PatchLink(self):
1077     """Return a CL link for this patch."""
1078     # GitRepoPatch instances don't have a CL link, so just return the string
1079     # representation.
1080     return str(self)
1081
1082   def __str__(self):
1083     """Returns custom string to identify this patch."""
1084     s = '%s:%s' % (self.project, self.ref)
1085     if self.sha1 is not None:
1086       s = '%s:%s%s' % (s, constants.CHANGE_PREFIX[self.remote], self.sha1[:8])
1087     # TODO(ferringb,build): This gets a bit long in output; should likely
1088     # do some form of truncation to it.
1089     if self._subject_line:
1090       s += ' "%s"' % (self._subject_line,)
1091     return s
1092
1093
1094 class LocalPatch(GitRepoPatch):
1095   """Represents patch coming from an on-disk git repo."""
1096
1097   def __init__(self, project_url, project, ref, tracking_branch, remote,
1098                sha1):
1099     GitRepoPatch.__init__(self, project_url, project, ref, tracking_branch,
1100                           remote, sha1=sha1)
1101     # Initialize our commit message/ChangeId now, since we know we have
1102     # access to the data right now.
1103     self.Fetch(project_url)
1104
1105   def _GetCarbonCopy(self):
1106     """Returns a copy of this commit object, with a different sha1.
1107
1108     This is used to work around a Gerrit bug, where a commit object cannot be
1109     uploaded for review if an existing branch (in refs/tryjobs/*) points to
1110     that same sha1.  So instead we create a copy of the commit object and upload
1111     that to refs/tryjobs/*.
1112
1113     Returns:
1114       The sha1 of the new commit object.
1115     """
1116     hash_fields = [('tree_hash', '%T'), ('parent_hash', '%P')]
1117     transfer_fields = [('GIT_AUTHOR_NAME', '%an'),
1118                        ('GIT_AUTHOR_EMAIL', '%ae'),
1119                        ('GIT_AUTHOR_DATE', '%ad'),
1120                        ('GIT_COMMITTER_NAME', '%cn'),
1121                        ('GIT_COMMITTER_EMAIL', '%ce'),
1122                        ('GIT_COMMITER_DATE', '%ct')]
1123     fields = hash_fields + transfer_fields
1124
1125     format_string = '%n'.join([code for _, code in fields] + ['%B'])
1126     result = git.RunGit(self.project_url,
1127         ['log', '--format=%s' % format_string, '-n1', self.sha1])
1128     lines = result.output.splitlines()
1129     field_value = dict(zip([name for name, _ in fields],
1130                            [line.strip() for line in lines]))
1131     commit_body = '\n'.join(lines[len(fields):])
1132
1133     if len(field_value['parent_hash'].split()) != 1:
1134       raise PatchException(self,
1135                            'Branch %s:%s contains merge result %s!'
1136                            % (self.project, self.ref, self.sha1))
1137
1138     extra_env = dict([(field, field_value[field]) for field, _ in
1139                       transfer_fields])
1140
1141     # Reset the commit date to a value that can't conflict; if we
1142     # leave this to git, it's possible for a fast moving set of commit/uploads
1143     # to all occur within the same second (thus the same commit date),
1144     # resulting in the same sha1.
1145     extra_env['GIT_COMMITTER_DATE'] = str(
1146         int(extra_env["GIT_COMMITER_DATE"]) - 1)
1147
1148     result = git.RunGit(
1149         self.project_url,
1150         ['commit-tree', field_value['tree_hash'], '-p',
1151          field_value['parent_hash']],
1152         extra_env=extra_env, input=commit_body)
1153
1154     new_sha1 = result.output.strip()
1155     if new_sha1 == self.sha1:
1156       raise PatchException(
1157           self,
1158           'Internal error!  Carbon copy of %s is the same as original!'
1159           % self.sha1)
1160
1161     return new_sha1
1162
1163   def Upload(self, push_url, remote_ref, carbon_copy=True, dryrun=False,
1164              reviewers=(), cc=()):
1165     """Upload the patch to a remote git branch.
1166
1167     Args:
1168       push_url: Which url to push to.
1169       remote_ref: The ref on the remote host to push to.
1170       carbon_copy: Use a carbon_copy of the local commit.
1171       dryrun: Do the git push with --dry-run
1172       reviewers: Iterable of reviewers to add.
1173       cc: Iterable of people to add to cc.
1174
1175     Returns:
1176       A list of gerrit URLs found in the output
1177     """
1178     if carbon_copy:
1179       ref_to_upload = self._GetCarbonCopy()
1180     else:
1181       ref_to_upload = self.sha1
1182
1183     cmd = ['push']
1184     if reviewers or cc:
1185       pack = '--receive-pack=git receive-pack '
1186       if reviewers:
1187         pack += ' '.join(['--reviewer=' + x for x in reviewers])
1188       if cc:
1189         pack += ' '.join(['--cc=' + x for x in cc])
1190       cmd.append(pack)
1191     cmd += [push_url, '%s:%s' % (ref_to_upload, remote_ref)]
1192     if dryrun:
1193       cmd.append('--dry-run')
1194
1195     lines = git.RunGit(self.project_url, cmd).error.splitlines()
1196     urls = []
1197     for num, line in enumerate(lines):
1198       # Look for output like:
1199       # remote: New Changes:
1200       # remote:   https://chromium-review.googlesource.com/36756
1201       if 'New Changes:' in line:
1202         urls = []
1203         for line in lines[num + 1:]:
1204           line = line.split()
1205           if len(line) != 2 or not line[1].startswith('http'):
1206             break
1207           urls.append(line[-1])
1208         break
1209     return urls
1210
1211
1212 class UploadedLocalPatch(GitRepoPatch):
1213   """Represents an uploaded local patch passed in using --remote-patch."""
1214
1215   def __init__(self, project_url, project, ref, tracking_branch,
1216                original_branch, original_sha1, remote, carbon_copy_sha1=None):
1217     """Initializes an UploadedLocalPatch instance.
1218
1219     Args:
1220       project_url: See GitRepoPatch for documentation.
1221       project: See GitRepoPatch for documentation.
1222       ref: See GitRepoPatch for documentation.
1223       tracking_branch: See GitRepoPatch for documentation.
1224       original_branch: The tracking branch of the local patch.
1225       original_sha1: The sha1 of the local commit.
1226       remote: See GitRepoPatch for documentation.
1227       carbon_copy_sha1: The alternative commit hash to use.
1228     """
1229     GitRepoPatch.__init__(self, project_url, project, ref, tracking_branch,
1230                           remote, sha1=carbon_copy_sha1)
1231     self.original_branch = original_branch
1232     self.original_sha1 = ParseSHA1(original_sha1)
1233     self._original_sha1_valid = False if self.original_sha1 is None else True
1234
1235   def LookupAliases(self):
1236     """Return the list of lookup keys this change is known by."""
1237     l = GitRepoPatch.LookupAliases(self)
1238     if self._original_sha1_valid:
1239       l.append(AddPrefix(self, self.original_sha1))
1240
1241     return l
1242
1243   def __str__(self):
1244     """Returns custom string to identify this patch."""
1245     s = '%s:%s:%s' % (self.project, self.original_branch,
1246                       self.original_sha1[:8])
1247     # TODO(ferringb,build): This gets a bit long in output; should likely
1248     # do some form of truncation to it.
1249     if self._subject_line:
1250       s += ':"%s"' % (self._subject_line,)
1251     return s
1252
1253
1254 class GerritFetchOnlyPatch(GitRepoPatch):
1255   """Object that contains information to cherry-pick a Gerrit CL."""
1256
1257   def __init__(self, project_url, project, ref, tracking_branch, remote,
1258                sha1, change_id, gerrit_number, patch_number, owner_email=None,
1259                fail_count=None, pass_count=None, total_fail_count=None):
1260     """Initializes a GerritFetchOnlyPatch object."""
1261     super(GerritFetchOnlyPatch, self).__init__(
1262         project_url, project, ref, tracking_branch, remote,
1263         change_id=change_id, sha1=sha1)
1264     self.gerrit_number = gerrit_number
1265     self.patch_number = patch_number
1266     # TODO: Do we need three variables for the commit hash?
1267     self.revision = self.commit = self.sha1
1268
1269     # Variables below are required to print the CL link.
1270     self.owner_email = owner_email
1271     self.owner = None
1272     if self.owner_email:
1273       self.owner = self.owner_email.split('@', 1)[0]
1274
1275     self.url = gob_util.GetChangePageUrl(
1276         constants.GERRIT_HOSTS[self.remote], int(self.gerrit_number))
1277     self.fail_count = fail_count
1278     self.pass_count = pass_count
1279     self.total_fail_count = total_fail_count
1280
1281   def _EnsureId(self, commit_message):
1282     """Ensure we have a usable Change-Id
1283
1284     Validate what we received from gerrit against what the commit message
1285     states.
1286     """
1287     # GerritPatch instances get their Change-Id from gerrit
1288     # directly; for this to fail, there is an internal bug.
1289     assert self.id is not None
1290
1291     # For GerritPatches, we still parse the ID- this is
1292     # primarily so we can throw an appropriate warning,
1293     # and also validate our parsing against gerrit's in
1294     # the process.
1295     try:
1296       parsed_id = self._ParseChangeId(commit_message)
1297       if parsed_id != self.change_id:
1298         raise AssertionError(
1299             'For Change-Id %s, sha %s, our parsing of the Change-Id did not '
1300             'match what gerrit told us.  This is an internal bug: either our '
1301             "parsing no longer matches gerrit's, or somehow this instance's "
1302             'stored change_id was invalidly modified.  Our parsing of the '
1303             'Change-Id yielded: %s'
1304             % (self.change_id, self.sha1, parsed_id))
1305
1306     except BrokenChangeID:
1307       cros_build_lib.Warning(
1308           'Change %s, Change-Id %s, sha1 %s lacks a change-id in its commit '
1309           'message.  This can break the ability for any children to depend on '
1310           'this Change as a parent.  Please add the appropriate '
1311           'Change-Id into the commit message to resolve this.',
1312           self, self.change_id, self.sha1)
1313
1314
1315 class GerritPatch(GerritFetchOnlyPatch):
1316   """Object that represents a Gerrit CL."""
1317
1318   def __init__(self, patch_dict, remote, url_prefix):
1319     """Construct a GerritPatch object from Gerrit query results.
1320
1321     Gerrit query JSON fields are documented at:
1322     http://gerrit-documentation.googlecode.com/svn/Documentation/2.2.1/json.html
1323
1324     Args:
1325       patch_dict: A dictionary containing the parsed JSON gerrit query results.
1326       remote: The manifest remote the patched project uses.
1327       url_prefix: The project name will be appended to this to get the full
1328                   repository URL.
1329     """
1330     self.patch_dict = patch_dict
1331     self.url_prefix = url_prefix
1332     current_patch_set = patch_dict.get('currentPatchSet', {})
1333     # id - The CL's ChangeId
1334     # revision - The CL's SHA1 hash.
1335     # number - The CL's gerrit number.
1336     super(GerritPatch, self).__init__(
1337         os.path.join(url_prefix, patch_dict['project']),
1338         patch_dict['project'],
1339         current_patch_set.get('ref'),
1340         patch_dict['branch'],
1341         remote,
1342         current_patch_set.get('revision'),
1343         patch_dict['id'],
1344         ParseGerritNumber(str(patch_dict['number'])),
1345         current_patch_set.get('number'),
1346         owner_email=patch_dict['owner']['email'])
1347
1348     prefix_str = constants.CHANGE_PREFIX[self.remote]
1349     self.gerrit_number_str = '%s%s' % (prefix_str, self.gerrit_number)
1350     self.url = patch_dict['url']
1351     # status - Current state of this change.  Can be one of
1352     # ['NEW', 'SUBMITTED', 'MERGED', 'ABANDONED'].
1353     self.status = patch_dict['status']
1354     self._approvals = []
1355     if 'currentPatchSet' in self.patch_dict:
1356       self._approvals = self.patch_dict['currentPatchSet'].get('approvals', [])
1357     self.approval_timestamp = \
1358         max(x['grantedOn'] for x in self._approvals) if self._approvals else 0
1359     self.commit_message = patch_dict.get('commitMessage')
1360
1361   @staticmethod
1362   def ConvertQueryResults(change, host):
1363     """Converts HTTP query results to the old SQL format.
1364
1365     The HTTP interface to gerrit uses a different json schema from the old SQL
1366     interface.  This method converts data from the new schema to the old one,
1367     typically before passing it to the GerritPatch constructor.
1368
1369     Old interface:
1370       http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/json.html
1371
1372     New interface:
1373       # pylint: disable=C0301
1374       https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#json-entities
1375     """
1376     _convert_tm = lambda tm: calendar.timegm(
1377         time.strptime(tm.partition('.')[0], '%Y-%m-%d %H:%M:%S'))
1378     _convert_user = lambda u: {
1379         'name': u.get('name', '??unknown??'),
1380         'email': u.get('email'),
1381         'username': u.get('name', '??unknown??'),
1382     }
1383     change_id = change['change_id'].split('~')[-1]
1384     patch_dict = {
1385        'project': change['project'],
1386        'branch': change['branch'],
1387        'createdOn': _convert_tm(change['created']),
1388        'lastUpdated': _convert_tm(change['updated']),
1389        'id': change_id,
1390        'owner': _convert_user(change['owner']),
1391        'number': str(change['_number']),
1392        'url': gob_util.GetChangePageUrl(host, change['_number']),
1393        'status': change['status'],
1394        'subject': change.get('subject'),
1395     }
1396     current_revision = change.get('current_revision', '')
1397     current_revision_info = change.get('revisions', {}).get(current_revision)
1398     if current_revision_info:
1399       approvals = []
1400       for label, label_data in change['labels'].iteritems():
1401         # Skip unknown labels.
1402         if label not in constants.GERRIT_ON_BORG_LABELS:
1403           continue
1404         for review_data in label_data.get('all', []):
1405           granted_on = review_data.get('date', change['created'])
1406           approvals.append({
1407               'type': constants.GERRIT_ON_BORG_LABELS[label],
1408               'description': label,
1409               'value': str(review_data.get('value', '0')),
1410               'grantedOn': _convert_tm(granted_on),
1411               'by': _convert_user(review_data),
1412           })
1413
1414       patch_dict['currentPatchSet'] = {
1415           'approvals': approvals,
1416           'ref': current_revision_info['fetch']['http']['ref'],
1417           'revision': current_revision,
1418           'number': str(current_revision_info['_number']),
1419           'draft': current_revision_info.get('draft', False),
1420       }
1421
1422       current_commit = current_revision_info.get('commit')
1423       if current_commit:
1424         patch_dict['commitMessage'] = current_commit['message']
1425         parents = current_commit.get('parents', [])
1426         patch_dict['dependsOn'] = [{'revision': p['commit']} for p in parents]
1427
1428     return patch_dict
1429
1430   def __reduce__(self):
1431     """Used for pickling to re-create patch object."""
1432     return self.__class__, (self.patch_dict.copy(), self.remote,
1433                             self.url_prefix)
1434
1435   def GerritDependencies(self):
1436     """Returns the list of PatchQuery objects that this patch depends on."""
1437     results = []
1438     for d in self.patch_dict.get('dependsOn', []):
1439       gerrit_number = d.get('number')
1440       if gerrit_number is not None:
1441         gerrit_number = ParseGerritNumber(gerrit_number, error_ok=False)
1442
1443       change_id = d.get('id')
1444       if change_id is not None:
1445         change_id = ParseChangeID(change_id, error_ok=False)
1446
1447       sha1 = d.get('revision')
1448       if sha1 is not None:
1449         sha1 = ParseSHA1(sha1, error_ok=False)
1450
1451       if not gerrit_number and not change_id and not sha1:
1452         raise AssertionError(
1453             'While processing the dependencies of change %s, no "number", "id",'
1454             ' or "revision" key found in: %r' % (self.gerrit_number, d))
1455
1456       results.append(
1457           PatchQuery(self.remote, project=self.project,
1458                        tracking_branch=self.tracking_branch,
1459                        gerrit_number=gerrit_number,
1460                        change_id=change_id, sha1=sha1))
1461     return results
1462
1463   def IsAlreadyMerged(self):
1464     """Returns whether the patch has already been merged in Gerrit."""
1465     return self.status == 'MERGED'
1466
1467   def HasApproval(self, field, value):
1468     """Return whether the current patchset has the specified approval.
1469
1470     Args:
1471       field: Which field to check.
1472         'VRIF': Whether patch was verified.
1473         'CRVW': Whether patch was approved.
1474         'COMR': Whether patch was marked ready.
1475         'TBVF': Whether patch was verified by trybot.
1476       value: The expected value of the specified field (as string, or as list
1477              of accepted strings).
1478     """
1479     # All approvals default to '0', so use that if there's no matches.
1480     type_approvals = [x['value'] for x in self._approvals if x['type'] == field]
1481     type_approvals = type_approvals or ['0']
1482     if isinstance(value, (tuple, list)):
1483       return bool(set(value) & set(type_approvals))
1484     else:
1485       return value in type_approvals
1486
1487   def GetLatestApproval(self, field):
1488     """Return most recent value of specific field on the current patchset.
1489
1490     Args:
1491       field: Which field to check ('VRIF', 'CRVW', ...).
1492
1493     Returns:
1494       Most recent field value (as str) or '0' if no such field.
1495     """
1496     # All approvals default to '0', so use that if there's no matches.
1497     type_approvals = [x['value'] for x in self._approvals if x['type'] == field]
1498     return type_approvals[-1] if type_approvals else '0'
1499
1500   def PatchLink(self):
1501     """Return a CL link for this patch."""
1502     return 'CL:%s' % (self.gerrit_number_str,)
1503
1504   def __str__(self):
1505     """Returns custom string to identify this patch."""
1506     s = '%s:%s' % (self.owner, self.gerrit_number_str)
1507     if self.sha1 is not None:
1508       s = '%s:%s%s' % (s, constants.CHANGE_PREFIX[self.remote], self.sha1[:8])
1509     if self._subject_line:
1510       s += ':"%s"' % (self._subject_line,)
1511     return s
1512
1513
1514 def GeneratePatchesFromRepo(git_repo, project, tracking_branch, branch, remote,
1515                             allow_empty=False):
1516   """Create a list of LocalPatch objects from a repo on disk.
1517
1518   Args:
1519     git_repo: The path to the repo.
1520     project: The name of the associated project.
1521     tracking_branch: The remote tracking branch we want to test against.
1522     branch: The name of our local branch, where we will look for patches.
1523     remote: The name of the remote to use. E.g. 'cros'
1524     allow_empty: Whether to allow the case where no patches were specified.
1525   """
1526
1527   result = git.RunGit(
1528       git_repo,
1529       ['rev-list', '--reverse', '%s..%s' % (tracking_branch, branch)])
1530
1531   sha1s = result.output.splitlines()
1532   if not sha1s:
1533     if not allow_empty:
1534       cros_build_lib.Die('No changes found in %s:%s' % (project, branch))
1535     return
1536
1537   for sha1 in sha1s:
1538     yield LocalPatch(os.path.join(git_repo, '.git'),
1539                      project, branch, tracking_branch,
1540                      remote, sha1)
1541
1542
1543 def PrepareLocalPatches(manifest, patches):
1544   """Finish validation of parameters, and save patches to a temp folder.
1545
1546   Args:
1547     manifest: The manifest object for the checkout in question.
1548     patches: A list of user-specified patches, in project:branch form.
1549       cbuildbot pre-processes the patch names before sending them to us,
1550       so we can expect that branch names will always be present.
1551   """
1552   patch_info = []
1553   for patch in patches:
1554     project, branch = patch.split(':')
1555     project_patch_info = []
1556     for checkout in manifest.FindCheckouts(project, only_patchable=True):
1557       tracking_branch = checkout['tracking_branch']
1558       project_dir = checkout.GetPath(absolute=True)
1559       remote = checkout['remote']
1560       project_patch_info.extend(GeneratePatchesFromRepo(
1561           project_dir, project, tracking_branch, branch, remote))
1562
1563     if not project_patch_info:
1564       cros_build_lib.Die('No changes found in %s:%s' % (project, branch))
1565     patch_info.extend(project_patch_info)
1566
1567   return patch_info
1568
1569
1570 def PrepareRemotePatches(patches):
1571   """Generate patch objects from list of --remote-patch parameters.
1572
1573   Args:
1574     patches: A list of --remote-patches strings that the user specified on
1575              the commandline.  Patch strings are colon-delimited.  Patches come
1576              in the format
1577              <project>:<original_branch>:<ref>:<tracking_branch>:<tag>.
1578              A description of each element:
1579              project: The manifest project name that the patch is for.
1580              original_branch: The name of the development branch that the local
1581                               patch came from.
1582              ref: The remote ref that points to the patch.
1583              tracking_branch: The upstream branch that the original_branch was
1584                               tracking.  Should be a manifest branch.
1585              tag: Denotes whether the project is an internal or external
1586                   project.
1587   """
1588   patch_info = []
1589   for patch in patches:
1590     try:
1591       project, original_branch, ref, tracking_branch, tag = patch.split(':')
1592     except ValueError as e:
1593       raise ValueError(
1594           "Unexpected tryjob format.  You may be running an "
1595           "older version of chromite.  Run 'repo sync "
1596           "chromiumos/chromite'.  Error was %s" % e)
1597
1598     if tag not in constants.PATCH_TAGS:
1599       raise ValueError('Bad remote patch format.  Unknown tag %s' % tag)
1600
1601     remote = constants.EXTERNAL_REMOTE
1602     if tag == constants.INTERNAL_PATCH_TAG:
1603       remote = constants.INTERNAL_REMOTE
1604
1605     push_url = constants.GIT_REMOTES[remote]
1606     patch_info.append(UploadedLocalPatch(os.path.join(push_url, project),
1607                                          project, ref, tracking_branch,
1608                                          original_branch,
1609                                          os.path.basename(ref), remote))
1610
1611   return patch_info