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