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