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.
5 """Module that handles the processing of patches to the source tree."""
7 from __future__ import print_function
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
21 # We import mock so that we can identify mock.MagicMock instances in tests
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_\-]*$')
38 def ParseSHA1(text, error_ok=True):
39 """Checks if |text| conforms to the SHA1 format and parses it.
42 text: The string to check.
43 error_ok: If set, do not raise an exception if |text| is not a
47 If |text| is a valid SHA1, returns |text|. Otherwise,
48 returns None when |error_ok| is set and raises an exception when
51 valid = git.IsSHA1(text)
52 if not error_ok and not valid:
53 raise ValueError('%s is not a valid SHA1', text)
55 return text if valid else None
58 def ParseGerritNumber(text, error_ok=True):
59 """Checks if |text| conforms to the Gerrit number format and parses it.
62 text: The string to check.
63 error_ok: If set, do not raise an exception if |text| is not a
67 If |text| is a valid Gerrit number, returns |text|. Otherwise,
68 returns None when |error_ok| is set and raises an exception when
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)
75 return text if valid else None
78 def ParseChangeID(text, error_ok=True):
79 """Checks if |text| conforms to the change-ID format and parses it.
81 Change-ID is a string that starts with I/i. E.g.
82 I47ea30385af60ae4cc2acc5d1a283a46423bc6e1
85 text: The string to check.
86 error_ok: If set, do not raise an exception if |text| is not a
90 If |text| is a valid change-ID, returns |text|. Otherwise,
91 returns None when |error_ok| is set and raises an exception when
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()))
98 if not error_ok and not valid:
99 raise ValueError('%s is not a valid change-ID', text)
101 return text if valid else None
104 def ParseFullChangeID(text, error_ok=True):
105 """Checks if |text| conforms to the full change-ID format and parses it.
107 Full change-ID format: project~branch~change-id. E.g.
108 chromiumos/chromite~master~I47ea30385af60ae4cc2acc5d1a283a46423bc6e1
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.
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.
120 fields = text.split('~')
121 if not len(fields) == 3:
123 raise ValueError('%s is not a valid full change-ID', text)
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)):
132 raise ValueError('%s is not a valid full change-ID', text)
136 return project, branch, change_id
139 class PatchException(Exception):
140 """Base exception class all patch exception derive from."""
142 # Unless instances override it, default all exceptions to ToT.
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:
149 "Patch must be a GitRepoPatch derivative; got type %s: %r"
150 % (type(patch), patch))
151 Exception.__init__(self)
153 self.message = message
155 if message is not None:
156 self.args += (message,)
158 def ShortExplanation(self):
159 """Print a short explanation of why the patch failed.
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.
165 return 'failed: %s' % (self.message,)
168 return '%s %s' % (self.patch.PatchLink(), self.ShortExplanation())
171 class ApplyPatchException(PatchException):
172 """Exception thrown if we fail to apply a patch."""
174 def __init__(self, patch, message=None, inflight=False, trivial=False,
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)
183 def _StringifyInflight(self):
184 return 'the current patch series' if self.inflight else 'ToT'
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
190 return '\n\n'.join('- %s' % x for x in self.files)
192 def ShortExplanation(self):
193 s = 'conflicted with %s' % (self._StringifyInflight(),)
195 s += (' because file content merging is disabled for this '
200 s += ('\n\nThe conflicting files are amongst:\n\n'
201 '%s' % (self._StringifyFilenames(),))
203 s += '\n\n%s' % (self.message,)
207 class EbuildConflict(ApplyPatchException):
208 """Exception thrown if two CLs delete the same ebuild."""
210 def __init__(self, patch, inflight, ebuilds):
211 ApplyPatchException.__init__(self, patch, inflight=inflight, files=ebuilds)
212 self.args = (patch, inflight, ebuilds)
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()))
222 class PatchIsEmpty(ApplyPatchException):
223 """Exception thrown if we try to apply an empty patch"""
225 def ShortExplanation(self):
226 return 'had no changes after rebasing to %s.' % (
227 self._StringifyInflight(),)
230 class DependencyError(PatchException):
231 """Thrown when a change cannot be applied due to a failure in a dependency."""
233 def __init__(self, patch, error):
234 """Initialize the error object.
237 patch: The GitRepoPatch instance that this exception concerns.
238 error: A PatchException object that can be stringified to describe
241 PatchException.__init__(self, patch)
242 self.inflight = error.inflight
244 self.args = (patch, error,)
246 def ShortExplanation(self):
247 link = self.error.patch.PatchLink()
248 return ('depends on %s, which %s' % (link, self.error.ShortExplanation()))
251 class BrokenCQDepends(PatchException):
252 """Raised if a patch has a CQ-DEPEND line that is ill formated."""
254 def __init__(self, patch, text, msg=None):
255 PatchException.__init__(self, patch)
258 self.args = (patch, text, msg)
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,)
267 class BrokenChangeID(PatchException):
268 """Raised if a patch has an invalid or missing Change-ID."""
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)
276 def ShortExplanation(self):
277 return 'has a broken ChangeId: %s' % (self.message,)
280 class ChangeMatchesMultipleCheckouts(PatchException):
281 """Raised if the given change matches multiple checkouts."""
283 def ShortExplanation(self):
284 return ('matches multiple checkouts. Does the manifest check out the '
285 'same project and branch to different locations?')
288 class ChangeNotInManifest(PatchException):
289 """Raised if we try to apply a not-in-manifest change."""
291 def ShortExplanation(self):
292 return 'could not be found in the repo manifest.'
295 def MakeChangeId(unusable=False):
296 """Create a random Change-Id.
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.
303 s = "%x" % (random.randint(0, 2 ** 160),)
304 s = s.rjust(_GERRIT_CHANGE_ID_LENGTH, '0')
306 return 'Fake-ID %s' % s
307 return '%s%s' % (_GERRIT_CHANGE_ID_PREFIX, s)
310 class PatchCache(object):
311 """Dict-like object used for tracking a group of patches.
313 This is usable both for existence checks against given string
314 deps, and for change querying.
317 def __init__(self, initial=()):
319 self.Inject(*initial)
321 def Inject(self, *args):
322 """Inject a sequence of changes into this cache."""
324 self.InjectCustomKeys(change.LookupAliases(), change)
326 def InjectCustomKeys(self, keys, change):
327 """Inject a change w/ a list of keys. Generally you want Inject instead.
330 keys: A list of keys to update.
331 change: The change to update the keys to.
334 self._dict[str(key)] = change
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,))
345 def Remove(self, *args):
346 """Remove a change from this cache."""
348 for alias in self._GetAliases(change):
349 self._dict.pop(alias, None)
352 return iter(set(self._dict.itervalues()))
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)
362 def __contains__(self, key):
363 return self[key] is not None
366 """Return a copy of this cache."""
367 return self.__class__(list(self))
370 def StripPrefix(text):
371 """Strips the leading '*' for internal change names.
374 text: text to examine.
377 A tuple of the corresponding remote and the stripped text.
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
388 def AddPrefix(patch, text):
389 """Add the leading '*' to |text| if applicable.
391 Examines patch.remote and adds the prefix to text if applicable.
394 patch: A PatchQuery object to examine.
395 text: The text to add prefix to.
398 |text| with an added prefix for internal patches; otherwise, returns text.
400 return '%s%s' % (constants.CHANGE_PREFIX[patch.remote], text)
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.
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.
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.
423 raise ValueError("ParsePatchDep invoked with an empty value: %r"
425 # Deal w/ CL: targets.
426 if text.upper().startswith("CL:"):
427 if not text.startswith("CL:"):
429 "ParsePatchDep: 'CL:' must be upper case: %r"
433 # Strip the prefix to determine the remote.
434 remote, text = StripPrefix(text)
436 parsed = ParseFullChangeID(text)
438 if no_full_change_id:
440 'ParsePatchDep: Full Change-ID is not allowed: %r.' % original_text)
442 project, branch, change_id = parsed
443 return PatchQuery(remote, project=project, tracking_branch=branch,
446 parsed = ParseChangeID(text)
450 'ParsePatchDep: Change-ID is not allowed: %r.' % original_text)
452 return PatchQuery(remote, change_id=parsed)
454 parsed = ParseGerritNumber(text)
458 'ParsePatchDep: Gerrit number is not allowed: %r.' % original_text)
460 return PatchQuery(remote, gerrit_number=parsed)
462 parsed = ParseSHA1(text)
466 'ParsePatchDep: SHA1 is not allowed: %r.' % original_text)
468 return PatchQuery(remote, sha1=parsed)
470 raise ValueError('Cannot parse the dependency: %s' % original_text)
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)
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)
492 class PatchQuery(object):
493 """Store information about a patch.
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
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.
503 def __init__(self, remote, project=None, tracking_branch=None, change_id=None,
504 sha1=None, gerrit_number=None):
505 """Initializes a PatchQuery instance.
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.
516 self.tracking_branch = None
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.
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)
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
544 if not self.full_change_id:
545 self._SetFullChangeID()
547 if self.full_change_id:
548 self.id = AddPrefix(self, self.full_change_id)
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)
555 def LookupAliases(self):
556 """Returns the list of lookup keys to query a PatchCache.
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
564 if self.gerrit_number:
565 l.append(self.gerrit_number)
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)
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.
576 return ['%s%s' % (constants.CHANGE_PREFIX[self.remote], x)
577 for x in l if x is not None]
579 def ToGerritQueryText(self):
580 """Generate a text used to query Gerrit.
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.
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
592 # SHA1 may not not be unique, but we don't handle that here.
595 # Fall back to use Change-Id, which is not unique.
596 return self.change_id
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.
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')
607 """Returns a hash to be used in a set or a list."""
611 return hash((self.remote, self.project, self.tracking_branch,
612 self.gerrit_number, self.change_id, self.sha1))
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
620 if self.id is not None:
621 return self.id == other.id
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))
629 class GitRepoPatch(PatchQuery):
630 """Representing a patch from a branch of a local or remote git repository."""
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
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*$',
643 def __init__(self, project_url, project, ref, tracking_branch, remote,
644 sha1=None, change_id=None):
645 """Initialization of abstract Patch class.
648 project_url: The url of the git repo (can be local or remote) to pull the
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.
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
666 self._is_fetched = set()
670 """Whether patch is to an internal cros project."""
671 return self.remote == constants.INTERNAL_REMOTE
673 def Fetch(self, git_repo):
674 """Fetch this patch into the given git repository.
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
681 While doing so, we'll load the commit message and Change-Id if not
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).
688 git_repo: The git repository to fetch this patch into.
691 The sha1 of the patch.
693 git_repo = os.path.normpath(git_repo)
694 if git_repo in self._is_fetched:
699 git_repo, ['log', '--pretty=format:%H%x00%s%x00%B', '-n1', rev],
701 if ret.returncode != 0:
702 return None, None, None
703 output = ret.output.split('\0')
705 return None, None, None
706 return [unicode(x.strip(), 'ascii', 'ignore') for x in output]
709 if self.sha1 is not None:
710 # See if we've already got the object.
711 sha1, subject, msg = _PullData(self.sha1)
714 git.RunGit(git_repo, ['fetch', '-f', self.project_url, self.ref])
715 sha1, subject, msg = _PullData(self.sha1 or 'FETCH_HEAD')
717 sha1 = ParseSHA1(sha1, error_ok=False)
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))
728 self.commit_message = msg
729 self._subject_line = subject
730 self._is_fetched.add(git_repo)
733 def GetDiffStatus(self, git_repo):
734 """Isolate the paths and modifications this patch induces.
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.
742 git_repo: Git repository to operate upon.
745 A dictionary of path -> modification_type tuples. See
746 `git log --help`, specifically the --diff-filter section for details.
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
758 if e.result.returncode != 128:
761 lines = lines.output.splitlines()
762 return dict(line.split('\t', 1)[::-1] for line in lines)
764 def CherryPick(self, git_repo, trivial=False, inflight=False,
766 """Attempts to cherry-pick the given rev into branch.
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.
775 A ApplyPatchException if the request couldn't be handled.
777 # Note the --ff; we do *not* want the sha1 to change unless it
779 cmd = ['cherry-pick', '--strategy', 'resolve', '--ff']
781 cmd += ['-X', 'trivial']
782 cmd.append(self.sha1)
784 reset_target = None if leave_dirty else 'HEAD'
786 git.RunGit(git_repo, cmd)
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",
795 raise ApplyPatchException(
796 self, inflight=inflight,
797 message=("Unknown exit code %s returned from cherry-pick "
798 "command: %s" % (ret, error)))
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
803 result = git.RunGit(git_repo,
804 ['diff', '--name-only', '--diff-filter=U'])
806 # Output is one line per filename.
807 conflicts = result.output.splitlines()
809 # No conflicts means the git repo is in a pristine state.
811 raise PatchIsEmpty(self, inflight=inflight)
813 # Making it here means that it wasn't trivial, nor was it already
816 raise ApplyPatchException(self, inflight=inflight, files=conflicts)
818 # ret=2 handling, this deals w/ trivial conflicts; including figuring
819 # out if it was trivial induced or not.
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.
828 self.CherryPick(git_repo, trivial=False, inflight=inflight)
829 # Since it succeeded, we need to rewind.
830 reset_target = 'HEAD^'
832 raise ApplyPatchException(self, trivial=True, inflight=inflight)
834 if reset_target is not None:
835 git.RunGit(git_repo, ['reset', '--hard', reset_target],
838 def Apply(self, git_repo, upstream, revision=None, trivial=False):
839 """Apply patch into a standalone git repo.
841 The git repo does not need to be part of a repo checkout.
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.
852 cros_build_lib.Info('Attempting to cherry-pick change %s', self)
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])
859 ['checkout', '-b', constants.PATCH_BRANCH, '-t', upstream])
861 git.RunGit(git_repo, ['reset', '--hard', revision])
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.
867 git.RunGit(git_repo, ['rev-list', '-n1', x]).output.strip()
868 for x in (upstream, 'HEAD')]
869 inflight = (head != upstream)
871 self._FindEbuildConflicts(git_repo, upstream, inflight=inflight)
875 self.CherryPick(git_repo, trivial=trivial, inflight=inflight)
878 except ApplyPatchException:
881 git.RunGit(git_repo, ['checkout', '-f', '--detach', upstream])
883 self.CherryPick(git_repo, trivial=trivial, inflight=False)
884 # Making it here means that it was an inflight issue; throw the original.
887 # Ensure we're on the correct branch on the way out.
889 git.RunGit(git_repo, ['checkout', '-f', constants.PATCH_BRANCH],
892 def ApplyAgainstManifest(self, manifest, trivial=False):
893 """Applies the patch against the specified manifest.
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.
900 ApplyPatchException: If the patch failed to apply.
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,
914 def GerritDependencies(self):
915 """Returns a list of Gerrit change numbers that this patch depends on.
917 Ordinary patches have no Gerrit-style dependencies since they're not
918 from Gerrit at all. See GerritPatch.GerritDependencies instead.
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.
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.
931 if self.id is not None:
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.',
943 self._SetID(self.sha1)
947 def _ParseChangeId(self, data):
948 """Parse a Change-Id out of a block of text.
950 Note that the returned content is *not* ran through FormatChangeId;
951 this is left up to the invoker.
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,),
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
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)
969 return ParseChangeID(change_id_match)
971 def PaladinDependencies(self, git_repo):
972 """Returns an ordered list of dependencies based on the Commit Message.
974 Parses the Commit message for this change looking for lines that follow
977 CQ-DEPEND=change_num+ e.g.
979 A commit which depends on a couple others.
983 CQ-DEPEND=10001,10002
986 cros_build_lib.Debug('Checking for CQ-DEPEND dependencies for change %s',
989 # Only fetch the commit message if needed.
990 if self.commit_message is None:
994 dependencies = GetPaladinDeps(self.commit_message)
995 except ValueError as e:
996 raise BrokenCQDepends(self, str(e))
999 cros_build_lib.Debug('Found %s Paladin dependencies for change %s',
1003 def _FindEbuildConflicts(self, git_repo, upstream, inflight=False):
1004 """Verify that there are no ebuild conflicts in the given |git_repo|.
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.
1013 git_repo: The directory to examine.
1014 upstream: The upstream git revision.
1015 inflight: Whether we currently have patches applied to this repository.
1017 ebuilds = [path for (path, mtype) in
1018 self.GetDiffStatus(git_repo).iteritems()
1019 if mtype == 'D' and path.endswith('.ebuild')]
1021 conflicts = self._FindMissingFiles(git_repo, 'HEAD', ebuilds)
1026 # If we're inflight, test against ToT for an accurate error message.
1027 tot_conflicts = self._FindMissingFiles(git_repo, upstream, ebuilds)
1030 conflicts = tot_conflicts
1032 raise EbuildConflict(self, inflight=inflight, ebuilds=conflicts)
1034 def _FindMissingFiles(self, git_repo, tree_revision, files):
1035 """Return a list of the |files| that are missing in |tree_revision|.
1038 git_repo: Git repository to work in.
1039 tree_revision: Revision of the tree to use.
1040 files: Files to look for.
1043 A list of the |files| that are missing in |tree_revision|.
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]
1053 def GetCheckout(self, manifest, strict=True):
1054 """Get the ProjectCheckout associated with this patch.
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.
1062 ChangeMatchesMultipleCheckouts if there are multiple checkouts that
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)
1071 raise ChangeNotInManifest(self)
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
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,)
1094 class LocalPatch(GitRepoPatch):
1095 """Represents patch coming from an on-disk git repo."""
1097 def __init__(self, project_url, project, ref, tracking_branch, remote,
1099 GitRepoPatch.__init__(self, project_url, project, ref, tracking_branch,
1101 # Initialize our commit message/ChangeId now, since we know we have
1102 # access to the data right now.
1103 self.Fetch(project_url)
1105 def _GetCarbonCopy(self):
1106 """Returns a copy of this commit object, with a different sha1.
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/*.
1114 The sha1 of the new commit object.
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
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):])
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))
1138 extra_env = dict([(field, field_value[field]) for field, _ in
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)
1148 result = git.RunGit(
1150 ['commit-tree', field_value['tree_hash'], '-p',
1151 field_value['parent_hash']],
1152 extra_env=extra_env, input=commit_body)
1154 new_sha1 = result.output.strip()
1155 if new_sha1 == self.sha1:
1156 raise PatchException(
1158 'Internal error! Carbon copy of %s is the same as original!'
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.
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.
1176 A list of gerrit URLs found in the output
1179 ref_to_upload = self._GetCarbonCopy()
1181 ref_to_upload = self.sha1
1185 pack = '--receive-pack=git receive-pack '
1187 pack += ' '.join(['--reviewer=' + x for x in reviewers])
1189 pack += ' '.join(['--cc=' + x for x in cc])
1191 cmd += [push_url, '%s:%s' % (ref_to_upload, remote_ref)]
1193 cmd.append('--dry-run')
1195 lines = git.RunGit(self.project_url, cmd).error.splitlines()
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:
1203 for line in lines[num + 1:]:
1205 if len(line) != 2 or not line[1].startswith('http'):
1207 urls.append(line[-1])
1212 class UploadedLocalPatch(GitRepoPatch):
1213 """Represents an uploaded local patch passed in using --remote-patch."""
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.
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.
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
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))
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,)
1254 class GerritFetchOnlyPatch(GitRepoPatch):
1255 """Object that contains information to cherry-pick a Gerrit CL."""
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
1269 # Variables below are required to print the CL link.
1270 self.owner_email = owner_email
1272 if self.owner_email:
1273 self.owner = self.owner_email.split('@', 1)[0]
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
1281 def _EnsureId(self, commit_message):
1282 """Ensure we have a usable Change-Id
1284 Validate what we received from gerrit against what the commit message
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
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
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))
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)
1315 class GerritPatch(GerritFetchOnlyPatch):
1316 """Object that represents a Gerrit CL."""
1318 def __init__(self, patch_dict, remote, url_prefix):
1319 """Construct a GerritPatch object from Gerrit query results.
1321 Gerrit query JSON fields are documented at:
1322 http://gerrit-documentation.googlecode.com/svn/Documentation/2.2.1/json.html
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
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'],
1342 current_patch_set.get('revision'),
1344 ParseGerritNumber(str(patch_dict['number'])),
1345 current_patch_set.get('number'),
1346 owner_email=patch_dict['owner']['email'])
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')
1362 def ConvertQueryResults(change, host):
1363 """Converts HTTP query results to the old SQL format.
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.
1370 http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/json.html
1373 # pylint: disable=C0301
1374 https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#json-entities
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??'),
1383 change_id = change['change_id'].split('~')[-1]
1385 'project': change['project'],
1386 'branch': change['branch'],
1387 'createdOn': _convert_tm(change['created']),
1388 'lastUpdated': _convert_tm(change['updated']),
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'),
1396 current_revision = change.get('current_revision', '')
1397 current_revision_info = change.get('revisions', {}).get(current_revision)
1398 if current_revision_info:
1400 for label, label_data in change['labels'].iteritems():
1401 # Skip unknown labels.
1402 if label not in constants.GERRIT_ON_BORG_LABELS:
1404 for review_data in label_data.get('all', []):
1405 granted_on = review_data.get('date', change['created'])
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),
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),
1422 current_commit = current_revision_info.get('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]
1430 def __reduce__(self):
1431 """Used for pickling to re-create patch object."""
1432 return self.__class__, (self.patch_dict.copy(), self.remote,
1435 def GerritDependencies(self):
1436 """Returns the list of PatchQuery objects that this patch depends on."""
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)
1443 change_id = d.get('id')
1444 if change_id is not None:
1445 change_id = ParseChangeID(change_id, error_ok=False)
1447 sha1 = d.get('revision')
1448 if sha1 is not None:
1449 sha1 = ParseSHA1(sha1, error_ok=False)
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))
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))
1463 def IsAlreadyMerged(self):
1464 """Returns whether the patch has already been merged in Gerrit."""
1465 return self.status == 'MERGED'
1467 def HasApproval(self, field, value):
1468 """Return whether the current patchset has the specified approval.
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).
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))
1485 return value in type_approvals
1487 def GetLatestApproval(self, field):
1488 """Return most recent value of specific field on the current patchset.
1491 field: Which field to check ('VRIF', 'CRVW', ...).
1494 Most recent field value (as str) or '0' if no such field.
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'
1500 def PatchLink(self):
1501 """Return a CL link for this patch."""
1502 return 'CL:%s' % (self.gerrit_number_str,)
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,)
1514 def GeneratePatchesFromRepo(git_repo, project, tracking_branch, branch, remote,
1516 """Create a list of LocalPatch objects from a repo on disk.
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.
1527 result = git.RunGit(
1529 ['rev-list', '--reverse', '%s..%s' % (tracking_branch, branch)])
1531 sha1s = result.output.splitlines()
1534 cros_build_lib.Die('No changes found in %s:%s' % (project, branch))
1538 yield LocalPatch(os.path.join(git_repo, '.git'),
1539 project, branch, tracking_branch,
1543 def PrepareLocalPatches(manifest, patches):
1544 """Finish validation of parameters, and save patches to a temp folder.
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.
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))
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)
1570 def PrepareRemotePatches(patches):
1571 """Generate patch objects from list of --remote-patch parameters.
1574 patches: A list of --remote-patches strings that the user specified on
1575 the commandline. Patch strings are colon-delimited. Patches come
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
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
1589 for patch in patches:
1591 project, original_branch, ref, tracking_branch, tag = patch.split(':')
1592 except ValueError as e:
1594 "Unexpected tryjob format. You may be running an "
1595 "older version of chromite. Run 'repo sync "
1596 "chromiumos/chromite'. Error was %s" % e)
1598 if tag not in constants.PATCH_TAGS:
1599 raise ValueError('Bad remote patch format. Unknown tag %s' % tag)
1601 remote = constants.EXTERNAL_REMOTE
1602 if tag == constants.INTERNAL_PATCH_TAG:
1603 remote = constants.INTERNAL_REMOTE
1605 push_url = constants.GIT_REMOTES[remote]
1606 patch_info.append(UploadedLocalPatch(os.path.join(push_url, project),
1607 project, ref, tracking_branch,
1609 os.path.basename(ref), remote))