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."""
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
19 # We import mock so that we can identify mock.MagicMock instances in tests
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_\-]*$')
36 def ParseSHA1(text, error_ok=True):
37 """Checks if |text| conforms to the SHA1 format and parses it.
40 text: The string to check.
41 error_ok: If set, do not raise an exception if |text| is not a
45 If |text| is a valid SHA1, returns |text|. Otherwise,
46 returns None when |error_ok| is set and raises an exception when
49 valid = git.IsSHA1(text)
50 if not error_ok and not valid:
51 raise ValueError('%s is not a valid SHA1', text)
53 return text if valid else None
56 def ParseGerritNumber(text, error_ok=True):
57 """Checks if |text| conforms to the Gerrit number format and parses it.
60 text: The string to check.
61 error_ok: If set, do not raise an exception if |text| is not a
65 If |text| is a valid Gerrit number, returns |text|. Otherwise,
66 returns None when |error_ok| is set and raises an exception when
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)
73 return text if valid else None
76 def ParseChangeID(text, error_ok=True):
77 """Checks if |text| conforms to the change-ID format and parses it.
79 Change-ID is a string that starts with I/i. E.g.
80 I47ea30385af60ae4cc2acc5d1a283a46423bc6e1
83 text: The string to check.
84 error_ok: If set, do not raise an exception if |text| is not a
88 If |text| is a valid change-ID, returns |text|. Otherwise,
89 returns None when |error_ok| is set and raises an exception when
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()))
96 if not error_ok and not valid:
97 raise ValueError('%s is not a valid change-ID', text)
99 return text if valid else None
102 def ParseFullChangeID(text, error_ok=True):
103 """Checks if |text| conforms to the full change-ID format and parses it.
105 Full change-ID format: project~branch~change-id. E.g.
106 chromiumos/chromite~master~I47ea30385af60ae4cc2acc5d1a283a46423bc6e1
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.
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.
118 fields = text.split('~')
119 if not len(fields) == 3:
121 raise ValueError('%s is not a valid full change-ID', text)
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)):
130 raise ValueError('%s is not a valid full change-ID', text)
134 return project, branch, change_id
137 class PatchException(Exception):
138 """Base exception class all patch exception derive from."""
140 # Unless instances override it, default all exceptions to ToT.
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:
147 "Patch must be a GitRepoPatch derivative; got type %s: %r"
148 % (type(patch), patch))
149 Exception.__init__(self)
151 self.message = message
153 if message is not None:
154 self.args += (message,)
156 def ShortExplanation(self):
157 """Print a short explanation of why the patch failed.
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.
163 return 'failed: %s' % (self.message,)
166 return '%s %s' % (self.patch.PatchLink(), self.ShortExplanation())
169 class ApplyPatchException(PatchException):
170 """Exception thrown if we fail to apply a patch."""
172 def __init__(self, patch, message=None, inflight=False, trivial=False,
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)
181 def _StringifyInflight(self):
182 return 'the current patch series' if self.inflight else 'ToT'
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
188 return '\n\n'.join('- %s' % x for x in self.files)
190 def ShortExplanation(self):
191 s = 'conflicted with %s' % (self._StringifyInflight(),)
193 s += (' because file content merging is disabled for this '
198 s += ('\n\nThe conflicting files are amongst:\n\n'
199 '%s' % (self._StringifyFilenames(),))
201 s += '\n\n%s' % (self.message,)
205 class EbuildConflict(ApplyPatchException):
206 """Exception thrown if two CLs delete the same ebuild."""
208 def __init__(self, patch, inflight, ebuilds):
209 ApplyPatchException.__init__(self, patch, inflight=inflight, files=ebuilds)
210 self.args = (patch, inflight, ebuilds)
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()))
220 class PatchAlreadyApplied(ApplyPatchException):
221 """Exception thrown if we fail to apply an already applied patch"""
223 def ShortExplanation(self):
224 return 'conflicted with %s because it\'s already committed.' % (
225 self._StringifyInflight(),)
228 class DependencyError(PatchException):
229 """Thrown when a change cannot be applied due to a failure in a dependency."""
231 def __init__(self, patch, error):
232 """Initialize the error object.
235 patch: The GitRepoPatch instance that this exception concerns.
236 error: A PatchException object that can be stringified to describe
239 PatchException.__init__(self, patch)
240 self.inflight = error.inflight
242 self.args = (patch, error,)
244 def ShortExplanation(self):
245 link = self.error.patch.PatchLink()
246 return ('depends on %s, which %s' % (link, self.error.ShortExplanation()))
249 class BrokenCQDepends(PatchException):
250 """Raised if a patch has a CQ-DEPEND line that is ill formated."""
252 def __init__(self, patch, text, msg=None):
253 PatchException.__init__(self, patch)
256 self.args = (patch, text, msg)
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,)
265 class BrokenChangeID(PatchException):
266 """Raised if a patch has an invalid or missing Change-ID."""
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)
274 def ShortExplanation(self):
275 return 'has a broken ChangeId: %s' % (self.message,)
278 class ChangeMatchesMultipleCheckouts(PatchException):
279 """Raised if the given change matches multiple checkouts."""
281 def ShortExplanation(self):
282 return ('matches multiple checkouts. Does the manifest check out the '
283 'same project and branch to different locations?')
286 class ChangeNotInManifest(PatchException):
287 """Raised if we try to apply a not-in-manifest change."""
289 def ShortExplanation(self):
290 return 'could not be found in the repo manifest.'
293 def MakeChangeId(unusable=False):
294 """Create a random Change-Id.
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.
301 s = "%x" % (random.randint(0, 2 ** 160),)
302 s = s.rjust(_GERRIT_CHANGE_ID_LENGTH, '0')
304 return 'Fake-ID %s' % s
305 return '%s%s' % (_GERRIT_CHANGE_ID_PREFIX, s)
308 class PatchCache(object):
309 """Dict-like object used for tracking a group of patches.
311 This is usable both for existence checks against given string
312 deps, and for change querying.
315 def __init__(self, initial=()):
317 self.Inject(*initial)
319 def Inject(self, *args):
320 """Inject a sequence of changes into this cache."""
322 self.InjectCustomKeys(change.LookupAliases(), change)
324 def InjectCustomKeys(self, keys, change):
325 """Inject a change w/ a list of keys. Generally you want Inject instead.
328 keys: A list of keys to update.
329 change: The change to update the keys to.
332 self._dict[str(key)] = change
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,))
343 def Remove(self, *args):
344 """Remove a change from this cache."""
346 for alias in self._GetAliases(change):
347 self._dict.pop(alias, None)
350 return iter(set(self._dict.itervalues()))
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)
360 def __contains__(self, key):
361 return self[key] is not None
364 """Return a copy of this cache."""
365 return self.__class__(list(self))
368 def StripPrefix(text):
369 """Strips the leading '*' for internal change names.
372 text: text to examine.
375 A tuple of the corresponding remote and the stripped text.
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
386 def AddPrefix(patch, text):
387 """Add the leading '*' to |text| if applicable.
389 Examines patch.remote and adds the prefix to text if applicable.
392 patch: A PatchQuery object to examine.
393 text: The text to add prefix to.
396 |text| with an added prefix for internal patches; otherwise, returns text.
398 return '%s%s' % (constants.CHANGE_PREFIX[patch.remote], text)
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.
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.
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.
421 raise ValueError("ParsePatchDep invoked with an empty value: %r"
423 # Deal w/ CL: targets.
424 if text.upper().startswith("CL:"):
425 if not text.startswith("CL:"):
427 "ParsePatchDep: 'CL:' must be upper case: %r"
431 # Strip the prefix to determine the remote.
432 remote, text = StripPrefix(text)
434 parsed = ParseFullChangeID(text)
436 if no_full_change_id:
438 'ParsePatchDep: Full Change-ID is not allowed: %r.' % original_text)
440 project, branch, change_id = parsed
441 return PatchQuery(remote, project=project, tracking_branch=branch,
444 parsed = ParseChangeID(text)
448 'ParsePatchDep: Change-ID is not allowed: %r.' % original_text)
450 return PatchQuery(remote, change_id=parsed)
452 parsed = ParseGerritNumber(text)
456 'ParsePatchDep: Gerrit number is not allowed: %r.' % original_text)
458 return PatchQuery(remote, gerrit_number=parsed)
460 parsed = ParseSHA1(text)
464 'ParsePatchDep: SHA1 is not allowed: %r.' % original_text)
466 return PatchQuery(remote, sha1=parsed)
468 raise ValueError('Cannot parse the dependency: %s' % original_text)
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)
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)
490 class PatchQuery(object):
491 """Store information about a patch.
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
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.
502 def __init__(self, remote, project=None, tracking_branch=None, change_id=None,
503 sha1=None, gerrit_number=None):
504 """Initializes a PatchQuery instance.
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.
515 self.tracking_branch = None
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.
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)
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
543 if not self.full_change_id:
544 self._SetFullChangeID()
546 if self.full_change_id:
547 self.id = AddPrefix(self, self.full_change_id)
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)
554 def LookupAliases(self):
555 """Returns the list of lookup keys to query a PatchCache.
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
563 if self.gerrit_number:
564 l.append(self.gerrit_number)
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)
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.
575 return ['%s%s' % (constants.CHANGE_PREFIX[self.remote], x)
576 for x in l if x is not None]
578 def ToGerritQueryText(self):
579 """Generate a text used to query Gerrit.
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.
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
591 # SHA1 may not not be unique, but we don't handle that here.
594 # Fall back to use Change-Id, which is not unique.
595 return self.change_id
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.
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')
606 """Returns a hash to be used in a set or a list."""
610 return hash((self.remote, self.project, self.tracking_branch,
611 self.gerrit_number, self.change_id, self.sha1))
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
619 if self.id is not None:
620 return self.id == other.id
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))
628 class GitRepoPatch(PatchQuery):
629 """Representing a patch from a branch of a local or remote git repository."""
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
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*$',
642 def __init__(self, project_url, project, ref, tracking_branch, remote,
643 sha1=None, change_id=None):
644 """Initialization of abstract Patch class.
647 project_url: The url of the git repo (can be local or remote) to pull the
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.
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
665 self._is_fetched = set()
669 """Whether patch is to an internal cros project."""
670 return self.remote == constants.INTERNAL_REMOTE
672 def Fetch(self, git_repo):
673 """Fetch this patch into the given git repository.
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
680 While doing so, we'll load the commit message and Change-Id if not
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).
687 git_repo: The git repository to fetch this patch into.
690 The sha1 of the patch.
692 git_repo = os.path.normpath(git_repo)
693 if git_repo in self._is_fetched:
698 git_repo, ['log', '--pretty=format:%H%x00%s%x00%B', '-n1', rev],
700 if ret.returncode != 0:
701 return None, None, None
702 output = ret.output.split('\0')
704 return None, None, None
705 return [unicode(x.strip(), 'ascii', 'ignore') for x in output]
708 if self.sha1 is not None:
709 # See if we've already got the object.
710 sha1, subject, msg = _PullData(self.sha1)
713 git.RunGit(git_repo, ['fetch', self.project_url, self.ref])
714 sha1, subject, msg = _PullData(self.sha1 or 'FETCH_HEAD')
716 sha1 = ParseSHA1(sha1, error_ok=False)
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))
727 self.commit_message = msg
728 self._subject_line = subject
729 self._is_fetched.add(git_repo)
732 def GetDiffStatus(self, git_repo):
733 """Isolate the paths and modifications this patch induces.
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.
741 git_repo: Git repository to operate upon.
744 A dictionary of path -> modification_type tuples. See
745 `git log --help`, specifically the --diff-filter section for details.
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
757 if e.result.returncode != 128:
760 lines = lines.output.splitlines()
761 return dict(line.split('\t', 1)[::-1] for line in lines)
763 def CherryPick(self, git_repo, trivial=False, inflight=False,
765 """Attempts to cherry-pick the given rev into branch.
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.
774 A ApplyPatchException if the request couldn't be handled.
776 # Note the --ff; we do *not* want the sha1 to change unless it
778 cmd = ['cherry-pick', '--strategy', 'resolve', '--ff']
780 cmd += ['-X', 'trivial']
781 cmd.append(self.sha1)
783 reset_target = None if leave_dirty else 'HEAD'
785 git.RunGit(git_repo, cmd)
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",
794 raise ApplyPatchException(
795 self, inflight=inflight,
796 message=("Unknown exit code %s returned from cherry-pick "
797 "command: %s" % (ret, error)))
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
802 result = git.RunGit(git_repo,
803 ['diff', '--name-only', '--diff-filter=U'])
805 # Output is one line per filename.
806 conflicts = result.output.splitlines()
808 # No conflicts means the git repo is in a pristine state.
810 raise PatchAlreadyApplied(self, inflight=inflight)
812 # Making it here means that it wasn't trivial, nor was it already
815 raise ApplyPatchException(self, inflight=inflight, files=conflicts)
817 # ret=2 handling, this deals w/ trivial conflicts; including figuring
818 # out if it was trivial induced or not.
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.
827 self.CherryPick(git_repo, trivial=False, inflight=inflight)
828 # Since it succeeded, we need to rewind.
829 reset_target = 'HEAD^'
831 raise ApplyPatchException(self, trivial=True, inflight=inflight)
833 if reset_target is not None:
834 git.RunGit(git_repo, ['reset', '--hard', reset_target],
837 def Apply(self, git_repo, upstream, trivial=False):
838 """Apply patch into a standalone git repo.
840 The git repo does not need to be part of a repo checkout.
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.
850 cros_build_lib.Info('Attempting to cherry-pick change %s', self)
852 if not git.DoesLocalBranchExist(git_repo, constants.PATCH_BRANCH):
853 cmd = ['checkout', '-b', constants.PATCH_BRANCH, '-t', upstream]
855 cmd = ['checkout', '-f', constants.PATCH_BRANCH]
856 git.RunGit(git_repo, cmd)
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.
862 git.RunGit(git_repo, ['rev-list', '-n1', x]).output.strip()
863 for x in (upstream, 'HEAD')]
864 inflight = (head != upstream)
866 self._FindEbuildConflicts(git_repo, upstream, inflight=inflight)
870 self.CherryPick(git_repo, trivial=trivial, inflight=inflight)
873 except ApplyPatchException:
876 git.RunGit(git_repo, ['checkout', '-f', '--detach', upstream])
878 self.CherryPick(git_repo, trivial=trivial, inflight=False)
879 # Making it here means that it was an inflight issue; throw the original.
882 # Ensure we're on the correct branch on the way out.
884 git.RunGit(git_repo, ['checkout', '-f', constants.PATCH_BRANCH],
887 def ApplyAgainstManifest(self, manifest, trivial=False):
888 """Applies the patch against the specified manifest.
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.
895 ApplyPatchException: If the patch failed to apply.
897 checkout = self.GetCheckout(manifest)
898 upstream = checkout['tracking_branch']
899 self.Apply(checkout.GetPath(absolute=True), upstream, trivial=trivial)
901 def GerritDependencies(self):
902 """Returns a list of Gerrit change numbers that this patch depends on.
904 Ordinary patches have no Gerrit-style dependencies since they're not
905 from Gerrit at all. See GerritPatch.GerritDependencies instead.
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.
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.
918 if self.id is not None:
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.',
930 self._SetID(self.sha1)
934 def _ParseChangeId(self, data):
935 """Parse a Change-Id out of a block of text.
937 Note that the returned content is *not* ran through FormatChangeId;
938 this is left up to the invoker.
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,),
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
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)
956 return ParseChangeID(change_id_match)
958 def PaladinDependencies(self, git_repo):
959 """Returns an ordered list of dependencies based on the Commit Message.
961 Parses the Commit message for this change looking for lines that follow
964 CQ-DEPEND=change_num+ e.g.
966 A commit which depends on a couple others.
970 CQ-DEPEND=10001,10002
973 cros_build_lib.Debug('Checking for CQ-DEPEND dependencies for change %s',
976 # Only fetch the commit message if needed.
977 if self.commit_message is None:
981 dependencies = GetPaladinDeps(self.commit_message)
982 except ValueError as e:
983 raise BrokenCQDepends(self, str(e))
986 cros_build_lib.Debug('Found %s Paladin dependencies for change %s',
990 def _FindEbuildConflicts(self, git_repo, upstream, inflight=False):
991 """Verify that there are no ebuild conflicts in the given |git_repo|.
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.
1000 git_repo: The directory to examine.
1001 upstream: The upstream git revision.
1002 inflight: Whether we currently have patches applied to this repository.
1004 ebuilds = [path for (path, mtype) in
1005 self.GetDiffStatus(git_repo).iteritems()
1006 if mtype == 'D' and path.endswith('.ebuild')]
1008 conflicts = self._FindMissingFiles(git_repo, 'HEAD', ebuilds)
1013 # If we're inflight, test against ToT for an accurate error message.
1014 tot_conflicts = self._FindMissingFiles(git_repo, upstream, ebuilds)
1017 conflicts = tot_conflicts
1019 raise EbuildConflict(self, inflight=inflight, ebuilds=conflicts)
1021 def _FindMissingFiles(self, git_repo, tree_revision, files):
1022 """Return a list of the |files| that are missing in |tree_revision|.
1025 git_repo: Git repository to work in.
1026 tree_revision: Revision of the tree to use.
1027 files: Files to look for.
1030 A list of the |files| that are missing in |tree_revision|.
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]
1040 def GetCheckout(self, manifest, strict=True):
1041 """Get the ProjectCheckout associated with this patch.
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.
1049 ChangeMatchesMultipleCheckouts if there are multiple checkouts that
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)
1058 raise ChangeNotInManifest(self)
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
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,)
1081 class LocalPatch(GitRepoPatch):
1082 """Represents patch coming from an on-disk git repo."""
1084 def __init__(self, project_url, project, ref, tracking_branch, remote,
1086 GitRepoPatch.__init__(self, project_url, project, ref, tracking_branch,
1088 # Initialize our commit message/ChangeId now, since we know we have
1089 # access to the data right now.
1090 self.Fetch(project_url)
1092 def _GetCarbonCopy(self):
1093 """Returns a copy of this commit object, with a different sha1.
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/*.
1101 The sha1 of the new commit object.
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
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):])
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))
1125 extra_env = dict([(field, field_value[field]) for field, _ in
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)
1135 result = git.RunGit(
1137 ['commit-tree', field_value['tree_hash'], '-p',
1138 field_value['parent_hash']],
1139 extra_env=extra_env, input=commit_body)
1141 new_sha1 = result.output.strip()
1142 if new_sha1 == self.sha1:
1143 raise PatchException(
1145 'Internal error! Carbon copy of %s is the same as original!'
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.
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.
1163 A list of gerrit URLs found in the output
1166 ref_to_upload = self._GetCarbonCopy()
1168 ref_to_upload = self.sha1
1172 pack = '--receive-pack=git receive-pack '
1174 pack += ' '.join(['--reviewer=' + x for x in reviewers])
1176 pack += ' '.join(['--cc=' + x for x in cc])
1178 cmd += [push_url, '%s:%s' % (ref_to_upload, remote_ref)]
1180 cmd.append('--dry-run')
1182 lines = git.RunGit(self.project_url, cmd).error.splitlines()
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:
1190 for line in lines[num + 1:]:
1192 if len(line) != 2 or not line[1].startswith('http'):
1194 urls.append(line[-1])
1199 class UploadedLocalPatch(GitRepoPatch):
1200 """Represents an uploaded local patch passed in using --remote-patch."""
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.
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.
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
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))
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,)
1241 class GerritFetchOnlyPatch(GitRepoPatch):
1242 """Object that contains information to cherry-pick a Gerrit CL."""
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
1255 # Set owner, owner_email, and url for printing the CL link.
1256 self.owner_email = owner_email
1258 if self.owner_email:
1259 self.owner = self.owner_email.split('@', 1)[0]
1261 self.url = gob_util.GetChangePageUrl(
1262 constants.GERRIT_HOSTS[self.remote], int(self.gerrit_number))
1264 def _EnsureId(self, commit_message):
1265 """Ensure we have a usable Change-Id
1267 Validate what we received from gerrit against what the commit message
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
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
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))
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)
1298 class GerritPatch(GerritFetchOnlyPatch):
1299 """Object that represents a Gerrit CL."""
1301 def __init__(self, patch_dict, remote, url_prefix):
1302 """Construct a GerritPatch object from Gerrit query results.
1304 Gerrit query JSON fields are documented at:
1305 http://gerrit-documentation.googlecode.com/svn/Documentation/2.2.1/json.html
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
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'],
1325 current_patch_set.get('revision'),
1327 ParseGerritNumber(str(patch_dict['number'])),
1328 current_patch_set.get('number'),
1329 owner_email=patch_dict['owner']['email'])
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')
1345 def ConvertQueryResults(change, host):
1346 """Converts HTTP query results to the old SQL format.
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.
1353 http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/json.html
1356 # pylint: disable=C0301
1357 https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#json-entities
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??'),
1366 change_id = change['change_id'].split('~')[-1]
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'),
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'),
1380 current_revision = change.get('current_revision', '')
1381 current_revision_info = change.get('revisions', {}).get(current_revision)
1382 if current_revision_info:
1384 for label, label_data in change['labels'].iteritems():
1385 # Skip unknown labels.
1386 if label not in constants.GERRIT_ON_BORG_LABELS:
1388 for review_data in label_data.get('all', []):
1389 granted_on = review_data.get('date', change['created'])
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),
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),
1406 current_commit = current_revision_info.get('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]
1414 def __reduce__(self):
1415 """Used for pickling to re-create patch object."""
1416 return self.__class__, (self.patch_dict.copy(), self.remote,
1419 def GerritDependencies(self):
1420 """Returns the list of PatchQuery objects that this patch depends on."""
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)
1427 change_id = d.get('id')
1428 if change_id is not None:
1429 change_id = ParseChangeID(change_id, error_ok=False)
1431 sha1 = d.get('revision')
1432 if sha1 is not None:
1433 sha1 = ParseSHA1(sha1, error_ok=False)
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))
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))
1447 def IsAlreadyMerged(self):
1448 """Returns whether the patch has already been merged in Gerrit."""
1449 return self.status == 'MERGED'
1451 def HasApproval(self, field, value):
1452 """Return whether the current patchset has the specified approval.
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).
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))
1469 return value in type_approvals
1471 def GetLatestApproval(self, field):
1472 """Return most recent value of specific field on the current patchset.
1475 field: Which field to check ('VRIF', 'CRVW', ...).
1478 Most recent field value (as str) or '0' if no such field.
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'
1484 def PatchLink(self):
1485 """Return a CL link for this patch."""
1486 return 'CL:%s' % (self.gerrit_number_str,)
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,)
1498 def GeneratePatchesFromRepo(git_repo, project, tracking_branch, branch, remote,
1500 """Create a list of LocalPatch objects from a repo on disk.
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.
1511 result = git.RunGit(
1513 ['rev-list', '--reverse', '%s..%s' % (tracking_branch, branch)])
1515 sha1s = result.output.splitlines()
1518 cros_build_lib.Die('No changes found in %s:%s' % (project, branch))
1522 yield LocalPatch(os.path.join(git_repo, '.git'),
1523 project, branch, tracking_branch,
1527 def PrepareLocalPatches(manifest, patches):
1528 """Finish validation of parameters, and save patches to a temp folder.
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.
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))
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)
1554 def PrepareRemotePatches(patches):
1555 """Generate patch objects from list of --remote-patch parameters.
1558 patches: A list of --remote-patches strings that the user specified on
1559 the commandline. Patch strings are colon-delimited. Patches come
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
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
1573 for patch in patches:
1575 project, original_branch, ref, tracking_branch, tag = patch.split(':')
1576 except ValueError as e:
1578 "Unexpected tryjob format. You may be running an "
1579 "older version of chromite. Run 'repo sync "
1580 "chromiumos/chromite'. Error was %s" % e)
1582 if tag not in constants.PATCH_TAGS:
1583 raise ValueError('Bad remote patch format. Unknown tag %s' % tag)
1585 remote = constants.EXTERNAL_REMOTE
1586 if tag == constants.INTERNAL_PATCH_TAG:
1587 remote = constants.INTERNAL_REMOTE
1589 push_url = constants.GIT_REMOTES[remote]
1590 patch_info.append(UploadedLocalPatch(os.path.join(push_url, project),
1591 project, ref, tracking_branch,
1593 os.path.basename(ref), remote))