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