xref: /aosp_15_r20/external/autotest/utils/frozen_chromite/lib/git.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# -*- coding: utf-8 -*-
2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Common functions for interacting with git and repo."""
7
8from __future__ import print_function
9
10import collections
11import datetime
12import errno
13import fnmatch
14import hashlib
15import os
16import re
17import string
18import subprocess
19from xml import sax
20
21import six
22
23from autotest_lib.utils.frozen_chromite.lib import config_lib
24from autotest_lib.utils.frozen_chromite.lib import constants
25from autotest_lib.utils.frozen_chromite.lib import cros_build_lib
26from autotest_lib.utils.frozen_chromite.lib import cros_logging as logging
27from autotest_lib.utils.frozen_chromite.lib import osutils
28
29
30class GitException(Exception):
31  """An exception related to git."""
32
33
34# remote: git remote name (e.g., 'origin',
35#   'https://chromium.googlesource.com/chromiumos/chromite.git', etc.).
36# ref: git remote/local ref name (e.g., 'refs/heads/master').
37# project_name: git project name (e.g., 'chromiumos/chromite'.)
38_RemoteRef = collections.namedtuple(
39    '_RemoteRef', ('remote', 'ref', 'project_name'))
40
41
42class RemoteRef(_RemoteRef):
43  """Object representing a remote ref."""
44
45  def __new__(cls, remote, ref, project_name=None):
46    return super(RemoteRef, cls).__new__(cls, remote, ref, project_name)
47
48
49def FindRepoDir(path):
50  """Returns the nearest higher-level repo dir from the specified path.
51
52  Args:
53    path: The path to use. Defaults to cwd.
54  """
55  return osutils.FindInPathParents(
56      '.repo', path, test_func=os.path.isdir)
57
58
59def FindRepoCheckoutRoot(path):
60  """Get the root of your repo managed checkout."""
61  repo_dir = FindRepoDir(path)
62  if repo_dir:
63    return os.path.dirname(repo_dir)
64  else:
65    return None
66
67
68def IsSubmoduleCheckoutRoot(path, remote, url):
69  """Tests to see if a directory is the root of a git submodule checkout.
70
71  Args:
72    path: The directory to test.
73    remote: The remote to compare the |url| with.
74    url: The exact URL the |remote| needs to be pointed at.
75  """
76  if os.path.isdir(path):
77    remote_url = cros_build_lib.run(
78        ['git', '--git-dir', path, 'config', 'remote.%s.url' % remote],
79        stdout=True, debug_level=logging.DEBUG,
80        check=False, encoding='utf-8').output.strip()
81    if remote_url == url:
82      return True
83  return False
84
85
86def GetGitGitdir(pwd):
87  """Probes for a git gitdir directory rooted at a directory.
88
89  Args:
90    pwd: Directory to probe. If a checkout, should be the root.
91
92  Returns:
93    Path of the gitdir directory. None if the directory is not a git repo.
94  """
95  if os.path.isdir(os.path.join(pwd, '.git')):
96    return os.path.join(pwd, '.git')
97  # Is this directory a bare repo with no checkout?
98  if os.path.isdir(os.path.join(
99      pwd, 'objects')) and os.path.isdir(os.path.join(pwd, 'refs')):
100    return pwd
101  return None
102
103
104def IsGitRepositoryCorrupted(cwd):
105  """Verify that the specified git repository is not corrupted.
106
107  Args:
108    cwd: The git repository to verify.
109
110  Returns:
111    True if the repository is corrupted.
112  """
113  cmd = ['fsck', '--no-progress', '--no-dangling']
114  try:
115    GarbageCollection(cwd)
116    RunGit(cwd, cmd)
117    return False
118  except cros_build_lib.RunCommandError as ex:
119    logging.warning(str(ex))
120    return True
121
122
123_HEX_CHARS = frozenset(string.hexdigits)
124
125
126def IsSHA1(value, full=True):
127  """Returns True if the given value looks like a sha1.
128
129  If full is True, then it must be full length- 40 chars.  If False, >=6, and
130  <40.
131  """
132  if not all(x in _HEX_CHARS for x in value):
133    return False
134  l = len(value)
135  if full:
136    return l == 40
137  return l >= 6 and l <= 40
138
139
140def IsRefsTags(value):
141  """Return True if the given value looks like a tag.
142
143  Currently this is identified via refs/tags/ prefixing.
144  """
145  return value.startswith('refs/tags/')
146
147
148def GetGitRepoRevision(cwd, branch='HEAD', short=False):
149  """Find the revision of a branch.
150
151  Args:
152    cwd: The git repository to work with.
153    branch: Branch name. Defaults to current branch.
154    short: If set, output shorter unique SHA-1.
155
156  Returns:
157    Revision SHA-1.
158  """
159  cmd = ['rev-parse', branch]
160  if short:
161    cmd.insert(1, '--short')
162  return RunGit(cwd, cmd).output.strip()
163
164
165def IsReachable(cwd, to_ref, from_ref):
166  """Determine whether one commit ref is reachable from another.
167
168  Args:
169    cwd: The git repository to work with.
170    to_ref: The commit ref that may be reachable.
171    from_ref: The commit ref that |to_ref| may be reachable from.
172
173  Returns:
174    True if |to_ref| is reachable from |from_ref|.
175
176  Raises:
177    RunCommandError: if some error occurs, such as a commit ref not existing.
178  """
179  try:
180    RunGit(cwd, ['merge-base', '--is-ancestor', to_ref, from_ref])
181  except cros_build_lib.RunCommandError as e:
182    if e.result.returncode == 1:
183      return False
184    raise
185  return True
186
187
188def DoesCommitExistInRepo(cwd, commit):
189  """Determine whether a commit (SHA1 or ref) exists in a repo.
190
191  Args:
192    cwd: A directory within the project repo.
193    commit: The commit to look for. This can be a SHA1 or it can be a ref.
194
195  Returns:
196    True if the commit exists in the repo.
197  """
198  try:
199    RunGit(cwd, ['rev-list', '-n1', commit, '--'])
200  except cros_build_lib.RunCommandError as e:
201    if e.result.returncode == 128:
202      return False
203    raise
204  return True
205
206
207def GetCurrentBranch(cwd):
208  """Returns current branch of a repo, and None if repo is on detached HEAD."""
209  try:
210    ret = RunGit(cwd, ['symbolic-ref', '-q', 'HEAD'])
211    return StripRefsHeads(ret.output.strip(), False)
212  except cros_build_lib.RunCommandError as e:
213    if e.result.returncode != 1:
214      raise
215    return None
216
217
218def StripRefsHeads(ref, strict=True):
219  """Remove leading 'refs/heads/' from a ref name.
220
221  If strict is True, an Exception is thrown if the ref doesn't start with
222  refs/heads.  If strict is False, the original ref is returned.
223  """
224  if not ref.startswith('refs/heads/') and strict:
225    raise Exception('Ref name %s does not start with refs/heads/' % ref)
226
227  return ref.replace('refs/heads/', '')
228
229
230def StripRefs(ref):
231  """Remove leading 'refs/heads', 'refs/remotes/[^/]+/' from a ref name."""
232  ref = StripRefsHeads(ref, False)
233  if ref.startswith('refs/remotes/'):
234    return ref.split('/', 3)[-1]
235  return ref
236
237
238def NormalizeRef(ref):
239  """Convert git branch refs into fully qualified form."""
240  if ref and not ref.startswith('refs/'):
241    ref = 'refs/heads/%s' % ref
242  return ref
243
244
245def NormalizeRemoteRef(remote, ref):
246  """Convert git branch refs into fully qualified remote form."""
247  if ref:
248    # Support changing local ref to remote ref, or changing the remote
249    # for a remote ref.
250    ref = StripRefs(ref)
251
252    if not ref.startswith('refs/'):
253      ref = 'refs/remotes/%s/%s' % (remote, ref)
254
255  return ref
256
257
258class ProjectCheckout(dict):
259  """Attributes of a given project in the manifest checkout.
260
261  TODO(davidjames): Convert this into an ordinary object instead of a dict.
262  """
263
264  def __init__(self, attrs):
265    """Constructor.
266
267    Args:
268      attrs: The attributes associated with this checkout, as a dictionary.
269    """
270    dict.__init__(self, attrs)
271
272  def AssertPushable(self):
273    """Verify that it is safe to push changes to this repository."""
274    if not self['pushable']:
275      remote = self['remote']
276      raise AssertionError('Remote %s is not pushable.' % (remote,))
277
278  def IsBranchableProject(self):
279    """Return whether we can create a branch in the repo for this project."""
280    # Backwards compatibility is an issue here. Older manifests used a heuristic
281    # based on where the project is hosted. We must continue supporting it.
282    # (crbug.com/470690)
283    # Prefer explicit tagging.
284    if (self[constants.MANIFEST_ATTR_BRANCHING] ==
285        constants.MANIFEST_ATTR_BRANCHING_CREATE):
286      return True
287    if self[constants.MANIFEST_ATTR_BRANCHING] in (
288        constants.MANIFEST_ATTR_BRANCHING_PIN,
289        constants.MANIFEST_ATTR_BRANCHING_TOT):
290      return False
291
292    # Old heuristic.
293    site_params = config_lib.GetSiteParams()
294    if (self['remote'] not in site_params.CROS_REMOTES or
295        self['remote'] not in site_params.BRANCHABLE_PROJECTS):
296      return False
297    return re.match(site_params.BRANCHABLE_PROJECTS[self['remote']],
298                    self['name'])
299
300  def IsPinnableProject(self):
301    """Return whether we should pin to a revision on the CrOS branch."""
302    # Backwards compatibility is an issue here. Older manifests used a different
303    # tag to spcify pinning behaviour. Support both for now. (crbug.com/470690)
304    # Prefer explicit tagging.
305    if self[constants.MANIFEST_ATTR_BRANCHING] != '':
306      return (self[constants.MANIFEST_ATTR_BRANCHING] ==
307              constants.MANIFEST_ATTR_BRANCHING_PIN)
308
309    # Old heuristic.
310    return cros_build_lib.BooleanShellValue(self.get('pin'), True)
311
312  def GetPath(self, absolute=False):
313    """Get the path to the checkout.
314
315    Args:
316      absolute: If True, return an absolute path. If False,
317        return a path relative to the repo root.
318    """
319    return self['local_path'] if absolute else self['path']
320
321
322class Manifest(object):
323  """SAX handler that parses the manifest document.
324
325  Attributes:
326    checkouts_by_name: A dictionary mapping the names for <project> tags to a
327      list of ProjectCheckout objects.
328    checkouts_by_path: A dictionary mapping paths for <project> tags to a single
329      ProjectCheckout object.
330    default: The attributes of the <default> tag.
331    includes: A list of XML files that should be pulled in to the manifest.
332      These includes are represented as a list of (name, path) tuples.
333    manifest_include_dir: If given, this is where to start looking for
334      include targets.
335    projects: DEPRECATED. A dictionary mapping the names for <project> tags to
336      a single ProjectCheckout object. This is now deprecated, since each
337      project can map to multiple ProjectCheckout objects.
338    remotes: A dictionary mapping <remote> tags to the associated attributes.
339    revision: The revision of the manifest repository. If not specified, this
340      will be TOT.
341  """
342
343  _instance_cache = {}
344
345  def __init__(self, source, manifest_include_dir=None):
346    """Initialize this instance.
347
348    Args:
349      source: The path to the manifest to parse.  May be a file handle.
350      manifest_include_dir: If given, this is where to start looking for
351        include targets.
352    """
353    self.source = source
354    self.default = {}
355    self._current_project_path = None
356    self._current_project_name = None
357    self._annotations = {}
358    self.checkouts_by_path = {}
359    self.checkouts_by_name = {}
360    self.remotes = {}
361    self.includes = []
362    self.revision = None
363    self.manifest_include_dir = manifest_include_dir
364    self._RunParser(source)
365    self.includes = tuple(self.includes)
366
367  def _RequireAttr(self, attr, attrs):
368    name = attrs.get('name')
369    assert attr in attrs, ('%s is missing a "%s" attribute; attrs: %r' %
370                           (name, attr, attrs))
371
372  def _RunParser(self, source, finalize=True):
373    parser = sax.make_parser()
374    handler = sax.handler.ContentHandler()
375    handler.startElement = self._StartElement
376    handler.endElement = self._EndElement
377    parser.setContentHandler(handler)
378
379    # Python 2 seems to expect either a file name (as a string) or an
380    # opened file as the parameter to parser.parse, whereas Python 3
381    # seems to expect a URL (as a string) or opened file. Make it
382    # compatible with both by opening files first.
383    with cros_build_lib.Open(source) as f:
384      parser.parse(f)
385
386    if finalize:
387      self._FinalizeAllProjectData()
388
389  def _StartElement(self, name, attrs):
390    """Stores the default manifest properties and per-project overrides."""
391    attrs = dict(attrs.items())
392    if name == 'default':
393      self.default = attrs
394    elif name == 'remote':
395      self._RequireAttr('name', attrs)
396      attrs.setdefault('alias', attrs['name'])
397      self.remotes[attrs['name']] = attrs
398    elif name == 'project':
399      self._RequireAttr('name', attrs)
400      self._current_project_path = attrs.get('path', attrs['name'])
401      self._current_project_name = attrs['name']
402      self.checkouts_by_path[self._current_project_path] = attrs
403      checkout = self.checkouts_by_name.setdefault(self._current_project_name,
404                                                   [])
405      checkout.append(attrs)
406      self._annotations = {}
407    elif name == 'annotation':
408      self._RequireAttr('name', attrs)
409      self._RequireAttr('value', attrs)
410      self._annotations[attrs['name']] = attrs['value']
411    elif name == 'manifest':
412      self.revision = attrs.get('revision')
413    elif name == 'include':
414      if self.manifest_include_dir is None:
415        raise OSError(
416            errno.ENOENT, 'No manifest_include_dir given, but an include was '
417            'encountered; attrs=%r' % (attrs,))
418      # Include is calculated relative to the manifest that has the include;
419      # thus set the path temporarily to the dirname of the target.
420      original_include_dir = self.manifest_include_dir
421      include_path = os.path.realpath(
422          os.path.join(original_include_dir, attrs['name']))
423      self.includes.append((attrs['name'], include_path))
424      self._RunParser(include_path, finalize=False)
425
426  def _EndElement(self, name):
427    """Store any child element properties into the parent element."""
428    if name == 'project':
429      assert (self._current_project_name is not None and
430              self._current_project_path is not None), (
431                  'Malformed xml: Encountered unmatched </project>')
432      self.checkouts_by_path[self._current_project_path].update(
433          self._annotations)
434      for checkout in self.checkouts_by_name[self._current_project_name]:
435        checkout.update(self._annotations)
436      self._current_project_path = None
437      self._current_project_name = None
438
439  def _FinalizeAllProjectData(self):
440    """Rewrite projects mixing defaults in and adding our attributes."""
441    for path_data in self.checkouts_by_path.values():
442      self._FinalizeProjectData(path_data)
443
444  def _FinalizeProjectData(self, attrs):
445    """Sets up useful properties for a project.
446
447    Args:
448      attrs: The attribute dictionary of a <project> tag.
449    """
450    for key in ('remote', 'revision'):
451      attrs.setdefault(key, self.default.get(key))
452
453    remote = attrs['remote']
454    assert remote in self.remotes, ('%s: %s not in %s' %
455                                    (self.source, remote, self.remotes))
456    remote_name = attrs['remote_alias'] = self.remotes[remote]['alias']
457
458    # 'repo manifest -r' adds an 'upstream' attribute to the project tag for the
459    # manifests it generates.  We can use the attribute to get a valid branch
460    # instead of a sha1 for these types of manifests.
461    upstream = attrs.get('upstream', attrs['revision'])
462    if IsSHA1(upstream):
463      # The current version of repo we use has a bug: When you create a new
464      # repo checkout from a revlocked manifest, the 'upstream' attribute will
465      # just point at a SHA1. The default revision will still be correct,
466      # however. For now, return the default revision as our best guess as to
467      # what the upstream branch for this repository would be. This guess may
468      # sometimes be wrong, but it's correct for all of the repositories where
469      # we need to push changes (e.g., the overlays).
470      # TODO(davidjames): Either fix the repo bug, or update our logic here to
471      # check the manifest repository to find the right tracking branch.
472      upstream = self.default.get('revision', 'refs/heads/master')
473
474    attrs['tracking_branch'] = 'refs/remotes/%s/%s' % (
475        remote_name, StripRefs(upstream),
476    )
477
478    site_params = config_lib.GetSiteParams()
479    attrs['pushable'] = remote in site_params.GIT_REMOTES
480    if attrs['pushable']:
481      attrs['push_remote'] = remote
482      attrs['push_remote_url'] = site_params.GIT_REMOTES[remote]
483      attrs['push_url'] = '%s/%s' % (attrs['push_remote_url'], attrs['name'])
484    groups = set(attrs.get('groups', 'default').replace(',', ' ').split())
485    groups.add('default')
486    attrs['groups'] = frozenset(groups)
487
488    # Compute the local ref space.
489    # Sanitize a couple path fragments to simplify assumptions in this
490    # class, and in consuming code.
491    attrs.setdefault('path', attrs['name'])
492    for key in ('name', 'path'):
493      attrs[key] = os.path.normpath(attrs[key])
494
495    if constants.MANIFEST_ATTR_BRANCHING in attrs:
496      assert (attrs[constants.MANIFEST_ATTR_BRANCHING] in
497              constants.MANIFEST_ATTR_BRANCHING_ALL)
498    else:
499      attrs[constants.MANIFEST_ATTR_BRANCHING] = ''
500
501  @staticmethod
502  def _GetManifestHash(source, ignore_missing=False):
503    if isinstance(source, six.string_types):
504      try:
505        # TODO(build): convert this to osutils.ReadFile once these
506        # classes are moved out into their own module (if possible;
507        # may still be cyclic).
508        with open(source, 'rb') as f:
509          return hashlib.md5(f.read()).hexdigest()
510      except EnvironmentError as e:
511        if e.errno != errno.ENOENT or not ignore_missing:
512          raise
513    source.seek(0)
514    md5 = hashlib.md5(source.read()).hexdigest()
515    source.seek(0)
516    return md5
517
518  @classmethod
519  def Cached(cls, source, manifest_include_dir=None):
520    """Return an instance, reusing an existing one if possible.
521
522    May be a seekable filehandle, or a filepath.
523    See __init__ for an explanation of these arguments.
524    """
525
526    md5 = cls._GetManifestHash(source)
527    obj, sources = cls._instance_cache.get(md5, (None, ()))
528    if manifest_include_dir is None and sources:
529      # We're being invoked in a different way than the orignal
530      # caching; disregard the cached entry.
531      # Most likely, the instantiation will explode; let it fly.
532      obj, sources = None, ()
533    for include_target, target_md5 in sources:
534      if cls._GetManifestHash(include_target, True) != target_md5:
535        obj = None
536        break
537    if obj is None:
538      obj = cls(source, manifest_include_dir=manifest_include_dir)
539      sources = tuple((abspath, cls._GetManifestHash(abspath))
540                      for (target, abspath) in obj.includes)
541      cls._instance_cache[md5] = (obj, sources)
542
543    return obj
544
545
546class ManifestCheckout(Manifest):
547  """A Manifest Handler for a specific manifest checkout."""
548
549  _instance_cache = {}
550
551  def __init__(self, path, manifest_path=None, search=True):
552    """Initialize this instance.
553
554    Args:
555      path: Path into a manifest checkout (doesn't have to be the root).
556      manifest_path: If supplied, the manifest to use.  Else the manifest
557        in the root of the checkout is used.  May be a seekable file handle.
558      search: If True, the path can point into the repo, and the root will
559        be found automatically.  If False, the path *must* be the root, else
560        an OSError ENOENT will be thrown.
561
562    Raises:
563      OSError: if a failure occurs.
564    """
565    self.root, manifest_path = self._NormalizeArgs(
566        path, manifest_path, search=search)
567
568    self.manifest_path = os.path.realpath(manifest_path)
569    # The include dir is always the manifest repo, not where the manifest file
570    # happens to live.
571    manifest_include_dir = os.path.join(self.root, '.repo', 'manifests')
572    self.manifest_branch = self._GetManifestsBranch(self.root)
573    self._content_merging = {}
574    Manifest.__init__(self, self.manifest_path,
575                      manifest_include_dir=manifest_include_dir)
576
577  @staticmethod
578  def _NormalizeArgs(path, manifest_path=None, search=True):
579    root = FindRepoCheckoutRoot(path)
580    if root is None:
581      raise OSError(errno.ENOENT, "Couldn't find repo root: %s" % (path,))
582    root = os.path.normpath(os.path.realpath(root))
583    if not search:
584      if os.path.normpath(os.path.realpath(path)) != root:
585        raise OSError(errno.ENOENT, 'Path %s is not a repo root, and search '
586                      'is disabled.' % path)
587    if manifest_path is None:
588      manifest_path = os.path.join(root, '.repo', 'manifest.xml')
589    return root, manifest_path
590
591  @staticmethod
592  def IsFullManifest(checkout_root):
593    """Returns True iff the given checkout is using a full manifest.
594
595    This method should go away as part of the cleanup related to brbug.com/854.
596
597    Args:
598      checkout_root: path to the root of an SDK checkout.
599
600    Returns:
601      True iff the manifest selected for the given SDK is a full manifest.
602      In this context we'll accept any manifest for which there are no groups
603      defined.
604    """
605    manifests_git_repo = os.path.join(checkout_root, '.repo', 'manifests.git')
606    cmd = ['config', '--local', '--get', 'manifest.groups']
607    result = RunGit(manifests_git_repo, cmd, check=False)
608
609    if result.output.strip():
610      # Full layouts don't define groups.
611      return False
612
613    return True
614
615  def FindCheckouts(self, project, branch=None):
616    """Returns the list of checkouts for a given |project|/|branch|.
617
618    Args:
619      project: Project name to search for.
620      branch: Branch to use.
621
622    Returns:
623      A list of ProjectCheckout objects.
624    """
625    checkouts = []
626    for checkout in self.checkouts_by_name.get(project, []):
627      tracking_branch = checkout['tracking_branch']
628      if branch is None or StripRefs(branch) == StripRefs(tracking_branch):
629        checkouts.append(checkout)
630    return checkouts
631
632  def FindCheckout(self, project, branch=None, strict=True):
633    """Returns the checkout associated with a given project/branch.
634
635    Args:
636      project: The project to look for.
637      branch: The branch that the project is tracking.
638      strict: Raise AssertionError if a checkout cannot be found.
639
640    Returns:
641      A ProjectCheckout object.
642
643    Raises:
644      AssertionError if there is more than one checkout associated with the
645      given project/branch combination.
646    """
647    checkouts = self.FindCheckouts(project, branch)
648    if len(checkouts) < 1:
649      if strict:
650        raise AssertionError('Could not find checkout of %s' % (project,))
651      return None
652    elif len(checkouts) > 1:
653      raise AssertionError('Too many checkouts found for %s' % project)
654    return checkouts[0]
655
656  def ListCheckouts(self):
657    """List the checkouts in the manifest.
658
659    Returns:
660      A list of ProjectCheckout objects.
661    """
662    return list(self.checkouts_by_path.values())
663
664  def FindCheckoutFromPath(self, path, strict=True):
665    """Find the associated checkouts for a given |path|.
666
667    The |path| can either be to the root of a project, or within the
668    project itself (chromite.cbuildbot for example).  It may be relative
669    to the repo root, or an absolute path.  If |path| is not within a
670    checkout, return None.
671
672    Args:
673      path: Path to examine.
674      strict: If True, fail when no checkout is found.
675
676    Returns:
677      None if no checkout is found, else the checkout.
678    """
679    # Realpath everything sans the target to keep people happy about
680    # how symlinks are handled; exempt the final node since following
681    # through that is unlikely even remotely desired.
682    tmp = os.path.join(self.root, os.path.dirname(path))
683    path = os.path.join(os.path.realpath(tmp), os.path.basename(path))
684    path = os.path.normpath(path) + '/'
685    candidates = []
686    for checkout in self.ListCheckouts():
687      if path.startswith(checkout['local_path'] + '/'):
688        candidates.append((checkout['path'], checkout))
689
690    if not candidates:
691      if strict:
692        raise AssertionError('Could not find repo project at %s' % (path,))
693      return None
694
695    # The checkout with the greatest common path prefix is the owner of
696    # the given pathway. Return that.
697    return max(candidates)[1]
698
699  def _FinalizeAllProjectData(self):
700    """Rewrite projects mixing defaults in and adding our attributes."""
701    Manifest._FinalizeAllProjectData(self)
702    for key, value in self.checkouts_by_path.items():
703      self.checkouts_by_path[key] = ProjectCheckout(value)
704    for key, value in self.checkouts_by_name.items():
705      self.checkouts_by_name[key] = \
706          [ProjectCheckout(x) for x in value]
707
708  def _FinalizeProjectData(self, attrs):
709    Manifest._FinalizeProjectData(self, attrs)
710    attrs['local_path'] = os.path.join(self.root, attrs['path'])
711
712  @staticmethod
713  def _GetManifestsBranch(root):
714    """Get the tracking branch of the manifest repository.
715
716    Returns:
717      The branch name.
718    """
719    # Suppress the normal "if it ain't refs/heads, we don't want none o' that"
720    # check for the merge target; repo writes the ambigious form of the branch
721    # target for `repo init -u url -b some-branch` usages (aka, 'master'
722    # instead of 'refs/heads/master').
723    path = os.path.join(root, '.repo', 'manifests')
724    current_branch = GetCurrentBranch(path)
725    if current_branch != 'default':
726      raise OSError(errno.ENOENT,
727                    'Manifest repository at %s is checked out to %s.  '
728                    "It should be checked out to 'default'."
729                    % (root, 'detached HEAD' if current_branch is None
730                       else current_branch))
731
732    result = GetTrackingBranchViaGitConfig(
733        path, 'default', allow_broken_merge_settings=True, for_checkout=False)
734
735    if result is not None:
736      return StripRefsHeads(result.ref, False)
737
738    raise OSError(errno.ENOENT,
739                  "Manifest repository at %s is checked out to 'default', but "
740                  'the git tracking configuration for that branch is broken; '
741                  'failing due to that.' % (root,))
742
743  # pylint: disable=arguments-differ
744  @classmethod
745  def Cached(cls, path, manifest_path=None, search=True):
746    """Return an instance, reusing an existing one if possible.
747
748    Args:
749      path: The pathway into a checkout; the root will be found automatically.
750      manifest_path: if given, the manifest.xml to use instead of the
751        checkouts internal manifest.  Use with care.
752      search: If True, the path can point into the repo, and the root will
753        be found automatically.  If False, the path *must* be the root, else
754        an OSError ENOENT will be thrown.
755    """
756    root, manifest_path = cls._NormalizeArgs(path, manifest_path,
757                                             search=search)
758
759    md5 = cls._GetManifestHash(manifest_path)
760    obj, sources = cls._instance_cache.get((root, md5), (None, ()))
761    for include_target, target_md5 in sources:
762      if cls._GetManifestHash(include_target, True) != target_md5:
763        obj = None
764        break
765    if obj is None:
766      obj = cls(root, manifest_path=manifest_path)
767      sources = tuple((abspath, cls._GetManifestHash(abspath))
768                      for (target, abspath) in obj.includes)
769      cls._instance_cache[(root, md5)] = (obj, sources)
770    return obj
771
772
773def RunGit(git_repo, cmd, **kwargs):
774  """Wrapper for git commands.
775
776  This suppresses print_cmd, and suppresses output by default.  Git
777  functionality w/in this module should use this unless otherwise
778  warranted, to standardize git output (primarily, keeping it quiet
779  and being able to throw useful errors for it).
780
781  Args:
782    git_repo: Pathway to the git repo to operate on.
783    cmd: A sequence of the git subcommand to run.  The 'git' prefix is
784      added automatically.  If you wished to run 'git remote update',
785      this would be ['remote', 'update'] for example.
786    kwargs: Any run or GenericRetry options/overrides to use.
787
788  Returns:
789    A CommandResult object.
790  """
791  kwargs.setdefault('print_cmd', False)
792  kwargs.setdefault('cwd', git_repo)
793  kwargs.setdefault('capture_output', True)
794  kwargs.setdefault('encoding', 'utf-8')
795  return cros_build_lib.run(['git'] + cmd, **kwargs)
796
797
798def Init(git_repo):
799  """Create a new git repository, in the given location.
800
801  Args:
802    git_repo: Path for where to create a git repo. Directory will be created if
803              it doesnt exist.
804  """
805  osutils.SafeMakedirs(git_repo)
806  RunGit(git_repo, ['init'])
807
808
809def Clone(dest_path, git_url, reference=None, depth=None, branch=None,
810          single_branch=False):
811  """Clone a git repository, into the given directory.
812
813  Args:
814    dest_path: Path to clone into. Will be created if it doesn't exist.
815    git_url: Git URL to clone from.
816    reference: Path to a git repositry to reference in the clone. See
817      documentation for `git clone --reference`.
818    depth: Create a shallow clone with the given history depth. Cannot be used
819      with 'reference'.
820    branch: Branch to use for the initial HEAD. Defaults to the remote's HEAD.
821    single_branch: Clone only the requested branch.
822  """
823  if reference and depth:
824    raise ValueError('reference and depth are mutually exclusive')
825  osutils.SafeMakedirs(dest_path)
826  cmd = ['clone', git_url, dest_path]
827  if reference:
828    cmd += ['--reference', reference]
829  if depth:
830    cmd += ['--depth', str(int(depth))]
831  if branch:
832    cmd += ['--branch', branch]
833  if single_branch:
834    cmd += ['--single-branch']
835  RunGit(dest_path, cmd, print_cmd=True)
836
837
838def ShallowFetch(git_repo, git_url, sparse_checkout=None):
839  """Fetch a shallow git repository.
840
841  Args:
842    git_repo: Path of the git repo.
843    git_url: Url to fetch the git repository from.
844    sparse_checkout: List of file paths to fetch.
845  """
846  Init(git_repo)
847  RunGit(git_repo, ['remote', 'add', 'origin', git_url])
848  if sparse_checkout is not None:
849    assert isinstance(sparse_checkout, list)
850    RunGit(git_repo, ['config', 'core.sparsecheckout', 'true'])
851    osutils.WriteFile(os.path.join(git_repo, '.git/info/sparse-checkout'),
852                      '\n'.join(sparse_checkout))
853    logging.info('Sparse checkout: %s', sparse_checkout)
854
855  utcnow = datetime.datetime.utcnow
856  start = utcnow()
857  # Only fetch TOT git metadata without revision history.
858  RunGit(git_repo, ['fetch', '--depth=1'],
859         print_cmd=True, stderr=True, capture_output=False)
860  # Pull the files in sparse_checkout.
861  RunGit(git_repo, ['pull', 'origin', 'master'],
862         print_cmd=True, stderr=True, capture_output=False)
863  logging.info('ShallowFetch completed in %s.', utcnow() - start)
864
865
866def FindGitTopLevel(path):
867  """Returns the top-level directory of the given git working tree path."""
868  try:
869    ret = RunGit(path, ['rev-parse', '--show-toplevel'])
870    return ret.output.strip()
871  except cros_build_lib.RunCommandError:
872    return None
873
874
875def GetProjectUserEmail(git_repo):
876  """Get the email configured for the project."""
877  output = RunGit(git_repo, ['var', 'GIT_COMMITTER_IDENT']).output
878  m = re.search(r'<([^>]*)>', output.strip())
879  return m.group(1) if m else None
880
881
882def MatchBranchName(git_repo, pattern, namespace=''):
883  """Return branches who match the specified regular expression.
884
885  Args:
886    git_repo: The git repository to operate upon.
887    pattern: The regexp to search with.
888    namespace: The namespace to restrict search to (e.g. 'refs/heads/').
889
890  Returns:
891    List of matching branch names (with |namespace| trimmed).
892  """
893  output = RunGit(git_repo, ['ls-remote', git_repo, namespace + '*']).output
894  branches = [x.split()[1] for x in output.splitlines()]
895  branches = [x[len(namespace):] for x in branches if x.startswith(namespace)]
896
897  # Try exact match first.
898  match = re.compile(r'(^|/)%s$' % (pattern,), flags=re.I)
899  ret = [x for x in branches if match.search(x)]
900  if ret:
901    return ret
902
903  # Fall back to regex match if no exact match.
904  match = re.compile(pattern, flags=re.I)
905  return [x for x in branches if match.search(x)]
906
907
908class AmbiguousBranchName(Exception):
909  """Error if given branch name matches too many branches."""
910
911
912def MatchSingleBranchName(*args, **kwargs):
913  """Match exactly one branch name, else throw an exception.
914
915  Args:
916    See MatchBranchName for more details; all args are passed on.
917
918  Returns:
919    The branch name.
920
921  Raises:
922    raise AmbiguousBranchName if we did not match exactly one branch.
923  """
924  ret = MatchBranchName(*args, **kwargs)
925  if len(ret) != 1:
926    raise AmbiguousBranchName('Did not match exactly 1 branch: %r' % ret)
927  return ret[0]
928
929
930def GetTrackingBranchViaGitConfig(git_repo, branch, for_checkout=True,
931                                  allow_broken_merge_settings=False,
932                                  recurse=10):
933  """Pull the remote and upstream branch of a local branch
934
935  Args:
936    git_repo: The git repository to operate upon.
937    branch: The branch to inspect.
938    for_checkout: Whether to return localized refspecs, or the remote's
939      view of it.
940    allow_broken_merge_settings: Repo in a couple of spots writes invalid
941      branch.mybranch.merge settings; if these are encountered, they're
942      normally treated as an error and this function returns None.  If
943      this option is set to True, it suppresses this check.
944    recurse: If given and the target is local, then recurse through any
945      remote=. (aka locals).  This is enabled by default, and is what allows
946      developers to have multiple local branches of development dependent
947      on one another; disabling this makes that work flow impossible,
948      thus disable it only with good reason.  The value given controls how
949      deeply to recurse.  Defaults to tracing through 10 levels of local
950      remotes. Disabling it is a matter of passing 0.
951
952  Returns:
953    A RemoteRef, or None.  If for_checkout, then it returns the localized
954    version of it.
955  """
956  try:
957    cmd = ['config', '--get-regexp',
958           r'branch\.%s\.(remote|merge)' % re.escape(branch)]
959    data = RunGit(git_repo, cmd).output.splitlines()
960
961    prefix = 'branch.%s.' % (branch,)
962    data = [x.split() for x in data]
963    vals = dict((x[0][len(prefix):], x[1]) for x in data)
964    if len(vals) != 2:
965      if not allow_broken_merge_settings:
966        return None
967      elif 'merge' not in vals:
968        # There isn't anything we can do here.
969        return None
970      elif 'remote' not in vals:
971        # Repo v1.9.4 and up occasionally invalidly leave the remote out.
972        # Only occurs for the manifest repo fortunately.
973        vals['remote'] = 'origin'
974    remote, rev = vals['remote'], vals['merge']
975    # Suppress non branches; repo likes to write revisions and tags here,
976    # which is wrong (git hates it, nor will it honor it).
977    if rev.startswith('refs/remotes/'):
978      if for_checkout:
979        return RemoteRef(remote, rev)
980      # We can't backtrack from here, or at least don't want to.
981      # This is likely refs/remotes/m/ which repo writes when dealing
982      # with a revision locked manifest.
983      return None
984    if not rev.startswith('refs/heads/'):
985      # We explicitly don't allow pushing to tags, nor can one push
986      # to a sha1 remotely (makes no sense).
987      if not allow_broken_merge_settings:
988        return None
989    elif remote == '.':
990      if recurse == 0:
991        raise Exception(
992            'While tracing out tracking branches, we recursed too deeply: '
993            'bailing at %s' % branch)
994      return GetTrackingBranchViaGitConfig(
995          git_repo, StripRefsHeads(rev), for_checkout=for_checkout,
996          allow_broken_merge_settings=allow_broken_merge_settings,
997          recurse=recurse - 1)
998    elif for_checkout:
999      rev = 'refs/remotes/%s/%s' % (remote, StripRefsHeads(rev))
1000    return RemoteRef(remote, rev)
1001  except cros_build_lib.RunCommandError as e:
1002    # 1 is the retcode for no matches.
1003    if e.result.returncode != 1:
1004      raise
1005  return None
1006
1007
1008def GetTrackingBranchViaManifest(git_repo, for_checkout=True, for_push=False,
1009                                 manifest=None):
1010  """Gets the appropriate push branch via the manifest if possible.
1011
1012  Args:
1013    git_repo: The git repo to operate upon.
1014    for_checkout: Whether to return localized refspecs, or the remote's
1015      view of it.  Note that depending on the remote, the remote may differ
1016      if for_push is True or set to False.
1017    for_push: Controls whether the remote and refspec returned is explicitly
1018      for pushing.
1019    manifest: A Manifest instance if one is available, else a
1020      ManifestCheckout is created and used.
1021
1022  Returns:
1023    A RemoteRef, or None.  If for_checkout, then it returns the localized
1024    version of it.
1025  """
1026  try:
1027    if manifest is None:
1028      manifest = ManifestCheckout.Cached(git_repo)
1029
1030    checkout = manifest.FindCheckoutFromPath(git_repo, strict=False)
1031
1032    if checkout is None:
1033      return None
1034
1035    if for_push:
1036      checkout.AssertPushable()
1037
1038    if for_push:
1039      remote = checkout['push_remote']
1040    else:
1041      remote = checkout['remote']
1042
1043    if for_checkout:
1044      revision = checkout['tracking_branch']
1045    else:
1046      revision = checkout['revision']
1047      if not revision.startswith('refs/heads/'):
1048        return None
1049
1050    project_name = checkout.get('name', None)
1051
1052    return RemoteRef(remote, revision, project_name=project_name)
1053  except EnvironmentError as e:
1054    if e.errno != errno.ENOENT:
1055      raise
1056  return None
1057
1058
1059def GetTrackingBranch(git_repo, branch=None, for_checkout=True, fallback=True,
1060                      manifest=None, for_push=False):
1061  """Gets the appropriate push branch for the specified directory.
1062
1063  This function works on both repo projects and regular git checkouts.
1064
1065  Assumptions:
1066   1. We assume the manifest defined upstream is desirable.
1067   2. No manifest?  Assume tracking if configured is accurate.
1068   3. If none of the above apply, you get 'origin', 'master' or None,
1069      depending on fallback.
1070
1071  Args:
1072    git_repo: Git repository to operate upon.
1073    branch: Find the tracking branch for this branch.  Defaults to the
1074      current branch for |git_repo|.
1075    for_checkout: Whether to return localized refspecs, or the remotes
1076      view of it.
1077    fallback: If true and no remote/branch could be discerned, return
1078      'origin', 'master'.  If False, you get None.
1079      Note that depending on the remote, the remote may differ
1080      if for_push is True or set to False.
1081    for_push: Controls whether the remote and refspec returned is explicitly
1082      for pushing.
1083    manifest: A Manifest instance if one is available, else a
1084      ManifestCheckout is created and used.
1085
1086  Returns:
1087    A RemoteRef, or None.
1088  """
1089  result = GetTrackingBranchViaManifest(git_repo, for_checkout=for_checkout,
1090                                        manifest=manifest, for_push=for_push)
1091  if result is not None:
1092    return result
1093
1094  if branch is None:
1095    branch = GetCurrentBranch(git_repo)
1096  if branch:
1097    result = GetTrackingBranchViaGitConfig(git_repo, branch,
1098                                           for_checkout=for_checkout)
1099    if result is not None:
1100      if (result.ref.startswith('refs/heads/') or
1101          result.ref.startswith('refs/remotes/')):
1102        return result
1103
1104  if not fallback:
1105    return None
1106  if for_checkout:
1107    return RemoteRef('origin', 'refs/remotes/origin/master')
1108  return RemoteRef('origin', 'master')
1109
1110
1111def CreateBranch(git_repo, branch, branch_point='HEAD', track=False):
1112  """Create a branch.
1113
1114  Args:
1115    git_repo: Git repository to act on.
1116    branch: Name of the branch to create.
1117    branch_point: The ref to branch from.  Defaults to 'HEAD'.
1118    track: Whether to setup the branch to track its starting ref.
1119  """
1120  cmd = ['checkout', '-B', branch, branch_point]
1121  if track:
1122    cmd.append('--track')
1123  RunGit(git_repo, cmd)
1124
1125
1126def AddPath(path):
1127  """Use 'git add' on a path.
1128
1129  Args:
1130    path: Path to the git repository and the path to add.
1131  """
1132  dirname, filename = os.path.split(path)
1133  RunGit(dirname, ['add', '--', filename])
1134
1135
1136def RmPath(path):
1137  """Use 'git rm' on a file.
1138
1139  Args:
1140    path: Path to the git repository and the path to rm.
1141  """
1142  dirname, filename = os.path.split(path)
1143  RunGit(dirname, ['rm', '--', filename])
1144
1145
1146def GetObjectAtRev(git_repo, obj, rev, binary=False):
1147  """Return the contents of a git object at a particular revision.
1148
1149  This could be used to look at an old version of a file or directory, for
1150  instance, without modifying the working directory.
1151
1152  Args:
1153    git_repo: Path to a directory in the git repository to query.
1154    obj: The name of the object to read.
1155    rev: The revision to retrieve.
1156    binary: If true, return bytes instead of decoding as a UTF-8 string.
1157
1158  Returns:
1159    The content of the object.
1160  """
1161  rev_obj = '%s:%s' % (rev, obj)
1162  encoding = None if binary else 'utf-8'
1163  return RunGit(git_repo, ['show', rev_obj], encoding=encoding).output
1164
1165
1166def RevertPath(git_repo, filename, rev):
1167  """Revert a single file back to a particular revision and 'add' it with git.
1168
1169  Args:
1170    git_repo: Path to the directory holding the file.
1171    filename: Name of the file to revert.
1172    rev: Revision to revert the file to.
1173  """
1174  RunGit(git_repo, ['checkout', rev, '--', filename])
1175
1176
1177# In Log, we use "format" to refer to the --format flag to
1178# git. Disable the nags from pylint.
1179# pylint: disable=redefined-builtin
1180def Log(git_repo, format=None, after=None, until=None,
1181        reverse=False, date=None, max_count=None, grep=None,
1182        rev='HEAD', paths=None):
1183  """Return git log output for the given arguments.
1184
1185  For more detailed description of the parameters, run `git help log`.
1186
1187  Args:
1188    git_repo: Path to a directory in the git repository.
1189    format: Passed directly to the --format flag.
1190    after: Passed directly to --after flag.
1191    until: Passed directly to --until flag.
1192    reverse: If true, set --reverse flag.
1193    date: Passed directly to --date flag.
1194    max_count: Passed directly to --max-count flag.
1195    grep: Passed directly to --grep flag.
1196    rev: Commit (or revision range) to log.
1197    paths: List of paths to log commits for (enumerated after final -- ).
1198
1199  Returns:
1200    The raw log output as a string.
1201  """
1202  cmd = ['log']
1203  if format:
1204    cmd.append('--format=%s' % format)
1205  if after:
1206    cmd.append('--after=%s' % after)
1207  if until:
1208    cmd.append('--until=%s' % until)
1209  if reverse:
1210    cmd.append('--reverse')
1211  if date:
1212    cmd.append('--date=%s' % date)
1213  if max_count:
1214    cmd.append('--max-count=%s' % max_count)
1215  if grep:
1216    cmd.append('--grep=%s' % grep)
1217  cmd.append(rev)
1218  if paths:
1219    cmd.append('--')
1220    cmd.extend(paths)
1221  return RunGit(git_repo, cmd, errors='replace').stdout
1222# pylint: enable=redefined-builtin
1223
1224
1225def GetChangeId(git_repo, rev='HEAD'):
1226  """Retrieve the Change-Id from the commit message
1227
1228  Args:
1229    git_repo: Path to the git repository where the commit is
1230    rev: Commit to inspect, defaults to HEAD
1231
1232  Returns:
1233    The Gerrit Change-Id assigned to the commit if it exists.
1234  """
1235  log = Log(git_repo, max_count=1, format='format:%B', rev=rev)
1236  m = re.findall(r'^Change-Id: (I[a-fA-F0-9]{40})$', log, flags=re.M)
1237  if not m:
1238    return None
1239  elif len(m) > 1:
1240    raise ValueError('Too many Change-Ids found')
1241  else:
1242    return m[0]
1243
1244
1245def Commit(git_repo, message, amend=False, allow_empty=False,
1246           reset_author=False):
1247  """Commit with git.
1248
1249  Args:
1250    git_repo: Path to the git repository to commit in.
1251    message: Commit message to use.
1252    amend: Whether to 'amend' the CL, default False
1253    allow_empty: Whether to allow an empty commit. Default False.
1254    reset_author: Whether to reset author according to current config.
1255
1256  Returns:
1257    The Gerrit Change-ID assigned to the CL if it exists.
1258  """
1259  cmd = ['commit', '-m', message]
1260  if amend:
1261    cmd.append('--amend')
1262  if allow_empty:
1263    cmd.append('--allow-empty')
1264  if reset_author:
1265    cmd.append('--reset-author')
1266  RunGit(git_repo, cmd)
1267  return GetChangeId(git_repo)
1268
1269
1270_raw_diff_components = ('src_mode', 'dst_mode', 'src_sha', 'dst_sha',
1271                        'status', 'score', 'src_file', 'dst_file')
1272# RawDiffEntry represents a line of raw formatted git diff output.
1273RawDiffEntry = collections.namedtuple('RawDiffEntry', _raw_diff_components)
1274
1275
1276# This regular expression pulls apart a line of raw formatted git diff output.
1277DIFF_RE = re.compile(
1278    r':(?P<src_mode>[0-7]*) (?P<dst_mode>[0-7]*) '
1279    r'(?P<src_sha>[0-9a-f]*)(\.)* (?P<dst_sha>[0-9a-f]*)(\.)* '
1280    r'(?P<status>[ACDMRTUX])(?P<score>[0-9]+)?\t'
1281    r'(?P<src_file>[^\t]+)\t?(?P<dst_file>[^\t]+)?')
1282
1283
1284def RawDiff(path, target):
1285  """Return the parsed raw format diff of target
1286
1287  Args:
1288    path: Path to the git repository to diff in.
1289    target: The target to diff.
1290
1291  Returns:
1292    A list of RawDiffEntry's.
1293  """
1294  entries = []
1295
1296  cmd = ['diff', '-M', '--raw', target]
1297  diff = RunGit(path, cmd).output
1298  diff_lines = diff.strip().splitlines()
1299  for line in diff_lines:
1300    match = DIFF_RE.match(line)
1301    if not match:
1302      raise GitException('Failed to parse diff output: %s' % line)
1303    entries.append(RawDiffEntry(*match.group(*_raw_diff_components)))
1304
1305  return entries
1306
1307
1308def UploadCL(git_repo, remote, branch, local_branch='HEAD', draft=False,
1309             reviewers=None, **kwargs):
1310  """Upload a CL to gerrit. The CL should be checked out currently.
1311
1312  Args:
1313    git_repo: Path to the git repository with the CL to upload checked out.
1314    remote: The remote to upload the CL to.
1315    branch: Branch to upload to.
1316    local_branch: Branch to upload.
1317    draft: Whether to upload as a draft.
1318    reviewers: Add the reviewers to the CL.
1319    kwargs: Extra options for GitPush. capture_output defaults to False so
1320      that the URL for new or updated CLs is shown to the user.
1321  """
1322  ref = ('refs/drafts/%s' if draft else 'refs/for/%s') % branch
1323  if reviewers:
1324    reviewer_list = ['r=%s' % i for i in reviewers]
1325    ref = ref + '%'+ ','.join(reviewer_list)
1326  remote_ref = RemoteRef(remote, ref)
1327  kwargs.setdefault('capture_output', False)
1328  kwargs.setdefault('stderr', subprocess.STDOUT)
1329  return GitPush(git_repo, local_branch, remote_ref, **kwargs)
1330
1331
1332def GitPush(git_repo, refspec, push_to, force=False, dry_run=False,
1333            capture_output=True, skip=False, **kwargs):
1334  """Wrapper for pushing to a branch.
1335
1336  Args:
1337    git_repo: Git repository to act on.
1338    refspec: The local ref to push to the remote.
1339    push_to: A RemoteRef object representing the remote ref to push to.
1340    force: Whether to bypass non-fastforward checks.
1341    dry_run: If True, do everything except actually push the remote ref.
1342    capture_output: Whether to capture output for this command.
1343    skip: Log the git command that would have been run, but don't run it; this
1344      avoids e.g. remote access checks that still apply to |dry_run|.
1345  """
1346  cmd = ['push', push_to.remote, '%s:%s' % (refspec, push_to.ref)]
1347  if force:
1348    cmd.append('--force')
1349  if dry_run:
1350    cmd.append('--dry-run')
1351
1352  if skip:
1353    logging.info('Would have run "%s"', cmd)
1354    return
1355
1356  return RunGit(git_repo, cmd, capture_output=capture_output,
1357                **kwargs)
1358
1359
1360# TODO(build): Switch callers of this function to use CreateBranch instead.
1361def CreatePushBranch(branch, git_repo, sync=True, remote_push_branch=None):
1362  """Create a local branch for pushing changes inside a repo repository.
1363
1364  Args:
1365    branch: Local branch to create.
1366    git_repo: Git repository to create the branch in.
1367    sync: Update remote before creating push branch.
1368    remote_push_branch: A RemoteRef to push to. i.e.,
1369                        RemoteRef('cros', 'master').  By default it tries to
1370                        automatically determine which tracking branch to use
1371                        (see GetTrackingBranch()).
1372  """
1373  if not remote_push_branch:
1374    remote_push_branch = GetTrackingBranch(git_repo, for_push=True)
1375
1376  if sync:
1377    cmd = ['remote', 'update', remote_push_branch.remote]
1378    RunGit(git_repo, cmd)
1379
1380  RunGit(git_repo, ['checkout', '-B', branch, '-t', remote_push_branch.ref])
1381
1382
1383def SyncPushBranch(git_repo, remote, target, use_merge=False, **kwargs):
1384  """Sync and rebase or merge a local push branch to the latest remote version.
1385
1386  Args:
1387    git_repo: Git repository to rebase in.
1388    remote: The remote returned by GetTrackingBranch(for_push=True)
1389    target: The branch name returned by GetTrackingBranch().  Must
1390      start with refs/remotes/ (specifically must be a proper remote
1391      target rather than an ambiguous name).
1392    use_merge: Default: False. If True, use merge to bring local branch up to
1393      date with remote branch. Otherwise, use rebase.
1394    kwargs: Arguments passed through to RunGit.
1395  """
1396  subcommand = 'merge' if use_merge else 'rebase'
1397
1398  if not target.startswith('refs/remotes/'):
1399    raise Exception(
1400        'Was asked to %s to a non branch target w/in the push pathways.  '
1401        'This is highly indicative of an internal bug.  remote %s, %s %s'
1402        % (subcommand, remote, subcommand, target))
1403
1404  cmd = ['remote', 'update', remote]
1405  RunGit(git_repo, cmd, **kwargs)
1406
1407  try:
1408    RunGit(git_repo, [subcommand, target], **kwargs)
1409  except cros_build_lib.RunCommandError:
1410    # Looks like our change conflicts with upstream. Cleanup our failed
1411    # rebase.
1412    RunGit(git_repo, [subcommand, '--abort'], check=False, **kwargs)
1413    raise
1414
1415
1416def PushBranch(branch, git_repo, dryrun=False,
1417               staging_branch=None, auto_merge=False):
1418  """General method to push local git changes.
1419
1420  This method only works with branches created via the CreatePushBranch
1421  function.
1422
1423  Args:
1424    branch: Local branch to push.  Branch should have already been created
1425      with a local change committed ready to push to the remote branch.  Must
1426      also already be checked out to that branch.
1427    git_repo: Git repository to push from.
1428    dryrun: Git push --dry-run if set to True.
1429    staging_branch: Push change commits to the staging_branch if it's not None
1430    auto_merge: Enable Gerrit's auto-merge feature. See here for more info:
1431      https://gerrit-review.googlesource.com/Documentation/user-upload.html#auto_merge
1432      Note: The setting must be enabled in Gerrit UI for the specific repo.
1433
1434  Raises:
1435    GitPushFailed if push was unsuccessful after retries
1436  """
1437  remote_ref = GetTrackingBranch(git_repo, branch, for_checkout=False,
1438                                 for_push=True)
1439  # Don't like invoking this twice, but there is a bit of API
1440  # impedence here; cros_mark_as_stable
1441  local_ref = GetTrackingBranch(git_repo, branch, for_push=True)
1442
1443  if not remote_ref.ref.startswith('refs/heads/'):
1444    raise Exception('Was asked to push to a non branch namespace: %s' %
1445                    remote_ref.ref)
1446
1447  if auto_merge:
1448    remote_ref = RemoteRef(remote=remote_ref.remote,
1449                           ref=remote_ref.ref.replace(
1450                               'heads', 'for', 1) + '%notify=NONE,submit',
1451                           project_name=remote_ref.project_name)
1452  # reference = staging_branch if staging_branch is not None else remote_ref.ref
1453  if staging_branch is not None:
1454    remote_ref = remote_ref._replace(ref=staging_branch)
1455
1456  logging.debug('Trying to push %s to %s:%s',
1457                git_repo, branch, remote_ref.ref)
1458
1459  if dryrun:
1460    dryrun = True
1461
1462  SyncPushBranch(git_repo, remote_ref.remote, local_ref.ref)
1463
1464  try:
1465    GitPush(git_repo, branch, remote_ref, skip=dryrun, print_cmd=True,
1466            debug_level=logging.DEBUG)
1467  except cros_build_lib.RunCommandError:
1468    raise
1469
1470  logging.info('Successfully pushed %s to %s %s:%s',
1471               git_repo, remote_ref.remote, branch, remote_ref.ref)
1472
1473
1474def CleanAndDetachHead(git_repo):
1475  """Remove all local changes and checkout a detached head.
1476
1477  Args:
1478    git_repo: Directory of git repository.
1479  """
1480  RunGit(git_repo, ['am', '--abort'], check=False)
1481  RunGit(git_repo, ['rebase', '--abort'], check=False)
1482  RunGit(git_repo, ['clean', '-dfx'])
1483  RunGit(git_repo, ['checkout', '--detach', '-f', 'HEAD'])
1484
1485
1486def CleanAndCheckoutUpstream(git_repo, refresh_upstream=True):
1487  """Remove all local changes and checkout the latest origin.
1488
1489  All local changes in the supplied repo will be removed. The branch will
1490  also be switched to a detached head pointing at the latest origin.
1491
1492  Args:
1493    git_repo: Directory of git repository.
1494    refresh_upstream: If True, run a remote update prior to checking it out.
1495  """
1496  remote_ref = GetTrackingBranch(git_repo, for_push=refresh_upstream)
1497  CleanAndDetachHead(git_repo)
1498  if refresh_upstream:
1499    RunGit(git_repo, ['remote', 'update', remote_ref.remote])
1500  RunGit(git_repo, ['checkout', remote_ref.ref])
1501
1502
1503def GetChromiteTrackingBranch():
1504  """Returns the remote branch associated with chromite."""
1505  cwd = os.path.dirname(os.path.realpath(__file__))
1506  result_ref = GetTrackingBranch(cwd, for_checkout=False, fallback=False)
1507  if result_ref:
1508    branch = result_ref.ref
1509    if branch.startswith('refs/heads/'):
1510      # Normal scenario.
1511      return StripRefsHeads(branch)
1512    # Reaching here means it was refs/remotes/m/blah, or just plain invalid,
1513    # or that we're on a detached head in a repo not managed by chromite.
1514
1515  # Manually try the manifest next.
1516  try:
1517    manifest = ManifestCheckout.Cached(cwd)
1518    # Ensure the manifest knows of this checkout.
1519    if manifest.FindCheckoutFromPath(cwd, strict=False):
1520      return manifest.manifest_branch
1521  except EnvironmentError as e:
1522    if e.errno != errno.ENOENT:
1523      raise
1524
1525  # Not a manifest checkout.
1526  logging.notice(
1527      "Chromite checkout at %s isn't controlled by repo, nor is it on a "
1528      'branch (or if it is, the tracking configuration is missing or broken).  '
1529      'Falling back to assuming the chromite checkout is derived from '
1530      "'master'; this *may* result in breakage." % cwd)
1531  return 'master'
1532
1533
1534def GarbageCollection(git_repo, prune_all=False):
1535  """Cleanup unnecessary files and optimize the local repository.
1536
1537  Args:
1538    git_repo: Directory of git repository.
1539    prune_all: If True, prune all loose objects regardless of gc.pruneExpire.
1540  """
1541  # Use --auto so it only runs if housekeeping is necessary.
1542  cmd = ['gc', '--auto']
1543  if prune_all:
1544    cmd.append('--prune=all')
1545  RunGit(git_repo, cmd)
1546
1547
1548def DeleteStaleLocks(git_repo):
1549  """Clean up stale locks left behind in a git repo.
1550
1551  This might occur if an earlier git command was killed during an operation.
1552  Warning: This is dangerous because these locks are intended to prevent
1553  corruption. Only use this if you are sure that no other git process is
1554  accessing the repo (such as at the beginning of a fresh build).
1555
1556  Args"
1557    git_repo: Directory of git repository.
1558  """
1559  git_gitdir = GetGitGitdir(git_repo)
1560  if not git_gitdir:
1561    raise GitException('Not a valid git repo: %s' % git_repo)
1562
1563  for root, _, filenames in os.walk(git_gitdir):
1564    for filename in fnmatch.filter(filenames, '*.lock'):
1565      p = os.path.join(root, filename)
1566      logging.info('Found stale git lock, removing: %s', p)
1567      os.remove(p)
1568