gclient_scm.py 46.7 KB
Newer Older
1
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 3
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
4

5
"""Gclient-specific SCM-specific operations."""
6

7 8
from __future__ import print_function

9
import errno
10
import logging
11
import os
12
import posixpath
13
import re
14
import sys
15
import tempfile
16
import traceback
17
import urlparse
18

19
import download_from_google_storage
20
import gclient_utils
21
import git_cache
22
import scm
23
import shutil
24
import subprocess2
25 26


27 28
THIS_FILE_PATH = os.path.abspath(__file__)

29
GSUTIL_DEFAULT_PATH = os.path.join(
30
    os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
31

32

33 34 35 36
class NoUsableRevError(gclient_utils.Error):
  """Raised if requested revision isn't found in checkout."""


37 38
class DiffFiltererWrapper(object):
  """Simple base class which tracks which file is being diffed and
39
  replaces instances of its file name in the original and
40
  working copy lines of the git diff output."""
41
  index_string = None
42 43 44
  original_prefix = "--- "
  working_prefix = "+++ "

45
  def __init__(self, relpath, print_func):
46
    # Note that we always use '/' as the path separator to be
47
    # consistent with cygwin-style output on Windows
48
    self._relpath = relpath.replace("\\", "/")
49
    self._current_file = None
50
    self._print_func = print_func
51

52 53
  def SetCurrentFile(self, current_file):
    self._current_file = current_file
54

55 56
  @property
  def _replacement_file(self):
57
    return posixpath.join(self._relpath, self._current_file)
58

59 60
  def _Replace(self, line):
    return line.replace(self._current_file, self._replacement_file)
61 62 63 64

  def Filter(self, line):
    if (line.startswith(self.index_string)):
      self.SetCurrentFile(line[len(self.index_string):])
65
      line = self._Replace(line)
66 67 68
    else:
      if (line.startswith(self.original_prefix) or
          line.startswith(self.working_prefix)):
69
        line = self._Replace(line)
70
    self._print_func(line)
71 72


73 74 75 76 77 78 79 80 81 82 83
class GitDiffFilterer(DiffFiltererWrapper):
  index_string = "diff --git "

  def SetCurrentFile(self, current_file):
    # Get filename by parsing "a/<filename> b/<filename>"
    self._current_file = current_file[:(len(current_file)/2)][2:]

  def _Replace(self, line):
    return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)


84 85
### SCM abstraction layer

86 87
# Factory Method for SCM wrapper creation

88
def GetScmName(url):
89 90 91 92 93 94 95 96 97
  if not url:
    return None
  url, _ = gclient_utils.SplitUrlRevision(url)
  if url.endswith('.git'):
    return 'git'
  protocol = url.split('://')[0]
  if protocol in (
      'file', 'git', 'git+http', 'git+https', 'http', 'https', 'ssh', 'sso'):
    return 'git'
98 99 100
  return None


101
def CreateSCM(url, root_dir=None, relpath=None, out_fh=None, out_cb=None):
102
  SCM_MAP = {
103
    'git' : GitWrapper,
104
  }
105

106 107 108
  scm_name = GetScmName(url)
  if not scm_name in SCM_MAP:
    raise gclient_utils.Error('No SCM found for url %s' % url)
109 110 111
  scm_class = SCM_MAP[scm_name]
  if not scm_class.BinaryExists():
    raise gclient_utils.Error('%s command not found' % scm_name)
112
  return scm_class(url, root_dir, relpath, out_fh, out_cb)
113 114 115 116


# SCMWrapper base class

117 118 119
class SCMWrapper(object):
  """Add necessary glue between all the supported SCM.

120 121
  This is the abstraction layer to bind to different SCM.
  """
122 123 124

  def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
               out_cb=None):
125
    self.url = url
126 127 128 129 130 131
    self._root_dir = root_dir
    if self._root_dir:
      self._root_dir = self._root_dir.replace('/', os.sep)
    self.relpath = relpath
    if self.relpath:
      self.relpath = self.relpath.replace('/', os.sep)
132 133
    if self.relpath and self._root_dir:
      self.checkout_path = os.path.join(self._root_dir, self.relpath)
134 135 136 137 138 139 140 141 142 143
    if out_fh is None:
      out_fh = sys.stdout
    self.out_fh = out_fh
    self.out_cb = out_cb

  def Print(self, *args, **kwargs):
    kwargs.setdefault('file', self.out_fh)
    if kwargs.pop('timestamp', True):
      self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
    print(*args, **kwargs)
144 145

  def RunCommand(self, command, options, args, file_list=None):
146
    commands = ['update', 'updatesingle', 'revert',
147
                'revinfo', 'status', 'diff', 'pack', 'runhooks']
148 149 150 151

    if not command in commands:
      raise gclient_utils.Error('Unknown command %s' % command)

152
    if not command in dir(self):
153
      raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
154
          command, self.__class__.__name__))
155 156 157

    return getattr(self, command)(options, args, file_list)

158 159 160 161 162 163 164 165
  @staticmethod
  def _get_first_remote_url(checkout_path):
    log = scm.GIT.Capture(
        ['config', '--local', '--get-regexp', r'remote.*.url'],
        cwd=checkout_path)
    # Get the second token of the first line of the log.
    return log.splitlines()[0].split(' ', 1)[1]

166 167 168 169 170 171
  def GetCacheMirror(self):
    if (getattr(self, 'cache_dir', None)):
      url, _ = gclient_utils.SplitUrlRevision(self.url)
      return git_cache.Mirror(url)
    return None

172
  def GetActualRemoteURL(self, options):
173
    """Attempt to determine the remote URL for this SCMWrapper."""
174
    # Git
175
    if os.path.exists(os.path.join(self.checkout_path, '.git')):
176
      actual_remote_url = self._get_first_remote_url(self.checkout_path)
177

178 179 180 181 182 183
      mirror = self.GetCacheMirror()
      # If the cache is used, obtain the actual remote URL from there.
      if (mirror and mirror.exists() and
          mirror.mirror_path.replace('\\', '/') ==
          actual_remote_url.replace('\\', '/')):
        actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
184
      return actual_remote_url
185 186
    return None

187
  def DoesRemoteURLMatch(self, options):
188 189 190 191 192
    """Determine whether the remote URL of this checkout is the expected URL."""
    if not os.path.exists(self.checkout_path):
      # A checkout which doesn't exist can't be broken.
      return True

193
    actual_remote_url = self.GetActualRemoteURL(options)
194
    if actual_remote_url:
195 196
      return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
              == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
197 198
    else:
      # This may occur if the self.checkout_path exists but does not contain a
199
      # valid git checkout.
200 201
      return False

202 203 204 205 206 207
  def _DeleteOrMove(self, force):
    """Delete the checkout directory or move it out of the way.

    Args:
        force: bool; if True, delete the directory. Otherwise, just move it.
    """
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
    if force and os.environ.get('CHROME_HEADLESS') == '1':
      self.Print('_____ Conflicting directory found in %s. Removing.'
                 % self.checkout_path)
      gclient_utils.AddWarning('Conflicting directory %s deleted.'
                               % self.checkout_path)
      gclient_utils.rmtree(self.checkout_path)
    else:
      bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
                                 os.path.dirname(self.relpath))

      try:
        os.makedirs(bad_scm_dir)
      except OSError as e:
        if e.errno != errno.EEXIST:
          raise

      dest_path = tempfile.mkdtemp(
          prefix=os.path.basename(self.relpath),
          dir=bad_scm_dir)
      self.Print('_____ Conflicting directory found in %s. Moving to %s.'
                 % (self.checkout_path, dest_path))
      gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
                               % (self.checkout_path, dest_path))
      shutil.move(self.checkout_path, dest_path)
232

233

234
class GitWrapper(SCMWrapper):
235
  """Wrapper for Git"""
236
  name = 'git'
237
  remote = 'origin'
238

239 240
  cache_dir = None

241
  def __init__(self, url=None, *args):
242 243 244
    """Removes 'git+' fake prefix from git URL."""
    if url.startswith('git+http://') or url.startswith('git+https://'):
      url = url[4:]
245 246 247 248 249
    SCMWrapper.__init__(self, url, *args)
    filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
    if self.out_cb:
      filter_kwargs['predicate'] = self.out_cb
    self.filter = gclient_utils.GitFilter(**filter_kwargs)
250

251 252 253 254 255 256 257 258 259 260 261 262
  @staticmethod
  def BinaryExists():
    """Returns true if the command exists."""
    try:
      # We assume git is newer than 1.7.  See: crbug.com/114483
      result, version = scm.GIT.AssertVersion('1.7')
      if not result:
        raise gclient_utils.Error('Git version is older than 1.7: %s' % version)
      return result
    except OSError:
      return False

263 264 265
  def GetCheckoutRoot(self):
    return scm.GIT.GetCheckoutRoot(self.checkout_path)

266
  def GetRevisionDate(self, _revision):
267 268 269 270 271 272
    """Returns the given revision's date in ISO-8601 format (which contains the
    time zone)."""
    # TODO(floitsch): get the time-stamp of the given revision and not just the
    # time-stamp of the currently checked out revision.
    return self._Capture(['log', '-n', '1', '--format=%ai'])

273
  def diff(self, options, _args, _file_list):
274 275 276 277 278
    try:
      merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
    except subprocess2.CalledProcessError:
      merge_base = []
    self._Run(['diff'] + merge_base, options)
279

280
  def pack(self, _options, _args, _file_list):
281
    """Generates a patch file which can be applied to the root of the
282 283 284 285 286
    repository.

    The patch file is generated from a diff of the merge base of HEAD and
    its upstream branch.
    """
287 288 289 290
    try:
      merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
    except subprocess2.CalledProcessError:
      merge_base = []
291
    gclient_utils.CheckCallAndFilter(
292
        ['git', 'diff'] + merge_base,
293
        cwd=self.checkout_path,
294
        filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
295

296 297
  def _Scrub(self, target, options):
    """Scrubs out all changes in the local repo, back to the state of target."""
298 299 300
    quiet = []
    if not options.verbose:
      quiet = ['--quiet']
301 302 303 304 305 306 307
    self._Run(['reset', '--hard', target] + quiet, options)
    if options.force and options.delete_unversioned_trees:
      # where `target` is a commit that contains both upper and lower case
      # versions of the same file on a case insensitive filesystem, we are
      # actually in a broken state here. The index will have both 'a' and 'A',
      # but only one of them will exist on the disk. To progress, we delete
      # everything that status thinks is modified.
308 309
      output = self._Capture(['status', '--porcelain'], strip=False)
      for line in output.splitlines():
310 311 312 313 314 315
        # --porcelain (v1) looks like:
        # XY filename
        try:
          filename = line[3:]
          self.Print('_____ Deleting residual after reset: %r.' % filename)
          gclient_utils.rm_file_or_tree(
316
            os.path.join(self.checkout_path, filename))
317 318 319 320 321
        except OSError:
          pass

  def _FetchAndReset(self, revision, file_list, options):
    """Equivalent to git fetch; git reset."""
322 323
    self._UpdateBranchHeads(options, fetch=False)

324
    self._Fetch(options, prune=True, quiet=options.verbose)
325
    self._Scrub(revision, options)
326 327 328 329
    if file_list is not None:
      files = self._Capture(['ls-files']).splitlines()
      file_list.extend([os.path.join(self.checkout_path, f) for f in files])

330 331 332 333 334 335
  def _DisableHooks(self):
    hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
    if not os.path.isdir(hook_dir):
      return
    for f in os.listdir(hook_dir):
      if not f.endswith('.sample') and not f.endswith('.disabled'):
336 337 338 339
        disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
        if os.path.exists(disabled_hook_path):
          os.remove(disabled_hook_path)
        os.rename(os.path.join(hook_dir, f), disabled_hook_path)
340

341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
  def _maybe_break_locks(self, options):
    """This removes all .lock files from this repo's .git directory, if the
    user passed the --break_repo_locks command line flag.

    In particular, this will cleanup index.lock files, as well as ref lock
    files.
    """
    if options.break_repo_locks:
      git_dir = os.path.join(self.checkout_path, '.git')
      for path, _, filenames in os.walk(git_dir):
        for filename in filenames:
          if filename.endswith('.lock'):
            to_break = os.path.join(path, filename)
            self.Print('breaking lock: %s' % (to_break,))
            try:
              os.remove(to_break)
            except OSError as ex:
              self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
              raise


362 363 364 365 366 367 368 369 370 371 372
  def update(self, options, args, file_list):
    """Runs git to update or transparently checkout the working copy.

    All updated files will be appended to file_list.

    Raises:
      Error: if can't get URL for relative path.
    """
    if args:
      raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))

373
    self._CheckMinVersion("1.6.6")
374

375
    # If a dependency is not pinned, track the default remote branch.
376
    default_rev = 'refs/remotes/%s/master' % self.remote
377 378
    url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
    revision = deps_revision
379
    managed = True
380
    if options.revision:
381 382
      # Override the revision number.
      revision = str(options.revision)
383
    if revision == 'unmanaged':
384 385 386
      # Check again for a revision in case an initial ref was specified
      # in the url, for example bla.git@refs/heads/custombranch
      revision = deps_revision
387
      managed = False
388 389
    if not revision:
      revision = default_rev
390

391 392 393
    if managed:
      self._DisableHooks()

394 395
    printed_path = False
    verbose = []
396
    if options.verbose:
397
      self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
398 399 400
      verbose = ['--verbose']
      printed_path = True

401 402
    remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
    if remote_ref:
403
      # Rewrite remote refs to their local equivalents.
404 405 406 407 408
      revision = ''.join(remote_ref)
      rev_type = "branch"
    elif revision.startswith('refs/'):
      # Local branch? We probably don't want to support, since DEPS should
      # always specify branches as they are in the upstream repo.
409 410 411 412 413
      rev_type = "branch"
    else:
      # hash is also a tag, only make a distinction at checkout
      rev_type = "hash"

414 415 416 417
    mirror = self._GetMirror(url, options)
    if mirror:
      url = mirror.mirror_path

418 419 420 421 422 423 424 425 426 427 428 429
    # If we are going to introduce a new project, there is a possibility that
    # we are syncing back to a state where the project was originally a
    # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
    # syncing backwards, when Blink was a DEPS entry and not part of src.git).
    # In such case, we might have a backup of the former .git folder, which can
    # be used to avoid re-fetching the entire repo again (useful for bisects).
    backup_dir = self.GetGitBackupDirPath()
    target_dir = os.path.join(self.checkout_path, '.git')
    if os.path.exists(backup_dir) and not os.path.exists(target_dir):
      gclient_utils.safe_makedirs(self.checkout_path)
      os.rename(backup_dir, target_dir)
      # Reset to a clean state
430
      self._Scrub('HEAD', options)
431

432 433 434
    if (not os.path.exists(self.checkout_path) or
        (os.path.isdir(self.checkout_path) and
         not os.path.exists(os.path.join(self.checkout_path, '.git')))):
435 436
      if mirror:
        self._UpdateMirror(mirror, options)
437
      try:
438
        self._Clone(revision, url, options)
439 440
      except subprocess2.CalledProcessError:
        self._DeleteOrMove(options.force)
441
        self._Clone(revision, url, options)
442 443 444 445 446 447
      if file_list is not None:
        files = self._Capture(['ls-files']).splitlines()
        file_list.extend([os.path.join(self.checkout_path, f) for f in files])
      if not verbose:
        # Make the output a little prettier. It's nice to have some whitespace
        # between projects when cloning.
448
        self.Print('')
449
      return self._Capture(['rev-parse', '--verify', 'HEAD'])
450

451 452 453 454 455
    if not managed:
      self._UpdateBranchHeads(options, fetch=False)
      self.Print('________ unmanaged solution; skipping %s' % self.relpath)
      return self._Capture(['rev-parse', '--verify', 'HEAD'])

456 457
    self._maybe_break_locks(options)

458 459 460
    if mirror:
      self._UpdateMirror(mirror, options)

461 462 463 464 465 466 467 468 469
    # See if the url has changed (the unittests use git://foo for the url, let
    # that through).
    current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
    return_early = False
    # TODO(maruel): Delete url != 'git://foo' since it's just to make the
    # unit test pass. (and update the comment above)
    # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
    # This allows devs to use experimental repos which have a different url
    # but whose branch(s) are the same as official repos.
470
    if (current_url.rstrip('/') != url.rstrip('/') and
471 472 473 474
        url != 'git://foo' and
        subprocess2.capture(
            ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
            cwd=self.checkout_path).strip() != 'False'):
475
      self.Print('_____ switching %s to a new upstream' % self.relpath)
476 477
      if not (options.force or options.reset):
        # Make sure it's clean
478
        self._CheckClean(revision)
479 480
      # Switch over to the new upstream
      self._Run(['remote', 'set-url', self.remote, url], options)
481 482 483 484 485
      if mirror:
        with open(os.path.join(
            self.checkout_path, '.git', 'objects', 'info', 'alternates'),
            'w') as fh:
          fh.write(os.path.join(url, 'objects'))
486
      self._EnsureValidHeadObjectOrCheckout(revision, options, url)
487
      self._FetchAndReset(revision, file_list, options)
488

489
      return_early = True
490 491
    else:
      self._EnsureValidHeadObjectOrCheckout(revision, options, url)
492

493 494 495
    if return_early:
      return self._Capture(['rev-parse', '--verify', 'HEAD'])

496 497
    cur_branch = self._GetCurrentBranch()

498
    # Cases:
499 500 501
    # 0) HEAD is detached. Probably from our initial clone.
    #   - make sure HEAD is contained by a named ref, then update.
    # Cases 1-4. HEAD is a branch.
502
    # 1) current branch is not tracking a remote branch
503 504 505
    #   - try to rebase onto the new hash or branch
    # 2) current branch is tracking a remote branch with local committed
    #    changes, but the DEPS file switched to point to a hash
506
    #   - rebase those changes on top of the hash
507 508
    # 3) current branch is tracking a remote branch w/or w/out changes, and
    #    no DEPS switch
509
    #   - see if we can FF, if not, prompt the user for rebase, merge, or stop
510 511 512 513 514 515 516
    # 4) current branch is tracking a remote branch, but DEPS switches to a
    #    different remote branch, and
    #   a) current branch has no local changes, and --force:
    #      - checkout new branch
    #   b) current branch has local changes, and --force and --reset:
    #      - checkout new branch
    #   c) otherwise exit
517

518 519
    # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
    # a tracking branch
520 521
    # or 'master' if not a tracking branch (it's based on a specific rev/hash)
    # or it returns None if it couldn't find an upstream
522 523 524 525
    if cur_branch is None:
      upstream_branch = None
      current_type = "detached"
      logging.debug("Detached HEAD")
526
    else:
527 528 529 530 531 532 533 534 535
      upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
      if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
        current_type = "hash"
        logging.debug("Current branch is not tracking an upstream (remote)"
                      " branch.")
      elif upstream_branch.startswith('refs/remotes'):
        current_type = "branch"
      else:
        raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
536

537
    if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
538
      # Update the remotes first so we have all the refs.
539
      remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
540 541
              cwd=self.checkout_path)
      if verbose:
542
        self.Print(remote_output)
543

544
    self._UpdateBranchHeads(options, fetch=True)
545

546
    # This is a big hammer, debatable if it should even be here...
547
    if options.force or options.reset:
548 549 550
      target = 'HEAD'
      if options.upstream and upstream_branch:
        target = upstream_branch
551
      self._Scrub(target, options)
552

553 554
    if current_type == 'detached':
      # case 0
555 556 557 558 559 560 561
      # We just did a Scrub, this is as clean as it's going to get. In
      # particular if HEAD is a commit that contains two versions of the same
      # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
      # to actually "Clean" the checkout; that commit is uncheckoutable on this
      # system. The best we can do is carry forward to the checkout step.
      if not (options.force or options.reset):
        self._CheckClean(revision)
562
      self._CheckDetachedHead(revision, options)
563
      if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
564 565
        self.Print('Up-to-date; skipping checkout.')
      else:
566 567
        # 'git checkout' may need to overwrite existing untracked files. Allow
        # it only when nuclear options are enabled.
568 569 570
        self._Checkout(
            options,
            revision,
571
            force=(options.force and options.delete_unversioned_trees),
572 573
            quiet=True,
        )
574
      if not printed_path:
575
        self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
576
    elif current_type == 'hash':
577
      # case 1
578 579 580 581 582 583
      # Can't find a merge-base since we don't know our upstream. That makes
      # this command VERY likely to produce a rebase failure. For now we
      # assume origin is our upstream since that's what the old behavior was.
      upstream_branch = self.remote
      if options.revision or deps_revision:
        upstream_branch = revision
584
      self._AttemptRebase(upstream_branch, file_list, options,
585 586
                          printed_path=printed_path, merge=options.merge)
      printed_path = True
587
    elif rev_type == 'hash':
588
      # case 2
589
      self._AttemptRebase(upstream_branch, file_list, options,
590
                          newbase=revision, printed_path=printed_path,
591
                          merge=options.merge)
592
      printed_path = True
593
    elif remote_ref and ''.join(remote_ref) != upstream_branch:
594
      # case 4
595
      new_base = ''.join(remote_ref)
596
      if not printed_path:
597
        self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
598
      switch_error = ("Could not switch upstream branch from %s to %s\n"
599
                     % (upstream_branch, new_base) +
600
                     "Please use --force or merge or rebase manually:\n" +
601 602
                     "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
                     "OR git checkout -b <some new branch> %s" % new_base)
603 604 605
      force_switch = False
      if options.force:
        try:
606
          self._CheckClean(revision)
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623
          # case 4a
          force_switch = True
        except gclient_utils.Error as e:
          if options.reset:
            # case 4b
            force_switch = True
          else:
            switch_error = '%s\n%s' % (e.message, switch_error)
      if force_switch:
        self.Print("Switching upstream branch from %s to %s" %
                   (upstream_branch, new_base))
        switch_branch = 'gclient_' + remote_ref[1]
        self._Capture(['branch', '-f', switch_branch, new_base])
        self._Checkout(options, switch_branch, force=True, quiet=True)
      else:
        # case 4c
        raise gclient_utils.Error(switch_error)
624 625
    else:
      # case 3 - the default case
626 627
      rebase_files = self._Capture(
          ['diff', upstream_branch, '--name-only']).split()
628
      if verbose:
629
        self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
630
      try:
631
        merge_args = ['merge']
632 633 634
        if options.merge:
          merge_args.append('--ff')
        else:
635 636
          merge_args.append('--ff-only')
        merge_args.append(upstream_branch)
637
        merge_output = self._Capture(merge_args)
638
      except subprocess2.CalledProcessError as e:
639
        rebase_files = []
640 641
        if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
          if not printed_path:
642 643
            self.Print('_____ %s at %s' % (self.relpath, revision),
                       timestamp=False)
644 645
            printed_path = True
          while True:
646 647 648 649 650 651 652 653 654 655
            if not options.auto_rebase:
              try:
                action = self._AskForData(
                    'Cannot %s, attempt to rebase? '
                    '(y)es / (q)uit / (s)kip : ' %
                        ('merge' if options.merge else 'fast-forward merge'),
                    options)
              except ValueError:
                raise gclient_utils.Error('Invalid Character')
            if options.auto_rebase or re.match(r'yes|y', action, re.I):
656
              self._AttemptRebase(upstream_branch, rebase_files, options,
657
                                  printed_path=printed_path, merge=False)
658 659 660 661 662 663 664 665
              printed_path = True
              break
            elif re.match(r'quit|q', action, re.I):
              raise gclient_utils.Error("Can't fast-forward, please merge or "
                                        "rebase manually.\n"
                                        "cd %s && git " % self.checkout_path
                                        + "rebase %s" % upstream_branch)
            elif re.match(r'skip|s', action, re.I):
666
              self.Print('Skipping %s' % self.relpath)
667 668
              return
            else:
669
              self.Print('Input not recognized')
670 671 672
        elif re.match("error: Your local changes to '.*' would be "
                      "overwritten by merge.  Aborting.\nPlease, commit your "
                      "changes or stash them before you can merge.\n",
673 674
                      e.stderr):
          if not printed_path:
675 676
            self.Print('_____ %s at %s' % (self.relpath, revision),
                       timestamp=False)
677 678 679 680 681
            printed_path = True
          raise gclient_utils.Error(e.stderr)
        else:
          # Some other problem happened with the merge
          logging.error("Error during fast-forward merge in %s!" % self.relpath)
682
          self.Print(e.stderr)
683 684 685 686 687
          raise
      else:
        # Fast-forward merge was successful
        if not re.match('Already up-to-date.', merge_output) or verbose:
          if not printed_path:
688 689
            self.Print('_____ %s at %s' % (self.relpath, revision),
                       timestamp=False)
690
            printed_path = True
691
          self.Print(merge_output.strip())
692 693 694
          if not verbose:
            # Make the output a little prettier. It's nice to have some
            # whitespace between projects when syncing.
695
            self.Print('')
696

697 698 699
      if file_list is not None:
        file_list.extend(
            [os.path.join(self.checkout_path, f) for f in rebase_files])
700 701

    # If the rebase generated a conflict, abort and ask user to fix
702
    if self._IsRebasing():
703
      raise gclient_utils.Error('\n____ %s at %s\n'
704 705 706
                                '\nConflict while rebasing this branch.\n'
                                'Fix the conflict and run gclient again.\n'
                                'See man git-rebase for details.\n'
707
                                % (self.relpath, revision))
708

709
    if verbose:
710 711
      self.Print('Checked out revision %s' % self.revinfo(options, (), None),
                 timestamp=False)
712

713 714 715 716 717 718 719 720 721 722 723 724
    # If --reset and --delete_unversioned_trees are specified, remove any
    # untracked directories.
    if options.reset and options.delete_unversioned_trees:
      # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
      # merge-base by default), so doesn't include untracked files. So we use
      # 'git ls-files --directory --others --exclude-standard' here directly.
      paths = scm.GIT.Capture(
          ['ls-files', '--directory', '--others', '--exclude-standard'],
          self.checkout_path)
      for path in (p for p in paths.splitlines() if p.endswith('/')):
        full_path = os.path.join(self.checkout_path, path)
        if not os.path.islink(full_path):
725
          self.Print('_____ removing unversioned directory %s' % path)
726
          gclient_utils.rmtree(full_path)
727

728 729
    return self._Capture(['rev-parse', '--verify', 'HEAD'])

730

731
  def revert(self, options, _args, file_list):
732 733 734 735
    """Reverts local modifications.

    All reverted files will be appended to file_list.
    """
736
    if not os.path.isdir(self.checkout_path):
737 738
      # revert won't work if the directory doesn't exist. It needs to
      # checkout instead.
739
      self.Print('_____ %s is missing, synching instead' % self.relpath)
740 741
      # Don't reuse the args.
      return self.update(options, [], file_list)
742 743

    default_rev = "refs/heads/master"
744 745 746 747
    if options.upstream:
      if self._GetCurrentBranch():
        upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
        default_rev = upstream_branch or default_rev
748
    _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
749 750
    if not deps_revision:
      deps_revision = default_rev
751 752
    if deps_revision.startswith('refs/heads/'):
      deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
753 754 755 756 757 758 759 760 761
    try:
      deps_revision = self.GetUsableRev(deps_revision, options)
    except NoUsableRevError as e:
      # If the DEPS entry's url and hash changed, try to update the origin.
      # See also http://crbug.com/520067.
      logging.warn(
          'Couldn\'t find usable revision, will retrying to update instead: %s',
          e.message)
      return self.update(options, [], file_list)
762

763 764 765
    if file_list is not None:
      files = self._Capture(['diff', deps_revision, '--name-only']).split()

766
    self._Scrub(deps_revision, options)
767
    self._Run(['clean', '-f', '-d'], options)
768

769 770 771 772
    if file_list is not None:
      file_list.extend([os.path.join(self.checkout_path, f) for f in files])

  def revinfo(self, _options, _args, _file_list):
773 774
    """Returns revision"""
    return self._Capture(['rev-parse', 'HEAD'])
775

776 777 778
  def runhooks(self, options, args, file_list):
    self.status(options, args, file_list)

779
  def status(self, options, _args, file_list):
780 781
    """Display status information."""
    if not os.path.isdir(self.checkout_path):
782 783
      self.Print('________ couldn\'t run status in %s:\n'
                 'The directory does not exist.' % self.checkout_path)
784
    else:
785 786 787 788 789
      try:
        merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
      except subprocess2.CalledProcessError:
        merge_base = []
      self._Run(['diff', '--name-status'] + merge_base, options,
790
                stdout=self.out_fh, always=options.verbose)
791
      if file_list is not None:
792
        files = self._Capture(['diff', '--name-only'] + merge_base).split()
793
        file_list.extend([os.path.join(self.checkout_path, f) for f in files])
794

795
  def GetUsableRev(self, rev, options):
796
    """Finds a useful revision for this repository."""
797 798 799
    sha1 = None
    if not os.path.isdir(self.checkout_path):
      raise NoUsableRevError(
800
          'This is not a git repo, so we cannot get a usable rev.')
801 802 803

    if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
      sha1 = rev
804
    else:
805 806 807
      # May exist in origin, but we don't have it yet, so fetch and look
      # again.
      self._Fetch(options)
808 809 810 811 812
      if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
        sha1 = rev

    if not sha1:
      raise NoUsableRevError(
813
          'Hash %s does not appear to be a valid hash in this repo.' % rev)
814 815 816

    return sha1

817 818 819 820 821 822
  def FullUrlForRelativeUrl(self, url):
    # Strip from last '/'
    # Equivalent to unix basename
    base_url = self.url
    return base_url[:base_url.rfind('/')] + url

823 824 825 826 827 828
  def GetGitBackupDirPath(self):
    """Returns the path where the .git folder for the current project can be
    staged/restored. Use case: subproject moved from DEPS <-> outer project."""
    return os.path.join(self._root_dir,
                        'old_' + self.relpath.replace(os.sep, '_')) + '.git'

829 830 831 832
  def _GetMirror(self, url, options):
    """Get a git_cache.Mirror object for the argument url."""
    if not git_cache.Mirror.GetCachePath():
      return None
hinoka@google.com's avatar
hinoka@google.com committed
833 834 835 836 837 838
    mirror_kwargs = {
        'print_func': self.filter,
        'refs': []
    }
    if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
      mirror_kwargs['refs'].append('refs/branch-heads/*')
839 840
    if hasattr(options, 'with_tags') and options.with_tags:
      mirror_kwargs['refs'].append('refs/tags/*')
841 842 843 844 845
    return git_cache.Mirror(url, **mirror_kwargs)

  @staticmethod
  def _UpdateMirror(mirror, options):
    """Update a git mirror by fetching the latest commits from the remote."""
846
    if getattr(options, 'shallow', False):
847
      # HACK(hinoka): These repositories should be super shallow.
848
      if 'flash' in mirror.url:
849 850 851 852 853
        depth = 10
      else:
        depth = 10000
    else:
      depth = None
854 855 856
    mirror.populate(verbose=options.verbose,
                    bootstrap=not getattr(options, 'no_bootstrap', False),
                    depth=depth,
857 858
                    ignore_lock=getattr(options, 'ignore_locks', False),
                    lock_timeout=getattr(options, 'lock_timeout', 0))
859
    mirror.unlock()
860

861
  def _Clone(self, revision, url, options):
862 863
    """Clone a git repository from the given URL.

864 865 866 867 868
    Once we've cloned the repo, we checkout a working branch if the specified
    revision is a branch head. If it is a tag or a specific commit, then we
    leave HEAD detached as it makes future updates simpler -- in this case the
    user should first create a new branch or switch to an existing branch before
    making changes in the repo."""
869
    if not options.verbose:
870 871
      # git clone doesn't seem to insert a newline properly before printing
      # to stdout
872
      self.Print('')
873
    cfg = gclient_utils.DefaultIndexPackConfig(url)
874
    clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
875 876
    if self.cache_dir:
      clone_cmd.append('--shared')
877
    if options.verbose:
878
      clone_cmd.append('--verbose')
879
    clone_cmd.append(url)
880 881 882
    # If the parent directory does not exist, Git clone on Windows will not
    # create it, so we need to do it manually.
    parent_dir = os.path.dirname(self.checkout_path)
883
    gclient_utils.safe_makedirs(parent_dir)
884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902

    template_dir = None
    if hasattr(options, 'no_history') and options.no_history:
      if gclient_utils.IsGitSha(revision):
        # In the case of a subproject, the pinned sha is not necessarily the
        # head of the remote branch (so we can't just use --depth=N). Instead,
        # we tell git to fetch all the remote objects from SHA..HEAD by means of
        # a template git dir which has a 'shallow' file pointing to the sha.
        template_dir = tempfile.mkdtemp(
            prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
            dir=parent_dir)
        self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
        with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
          template_file.write(revision)
        clone_cmd.append('--template=' + template_dir)
      else:
        # Otherwise, we're just interested in the HEAD. Just use --depth.
        clone_cmd.append('--depth=1')

903 904 905 906 907
    tmp_dir = tempfile.mkdtemp(
        prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
        dir=parent_dir)
    try:
      clone_cmd.append(tmp_dir)
908
      self._Run(clone_cmd, options, cwd=self._root_dir, retry=True)
909
      gclient_utils.safe_makedirs(self.checkout_path)
910 911
      gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
                                os.path.join(self.checkout_path, '.git'))
912
    except:
913
      traceback.print_exc(file=self.out_fh)
914
      raise
915 916
    finally:
      if os.listdir(tmp_dir):
917
        self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
918
      gclient_utils.rmtree(tmp_dir)
919 920
      if template_dir:
        gclient_utils.rmtree(template_dir)
921
    self._UpdateBranchHeads(options, fetch=True)
922 923
    remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
    self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
924
    if self._GetCurrentBranch() is None:
925
      # Squelch git's very verbose detached HEAD warning and use our own
926
      self.Print(
927 928 929 930
        ('Checked out %s to a detached HEAD. Before making any commits\n'
         'in this repo, you should use \'git checkout <branch>\' to switch to\n'
         'an existing branch or use \'git checkout %s -b <branch>\' to\n'
         'create a new branch for your work.') % (revision, self.remote))
931

932
  def _AskForData(self, prompt, options):
933
    if options.jobs > 1:
934
      self.Print(prompt)
935 936 937 938 939 940 941 942 943 944
      raise gclient_utils.Error("Background task requires input. Rerun "
                                "gclient with --jobs=1 so that\n"
                                "interaction is possible.")
    try:
      return raw_input(prompt)
    except KeyboardInterrupt:
      # Hide the exception.
      sys.exit(1)


945
  def _AttemptRebase(self, upstream, files, options, newbase=None,
946
                     branch=None, printed_path=False, merge=False):
947
    """Attempt to rebase onto either upstream or, if specified, newbase."""
948 949
    if files is not None:
      files.extend(self._Capture(['diff', upstream, '--name-only']).split())
950 951 952
    revision = upstream
    if newbase:
      revision = newbase
953
    action = 'merge' if merge else 'rebase'
954
    if not printed_path:
955
      self.Print('_____ %s : Attempting %s onto %s...' % (
956
          self.relpath, action, revision))
957 958
      printed_path = True
    else:
959
      self.Print('Attempting %s onto %s...' % (action, revision))
960 961 962 963

    if merge:
      merge_output = self._Capture(['merge', revision])
      if options.verbose:
964
        self.Print(merge_output)
965
      return
966 967 968 969

    # Build the rebase command here using the args
    # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
    rebase_cmd = ['rebase']
970
    if options.verbose:
971 972 973 974 975 976 977 978
      rebase_cmd.append('--verbose')
    if newbase:
      rebase_cmd.extend(['--onto', newbase])
    rebase_cmd.append(upstream)
    if branch:
      rebase_cmd.append(branch)

    try:
979
      rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
980
    except subprocess2.CalledProcessError, e:
981 982 983
      if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
          re.match(r'cannot rebase: your index contains uncommitted changes',
                   e.stderr)):
984
        while True:
985
          rebase_action = self._AskForData(
986 987 988
              'Cannot rebase because of unstaged changes.\n'
              '\'git reset --hard HEAD\' ?\n'
              'WARNING: destroys any uncommitted work in your current branch!'
989
              ' (y)es / (q)uit / (s)how : ', options)
990
          if re.match(r'yes|y', rebase_action, re.I):
991
            self._Scrub('HEAD', options)
992
            # Should this be recursive?
993
            rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
994 995 996 997 998 999
            break
          elif re.match(r'quit|q', rebase_action, re.I):
            raise gclient_utils.Error("Please merge or rebase manually\n"
                                      "cd %s && git " % self.checkout_path
                                      + "%s" % ' '.join(rebase_cmd))
          elif re.match(r'show|s', rebase_action, re.I):
1000
            self.Print('%s' % e.stderr.strip())
1001 1002 1003 1004 1005 1006 1007 1008 1009
            continue
          else:
            gclient_utils.Error("Input not recognized")
            continue
      elif re.search(r'^CONFLICT', e.stdout, re.M):
        raise gclient_utils.Error("Conflict while rebasing this branch.\n"
                                  "Fix the conflict and run gclient again.\n"
                                  "See 'man git-rebase' for details.\n")
      else:
1010 1011
        self.Print(e.stdout.strip())
        self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
1012 1013 1014 1015 1016
        raise gclient_utils.Error("Unrecognized error, please merge or rebase "
                                  "manually.\ncd %s && git " %
                                  self.checkout_path
                                  + "%s" % ' '.join(rebase_cmd))

1017
    self.Print(rebase_output.strip())
1018
    if not options.verbose:
1019 1020
      # Make the output a little prettier. It's nice to have some
      # whitespace between projects when syncing.
1021
      self.Print('')
1022

1023 1024
  @staticmethod
  def _CheckMinVersion(min_version):
1025 1026 1027 1028
    (ok, current_version) = scm.GIT.AssertVersion(min_version)
    if not ok:
      raise gclient_utils.Error('git version %s < minimum required %s' %
                                (current_version, min_version))
1029

1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052
  def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
    # Special case handling if all 3 conditions are met:
    #   * the mirros have recently changed, but deps destination remains same,
    #   * the git histories of mirrors are conflicting.
    #   * git cache is used
    # This manifests itself in current checkout having invalid HEAD commit on
    # most git operations. Since git cache is used, just deleted the .git
    # folder, and re-create it by cloning.
    try:
      self._Capture(['rev-list', '-n', '1', 'HEAD'])
    except subprocess2.CalledProcessError as e:
      if ('fatal: bad object HEAD' in e.stderr
          and self.cache_dir and self.cache_dir in url):
        self.Print((
          'Likely due to DEPS change with git cache_dir, '
          'the current commit points to no longer existing object.\n'
          '%s' % e)
        )
        self._DeleteOrMove(options.force)
        self._Clone(revision, url, options)
      else:
        raise

1053 1054 1055 1056 1057 1058 1059 1060 1061
  def _IsRebasing(self):
    # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
    # have a plumbing command to determine whether a rebase is in progress, so
    # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
    g = os.path.join(self.checkout_path, '.git')
    return (
      os.path.isdir(os.path.join(g, "rebase-merge")) or
      os.path.isdir(os.path.join(g, "rebase-apply")))

1062
  def _CheckClean(self, revision, fixup=False):
1063 1064 1065
    lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
    if os.path.exists(lockfile):
      raise gclient_utils.Error(
1066
        '\n____ %s at %s\n'
1067 1068
        '\tYour repo is locked, possibly due to a concurrent git process.\n'
        '\tIf no git executable is running, then clean up %r and try again.\n'
1069
        % (self.relpath, revision, lockfile))
1070

1071 1072 1073
    # Make sure the tree is clean; see git-rebase.sh for reference
    try:
      scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
1074
                      cwd=self.checkout_path)
1075
    except subprocess2.CalledProcessError:
1076
      raise gclient_utils.Error('\n____ %s at %s\n'
1077 1078
                                '\tYou have unstaged changes.\n'
                                '\tPlease commit, stash, or reset.\n'
1079
                                  % (self.relpath, revision))
1080 1081
    try:
      scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
1082
                       '--ignore-submodules', 'HEAD', '--'],
1083
                       cwd=self.checkout_path)
1084
    except subprocess2.CalledProcessError:
1085
      raise gclient_utils.Error('\n____ %s at %s\n'
1086 1087
                                '\tYour index contains uncommitted changes\n'
                                '\tPlease commit, stash, or reset.\n'
1088
                                  % (self.relpath, revision))
1089

1090
  def _CheckDetachedHead(self, revision, _options):
1091 1092 1093 1094
    # HEAD is detached. Make sure it is safe to move away from (i.e., it is
    # reference by a commit). If not, error out -- most likely a rebase is
    # in progress, try to detect so we can give a better error.
    try:
1095 1096
      scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
          cwd=self.checkout_path)
1097
    except subprocess2.CalledProcessError:
1098 1099 1100
      # Commit is not contained by any rev. See if the user is rebasing:
      if self._IsRebasing():
        # Punt to the user
1101
        raise gclient_utils.Error('\n____ %s at %s\n'
1102 1103 1104 1105
                                  '\tAlready in a conflict, i.e. (no branch).\n'
                                  '\tFix the conflict and run gclient again.\n'
                                  '\tOr to abort run:\n\t\tgit-rebase --abort\n'
                                  '\tSee man git-rebase for details.\n'
1106
                                   % (self.relpath, revision))
1107
      # Let's just save off the commit so we can proceed.
1108 1109
      name = ('saved-by-gclient-' +
              self._Capture(['rev-parse', '--short', 'HEAD']))
1110
      self._Capture(['branch', '-f', name])
1111
      self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
1112
          name)
1113

1114
  def _GetCurrentBranch(self):
1115
    # Returns name of current branch or None for detached HEAD
1116
    branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
1117
    if branch == 'HEAD':
1118 1119 1120
      return None
    return branch

1121
  def _Capture(self, args, **kwargs):
1122 1123
    kwargs.setdefault('cwd', self.checkout_path)
    kwargs.setdefault('stderr', subprocess2.PIPE)
1124
    strip = kwargs.pop('strip', True)
1125
    env = scm.GIT.ApplyEnvVars(kwargs)
1126 1127 1128 1129
    ret = subprocess2.check_output(['git'] + args, env=env, **kwargs)
    if strip:
      ret = ret.strip()
    return ret
1130

1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150
  def _Checkout(self, options, ref, force=False, quiet=None):
    """Performs a 'git-checkout' operation.

    Args:
      options: The configured option set
      ref: (str) The branch/commit to checkout
      quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
          'None', the behavior is inferred from 'options.verbose'.
    Returns: (str) The output of the checkout operation
    """
    if quiet is None:
      quiet = (not options.verbose)
    checkout_args = ['checkout']
    if force:
      checkout_args.append('--force')
    if quiet:
      checkout_args.append('--quiet')
    checkout_args.append(ref)
    return self._Capture(checkout_args)

1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
  def _Fetch(self, options, remote=None, prune=False, quiet=False):
    cfg = gclient_utils.DefaultIndexPackConfig(self.url)
    fetch_cmd =  cfg + [
        'fetch',
        remote or self.remote,
    ]

    if prune:
      fetch_cmd.append('--prune')
    if options.verbose:
      fetch_cmd.append('--verbose')
    elif quiet:
      fetch_cmd.append('--quiet')
1164
    self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
1165 1166 1167 1168

    # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
    return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])

1169
  def _UpdateBranchHeads(self, options, fetch=False):
1170 1171 1172
    """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
    if requested."""
    need_fetch = fetch
1173
    if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1174
      config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1175 1176 1177
                    '+refs/branch-heads/*:refs/remotes/branch-heads/*',
                    '^\\+refs/branch-heads/\\*:.*$']
      self._Run(config_cmd, options)
1178 1179 1180 1181 1182 1183 1184 1185
      need_fetch = True
    if hasattr(options, 'with_tags') and options.with_tags:
      config_cmd = ['config', 'remote.%s.fetch' % self.remote,
                    '+refs/tags/*:refs/tags/*',
                    '^\\+refs/tags/\\*:.*$']
      self._Run(config_cmd, options)
      need_fetch = True
    if fetch and need_fetch:
1186
      self._Fetch(options, prune=options.force)
1187

1188
  def _Run(self, args, options, show_header=True, **kwargs):
1189
    # Disable 'unused options' warning | pylint: disable=unused-argument
1190
    kwargs.setdefault('cwd', self.checkout_path)
1191
    kwargs.setdefault('stdout', self.out_fh)
1192
    kwargs['filter_fn'] = self.filter
1193
    kwargs.setdefault('print_stdout', False)
1194
    env = scm.GIT.ApplyEnvVars(kwargs)
1195
    cmd = ['git'] + args
1196
    if show_header:
1197 1198 1199
      gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
    else:
      gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)