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