repo 30.6 KB
Newer Older
1
#!/usr/bin/env python
2 3 4 5 6 7 8 9 10 11
# -*- coding:utf-8 -*-

"""Repo launcher.

This is a standalone tool that people may copy to anywhere in their system.
It is used to get an initial repo client checkout, and after that it runs the
copy of repo in the checkout.
"""

from __future__ import print_function
12

13
import datetime
14 15 16 17 18 19 20 21 22 23 24 25 26 27
import os
import platform
import subprocess
import sys


def exec_command(cmd):
  """Execute |cmd| or return None on failure."""
  try:
    if platform.system() == 'Windows':
      ret = subprocess.call(cmd)
      sys.exit(ret)
    else:
      os.execvp(cmd[0], cmd)
28
  except Exception:
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
    pass


def check_python_version():
  """Make sure the active Python version is recent enough."""
  def reexec(prog):
    exec_command([prog] + sys.argv)

  MIN_PYTHON_VERSION = (3, 6)

  ver = sys.version_info
  major = ver.major
  minor = ver.minor

  # Abort on very old Python 2 versions.
  if (major, minor) < (2, 7):
    print('repo: error: Your Python version is too old. '
          'Please use Python {}.{} or newer instead.'.format(
              *MIN_PYTHON_VERSION), file=sys.stderr)
    sys.exit(1)

  # Try to re-exec the version specific Python 3 if needed.
  if (major, minor) < MIN_PYTHON_VERSION:
    # Python makes releases ~once a year, so try our min version +10 to help
    # bridge the gap.  This is the fallback anyways so perf isn't critical.
    min_major, min_minor = MIN_PYTHON_VERSION
    for inc in range(0, 10):
      reexec('python{}.{}'.format(min_major, min_minor + inc))

    # Try the generic Python 3 wrapper, but only if it's new enough.  We don't
    # want to go from (still supported) Python 2.7 to (unsupported) Python 3.5.
    try:
      proc = subprocess.Popen(
          ['python3', '-c', 'import sys; '
           'print(sys.version_info.major, sys.version_info.minor)'],
          stdout=subprocess.PIPE, stderr=subprocess.PIPE)
      (output, _) = proc.communicate()
      python3_ver = tuple(int(x) for x in output.decode('utf-8').split())
    except (OSError, subprocess.CalledProcessError):
      python3_ver = None

    # The python3 version looks like it's new enough, so give it a try.
    if python3_ver and python3_ver >= MIN_PYTHON_VERSION:
      reexec('python3')

    # We're still here, so diagnose things for the user.
    if major < 3:
      print('repo: warning: Python 2 is no longer supported; '
            'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION),
            file=sys.stderr)
    else:
      print('repo: error: Python 3 version is too old; '
            'Please use Python {}.{} or newer.'.format(*MIN_PYTHON_VERSION),
            file=sys.stderr)
      sys.exit(1)


if __name__ == '__main__':
  # TODO(vapier): Enable this on Windows once we have Python 3 issues fixed.
  if platform.system() != 'Windows':
    check_python_version()


vapier's avatar
vapier committed
92 93 94 95 96 97
# repo default configuration
#
import os
REPO_URL = os.environ.get('REPO_URL', None)
if not REPO_URL:
  REPO_URL = 'https://chromium.googlesource.com/external/repo'
98 99 100
REPO_REV = os.environ.get('REPO_REV')
if not REPO_REV:
  REPO_REV = 'stable'
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116

# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# increment this whenever we make important changes to this script
117
VERSION = (2, 3)
118 119

# increment this if the MAINTAINER_KEYS block is modified
120
KEYRING_VERSION = (2, 0)
121 122 123

# Each individual key entry is created by using:
# gpg --armor --export keyid
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
MAINTAINER_KEYS = """

     Repo Maintainer <repo@android.kernel.org>
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.2.2 (GNU/Linux)

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
=CMiZ
-----END PGP PUBLIC KEY BLOCK-----
"""

vapier's avatar
vapier committed
169
GIT = 'git'                      # our git command
170 171 172 173 174 175
# NB: The version of git that the repo launcher requires may be much older than
# the version of git that the main repo source tree requires.  Keeping this at
# an older version also makes it easier for users to upgrade/rollback as needed.
#
# git-1.7 is in (EOL) Ubuntu Precise.
MIN_GIT_VERSION = (1, 7, 2)      # minimum supported git version
vapier's avatar
vapier committed
176 177 178 179 180 181
repodir = '.repo'                # name of repo's private directory
S_repo = 'repo'                  # special repo repository
S_manifests = 'manifests'        # special manifest repository
REPO_MAIN = S_repo + '/main.py'  # main script
GITC_CONFIG_FILE = '/gitc/.config'
GITC_FS_ROOT_DIR = '/gitc/manifest-rw/'
182 183


184
import collections
185
import errno
186 187
import optparse
import re
vapier's avatar
vapier committed
188
import shutil
189 190 191 192 193 194 195 196 197 198 199 200 201
import stat

if sys.version_info[0] == 3:
  import urllib.request
  import urllib.error
else:
  import imp
  import urllib2
  urllib = imp.new_module('urllib')
  urllib.request = urllib2
  urllib.error = urllib2


vapier's avatar
vapier committed
202
home_dot_repo = os.path.expanduser('~/.repoconfig')
203 204 205 206 207
gpg_dir = os.path.join(home_dot_repo, 'gnupg')

extra_args = []
init_optparse = optparse.OptionParser(usage="repo init -u url [options]")

208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
def _InitParser():
  """Setup the init subcommand parser."""
  # Logging.
  group = init_optparse.add_option_group('Logging options')
  group.add_option('-q', '--quiet',
                   action='store_true', default=False,
                   help='be quiet')

  # Manifest.
  group = init_optparse.add_option_group('Manifest options')
  group.add_option('-u', '--manifest-url',
                   help='manifest repository location', metavar='URL')
  group.add_option('-b', '--manifest-branch',
                   help='manifest branch or revision', metavar='REVISION')
  group.add_option('-m', '--manifest-name',
                   help='initial manifest file', metavar='NAME.xml')
  group.add_option('--current-branch',
                   dest='current_branch_only', action='store_true',
                   help='fetch only current manifest branch from server')
  group.add_option('--mirror', action='store_true',
                   help='create a replica of the remote repositories '
                        'rather than a client working directory')
  group.add_option('--reference',
                   help='location of mirror directory', metavar='DIR')
  group.add_option('--dissociate', action='store_true',
                   help='dissociate from reference mirrors after clone')
  group.add_option('--depth', type='int', default=None,
                   help='create a shallow clone with given depth; '
                        'see git clone')
  group.add_option('--partial-clone', action='store_true',
                   help='perform partial clone (https://git-scm.com/'
                        'docs/gitrepository-layout#_code_partialclone_code)')
  group.add_option('--clone-filter', action='store', default='blob:none',
                   help='filter for use with --partial-clone '
                        '[default: %default]')
  group.add_option('--archive', action='store_true',
                   help='checkout an archive instead of a git repository for '
                        'each project. See git archive.')
  group.add_option('--submodules', action='store_true',
                   help='sync any submodules associated with the manifest repo')
  group.add_option('-g', '--groups', default='default',
                   help='restrict manifest projects to ones with specified '
                        'group(s) [default|all|G1,G2,G3|G4,-G5,-G6]',
                   metavar='GROUP')
  group.add_option('-p', '--platform', default='auto',
                   help='restrict manifest projects to ones with a specified '
                        'platform group [auto|all|none|linux|darwin|...]',
                   metavar='PLATFORM')
  group.add_option('--no-clone-bundle', action='store_true',
                   help='disable use of /clone.bundle on HTTP/HTTPS')
  group.add_option('--no-tags', action='store_true',
                   help="don't fetch tags in the manifest")

  # Tool.
  group = init_optparse.add_option_group('repo Version options')
  group.add_option('--repo-url', metavar='URL',
                   help='repo repository location ($REPO_URL)')
  group.add_option('--repo-branch', metavar='REVISION',
                   help='repo branch or revision ($REPO_REV)')
  group.add_option('--no-repo-verify', action='store_true',
                   help='do not verify repo source code')

  # Other.
  group = init_optparse.add_option_group('Other options')
  group.add_option('--config-name',
                   action='store_true', default=False,
                   help='Always prompt for name/e-mail')
275

vapier's avatar
vapier committed
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327

def _GitcInitOptions(init_optparse_arg):
  init_optparse_arg.set_usage("repo gitc-init -u url -c client [options]")
  g = init_optparse_arg.add_option_group('GITC options')
  g.add_option('-f', '--manifest-file',
               dest='manifest_file',
               help='Optional manifest file to use for this GITC client.')
  g.add_option('-c', '--gitc-client',
               dest='gitc_client',
               help='The name of the gitc_client instance to create or modify.')

_gitc_manifest_dir = None


def get_gitc_manifest_dir():
  global _gitc_manifest_dir
  if _gitc_manifest_dir is None:
    _gitc_manifest_dir = ''
    try:
      with open(GITC_CONFIG_FILE, 'r') as gitc_config:
        for line in gitc_config:
          match = re.match('gitc_dir=(?P<gitc_manifest_dir>.*)', line)
          if match:
            _gitc_manifest_dir = match.group('gitc_manifest_dir')
    except IOError:
      pass
  return _gitc_manifest_dir


def gitc_parse_clientdir(gitc_fs_path):
  """Parse a path in the GITC FS and return its client name.

  @param gitc_fs_path: A subdirectory path within the GITC_FS_ROOT_DIR.

  @returns: The GITC client name
  """
  if gitc_fs_path == GITC_FS_ROOT_DIR:
    return None
  if not gitc_fs_path.startswith(GITC_FS_ROOT_DIR):
    manifest_dir = get_gitc_manifest_dir()
    if manifest_dir == '':
      return None
    if manifest_dir[-1] != '/':
      manifest_dir += '/'
    if gitc_fs_path == manifest_dir:
      return None
    if not gitc_fs_path.startswith(manifest_dir):
      return None
    return gitc_fs_path.split(manifest_dir)[1].split('/')[0]
  return gitc_fs_path.split(GITC_FS_ROOT_DIR)[1].split('/')[0]


328
class CloneFailure(Exception):
vapier's avatar
vapier committed
329

330 331 332 333
  """Indicate the remote clone of repo itself failed.
  """


vapier's avatar
vapier committed
334
def _Init(args, gitc_init=False):
335 336
  """Installs repo by cloning it over the network.
  """
vapier's avatar
vapier committed
337 338
  if gitc_init:
    _GitcInitOptions(init_optparse)
339
  opt, args = init_optparse.parse_args(args)
340
  if args:
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
    init_optparse.print_usage()
    sys.exit(1)

  url = opt.repo_url
  if not url:
    url = REPO_URL
    extra_args.append('--repo-url=%s' % url)

  branch = opt.repo_branch
  if not branch:
    branch = REPO_REV
    extra_args.append('--repo-branch=%s' % branch)

  if branch.startswith('refs/heads/'):
    branch = branch[len('refs/heads/'):]
  if branch.startswith('refs/'):
357
    print("fatal: invalid branch name '%s'" % branch, file=sys.stderr)
358 359
    raise CloneFailure()

360
  try:
vapier's avatar
vapier committed
361 362 363
    if gitc_init:
      gitc_manifest_dir = get_gitc_manifest_dir()
      if not gitc_manifest_dir:
364 365
        print('fatal: GITC filesystem is not available. Exiting...',
              file=sys.stderr)
vapier's avatar
vapier committed
366 367 368 369 370
        sys.exit(1)
      gitc_client = opt.gitc_client
      if not gitc_client:
        gitc_client = gitc_parse_clientdir(os.getcwd())
      if not gitc_client:
371
        print('fatal: GITC client (-c) is required.', file=sys.stderr)
vapier's avatar
vapier committed
372 373 374 375 376 377 378 379 380
        sys.exit(1)
      client_dir = os.path.join(gitc_manifest_dir, gitc_client)
      if not os.path.exists(client_dir):
        os.makedirs(client_dir)
      os.chdir(client_dir)
      if os.path.exists(repodir):
        # This GITC Client has already initialized repo so continue.
        return

381 382 383
    os.mkdir(repodir)
  except OSError as e:
    if e.errno != errno.EEXIST:
384 385
      print('fatal: cannot make %s directory: %s'
            % (repodir, e.strerror), file=sys.stderr)
386
      # Don't raise CloneFailure; that would delete the
387 388 389 390 391 392
      # name. Instead exit immediately.
      #
      sys.exit(1)

  _CheckGitVersion()
  try:
393 394
    if opt.no_repo_verify:
      do_verify = False
395
    else:
396 397 398 399
      if NeedSetupGnuPG():
        do_verify = SetupGnuPG(opt.quiet)
      else:
        do_verify = True
400 401

    dst = os.path.abspath(os.path.join(repodir, S_repo))
vapier's avatar
vapier committed
402
    _Clone(url, dst, opt.quiet, not opt.no_clone_bundle)
403

404
    if do_verify:
405 406 407 408 409
      rev = _Verify(dst, branch, opt.quiet)
    else:
      rev = 'refs/remotes/origin/%s^0' % branch

    _Checkout(dst, branch, rev, opt.quiet)
410 411 412 413 414

    if not os.path.isfile(os.path.join(dst, 'repo')):
      print("warning: '%s' does not look like a git-repo repository, is "
            "REPO_URL set correctly?" % url, file=sys.stderr)

415 416
  except CloneFailure:
    if opt.quiet:
417 418
      print('fatal: repo init failed; run without --quiet to see why',
            file=sys.stderr)
419 420 421
    raise


422 423 424 425 426 427 428 429 430 431
# The git version info broken down into components for easy analysis.
# Similar to Python's sys.version_info.
GitVersion = collections.namedtuple(
    'GitVersion', ('major', 'minor', 'micro', 'full'))

def ParseGitVersion(ver_str=None):
  if ver_str is None:
    # Load the version ourselves.
    ver_str = _GetGitVersion()

432 433 434
  if not ver_str.startswith('git version '):
    return None

435 436
  full_version = ver_str[len('git version '):].strip()
  num_ver_str = full_version.split('-')[0]
437 438 439 440 441 442
  to_tuple = []
  for num_str in num_ver_str.split('.')[:3]:
    if num_str.isdigit():
      to_tuple.append(int(num_str))
    else:
      to_tuple.append(0)
443 444
  to_tuple.append(full_version)
  return GitVersion(*to_tuple)
445 446


447
def _GetGitVersion():
448
  cmd = [GIT, '--version']
449 450
  try:
    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
451
  except OSError as e:
452 453 454 455 456 457 458
    print(file=sys.stderr)
    print("fatal: '%s' is not available" % GIT, file=sys.stderr)
    print('fatal: %s' % e, file=sys.stderr)
    print(file=sys.stderr)
    print('Please make sure %s is installed and in your path.' % GIT,
          file=sys.stderr)
    raise
459

460 461 462
  ver_str = proc.stdout.read().strip()
  proc.stdout.close()
  proc.wait()
463 464 465 466 467 468 469 470
  return ver_str.decode('utf-8')


def _CheckGitVersion():
  try:
    ver_act = ParseGitVersion()
  except OSError:
    raise CloneFailure()
471

472
  if ver_act is None:
473
    print('fatal: unable to detect git version', file=sys.stderr)
474 475 476
    raise CloneFailure()

  if ver_act < MIN_GIT_VERSION:
477
    need = '.'.join(map(str, MIN_GIT_VERSION))
478
    print('fatal: git %s or later required' % need, file=sys.stderr)
479 480 481
    raise CloneFailure()


482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
def SetGitTrace2ParentSid(env=None):
  """Set up GIT_TRACE2_PARENT_SID for git tracing."""
  # We roughly follow the format git itself uses in trace2/tr2_sid.c.
  # (1) Be unique (2) be valid filename (3) be fixed length.
  #
  # Since we always export this variable, we try to avoid more expensive calls.
  # e.g. We don't attempt hostname lookups or hashing the results.
  if env is None:
    env = os.environ

  KEY = 'GIT_TRACE2_PARENT_SID'

  now = datetime.datetime.utcnow()
  value = 'repo-%s-P%08x' % (now.strftime('%Y%m%dT%H%M%SZ'), os.getpid())

  # If it's already set, then append ourselves.
  if KEY in env:
    value = env[KEY] + '/' + value

  _setenv(KEY, value, env=env)


def _setenv(key, value, env=None):
  """Set |key| in the OS environment |env| to |value|."""
  if env is None:
    env = os.environ
  # Environment handling across systems is messy.
  try:
    env[key] = value
  except UnicodeEncodeError:
    env[key] = value.encode()


515
def NeedSetupGnuPG():
516 517 518 519 520 521 522 523 524 525 526
  if not os.path.isdir(home_dot_repo):
    return True

  kv = os.path.join(home_dot_repo, 'keyring-version')
  if not os.path.exists(kv):
    return True

  kv = open(kv).read()
  if not kv:
    return True

527
  kv = tuple(map(int, kv.split('.')))
528 529 530 531 532
  if kv < KEYRING_VERSION:
    return True
  return False


533
def SetupGnuPG(quiet):
534 535 536 537
  try:
    os.mkdir(home_dot_repo)
  except OSError as e:
    if e.errno != errno.EEXIST:
538 539
      print('fatal: cannot make %s directory: %s'
            % (home_dot_repo, e.strerror), file=sys.stderr)
540 541
      sys.exit(1)

542 543 544 545
  try:
    os.mkdir(gpg_dir, stat.S_IRWXU)
  except OSError as e:
    if e.errno != errno.EEXIST:
546 547
      print('fatal: cannot make %s directory: %s' % (gpg_dir, e.strerror),
            file=sys.stderr)
548 549
      sys.exit(1)

550
  env = os.environ.copy()
551
  _setenv('GNUPGHOME', gpg_dir, env)
552 553 554 555

  cmd = ['gpg', '--import']
  try:
    proc = subprocess.Popen(cmd,
vapier's avatar
vapier committed
556 557
                            env=env,
                            stdin=subprocess.PIPE)
558
  except OSError as e:
559
    if not quiet:
560 561 562
      print('warning: gpg (GnuPG) is not available.', file=sys.stderr)
      print('warning: Installing it is strongly encouraged.', file=sys.stderr)
      print(file=sys.stderr)
563 564
    return False

565
  proc.stdin.write(MAINTAINER_KEYS.encode('utf-8'))
566 567 568
  proc.stdin.close()

  if proc.wait() != 0:
569
    print('fatal: registering repo maintainer keys failed', file=sys.stderr)
570
    sys.exit(1)
571
  print()
572

573 574
  with open(os.path.join(home_dot_repo, 'keyring-version'), 'w') as fd:
    fd.write('.'.join(map(str, KEYRING_VERSION)) + '\n')
575 576 577 578 579 580 581
  return True


def _SetConfig(local, name, value):
  """Set a git configuration option to the specified value.
  """
  cmd = [GIT, 'config', name, value]
vapier's avatar
vapier committed
582
  if subprocess.Popen(cmd, cwd=local).wait() != 0:
583 584 585
    raise CloneFailure()


586 587 588
def _InitHttp():
  handlers = []

589
  mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
590 591 592 593 594
  try:
    import netrc
    n = netrc.netrc()
    for host in n.hosts:
      p = n.hosts[host]
vapier's avatar
vapier committed
595
      mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2])
596
      mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2])
597
  except:
598
    pass
599 600
  handlers.append(urllib.request.HTTPBasicAuthHandler(mgr))
  handlers.append(urllib.request.HTTPDigestAuthHandler(mgr))
601 602 603

  if 'http_proxy' in os.environ:
    url = os.environ['http_proxy']
604
    handlers.append(urllib.request.ProxyHandler({'http': url, 'https': url}))
605
  if 'REPO_CURL_VERBOSE' in os.environ:
606 607 608
    handlers.append(urllib.request.HTTPHandler(debuglevel=1))
    handlers.append(urllib.request.HTTPSHandler(debuglevel=1))
  urllib.request.install_opener(urllib.request.build_opener(*handlers))
609

vapier's avatar
vapier committed
610

611 612
def _Fetch(url, local, src, quiet):
  if not quiet:
613
    print('Get %s' % url, file=sys.stderr)
614

615 616 617 618 619 620
  cmd = [GIT, 'fetch']
  if quiet:
    cmd.append('--quiet')
    err = subprocess.PIPE
  else:
    err = None
621 622
  cmd.append(src)
  cmd.append('+refs/heads/*:refs/remotes/origin/*')
623
  cmd.append('+refs/tags/*:refs/tags/*')
624

vapier's avatar
vapier committed
625
  proc = subprocess.Popen(cmd, cwd=local, stderr=err)
626 627 628 629 630 631
  if err:
    proc.stderr.read()
    proc.stderr.close()
  if proc.wait() != 0:
    raise CloneFailure()

vapier's avatar
vapier committed
632

633 634 635 636 637 638
def _DownloadBundle(url, local, quiet):
  if not url.endswith('/'):
    url += '/'
  url += 'clone.bundle'

  proc = subprocess.Popen(
vapier's avatar
vapier committed
639 640 641
      [GIT, 'config', '--get-regexp', 'url.*.insteadof'],
      cwd=local,
      stdout=subprocess.PIPE)
642
  for line in proc.stdout:
643
    line = line.decode('utf-8')
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
    m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line)
    if m:
      new_url = m.group(1)
      old_url = m.group(2)
      if url.startswith(old_url):
        url = new_url + url[len(old_url):]
        break
  proc.stdout.close()
  proc.wait()

  if not url.startswith('http:') and not url.startswith('https:'):
    return False

  dest = open(os.path.join(local, '.git', 'clone.bundle'), 'w+b')
  try:
    try:
660 661
      r = urllib.request.urlopen(url)
    except urllib.error.HTTPError as e:
vapier's avatar
vapier committed
662
      if e.code in [401, 403, 404, 501]:
663
        return False
664 665
      print('fatal: Cannot get %s' % url, file=sys.stderr)
      print('fatal: HTTP error %s' % e.code, file=sys.stderr)
666
      raise CloneFailure()
667
    except urllib.error.URLError as e:
668 669
      print('fatal: Cannot get %s' % url, file=sys.stderr)
      print('fatal: error %s' % e.reason, file=sys.stderr)
670 671 672
      raise CloneFailure()
    try:
      if not quiet:
673
        print('Get %s' % url, file=sys.stderr)
674 675
      while True:
        buf = r.read(8192)
676
        if not buf:
677 678 679 680 681 682 683
          return True
        dest.write(buf)
    finally:
      r.close()
  finally:
    dest.close()

vapier's avatar
vapier committed
684

685 686 687 688 689 690
def _ImportBundle(local):
  path = os.path.join(local, '.git', 'clone.bundle')
  try:
    _Fetch(local, local, path, True)
  finally:
    os.remove(path)
691

vapier's avatar
vapier committed
692 693

def _Clone(url, local, quiet, clone_bundle):
694 695 696 697
  """Clones a git repository to a new subdirectory of repodir
  """
  try:
    os.mkdir(local)
698
  except OSError as e:
699 700
    print('fatal: cannot make %s directory: %s' % (local, e.strerror),
          file=sys.stderr)
701 702 703 704
    raise CloneFailure()

  cmd = [GIT, 'init', '--quiet']
  try:
vapier's avatar
vapier committed
705
    proc = subprocess.Popen(cmd, cwd=local)
706
  except OSError as e:
707 708 709 710 711 712
    print(file=sys.stderr)
    print("fatal: '%s' is not available" % GIT, file=sys.stderr)
    print('fatal: %s' % e, file=sys.stderr)
    print(file=sys.stderr)
    print('Please make sure %s is installed and in your path.' % GIT,
          file=sys.stderr)
713 714
    raise CloneFailure()
  if proc.wait() != 0:
715
    print('fatal: could not create %s' % local, file=sys.stderr)
716 717
    raise CloneFailure()

718
  _InitHttp()
719
  _SetConfig(local, 'remote.origin.url', url)
vapier's avatar
vapier committed
720 721 722 723
  _SetConfig(local,
             'remote.origin.fetch',
             '+refs/heads/*:refs/remotes/origin/*')
  if clone_bundle and _DownloadBundle(url, local, quiet):
724
    _ImportBundle(local)
vapier's avatar
vapier committed
725
  _Fetch(url, local, 'origin', quiet)
726 727 728 729 730 731 732 733 734


def _Verify(cwd, branch, quiet):
  """Verify the branch has been signed by a tag.
  """
  cmd = [GIT, 'describe', 'origin/%s' % branch]
  proc = subprocess.Popen(cmd,
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE,
vapier's avatar
vapier committed
735
                          cwd=cwd)
736
  cur = proc.stdout.read().strip().decode('utf-8')
737 738 739 740 741 742
  proc.stdout.close()

  proc.stderr.read()
  proc.stderr.close()

  if proc.wait() != 0 or not cur:
743 744
    print(file=sys.stderr)
    print("fatal: branch '%s' has not been signed" % branch, file=sys.stderr)
745 746 747 748 749 750
    raise CloneFailure()

  m = re.compile(r'^(.*)-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur)
  if m:
    cur = m.group(1)
    if not quiet:
751 752 753 754
      print(file=sys.stderr)
      print("info: Ignoring branch '%s'; using tagged release '%s'"
            % (branch, cur), file=sys.stderr)
      print(file=sys.stderr)
755

756
  env = os.environ.copy()
757
  _setenv('GNUPGHOME', gpg_dir, env)
758 759 760

  cmd = [GIT, 'tag', '-v', cur]
  proc = subprocess.Popen(cmd,
vapier's avatar
vapier committed
761 762 763 764
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE,
                          cwd=cwd,
                          env=env)
765
  out = proc.stdout.read().decode('utf-8')
766 767
  proc.stdout.close()

768
  err = proc.stderr.read().decode('utf-8')
769 770 771
  proc.stderr.close()

  if proc.wait() != 0:
772 773 774 775
    print(file=sys.stderr)
    print(out, file=sys.stderr)
    print(err, file=sys.stderr)
    print(file=sys.stderr)
776 777 778 779 780 781 782 783
    raise CloneFailure()
  return '%s^0' % cur


def _Checkout(cwd, branch, rev, quiet):
  """Checkout an upstream branch into the repository and track it.
  """
  cmd = [GIT, 'update-ref', 'refs/heads/default', rev]
vapier's avatar
vapier committed
784
  if subprocess.Popen(cmd, cwd=cwd).wait() != 0:
785 786 787 788 789 790
    raise CloneFailure()

  _SetConfig(cwd, 'branch.default.remote', 'origin')
  _SetConfig(cwd, 'branch.default.merge', 'refs/heads/%s' % branch)

  cmd = [GIT, 'symbolic-ref', 'HEAD', 'refs/heads/default']
vapier's avatar
vapier committed
791
  if subprocess.Popen(cmd, cwd=cwd).wait() != 0:
792 793 794 795 796 797
    raise CloneFailure()

  cmd = [GIT, 'read-tree', '--reset', '-u']
  if not quiet:
    cmd.append('-v')
  cmd.append('HEAD')
vapier's avatar
vapier committed
798
  if subprocess.Popen(cmd, cwd=cwd).wait() != 0:
799 800 801 802 803 804
    raise CloneFailure()


def _FindRepo():
  """Look for a repo installation, starting at the current directory.
  """
805
  curdir = os.getcwd()
806 807
  repo = None

808
  olddir = None
809
  while curdir != '/' \
vapier's avatar
vapier committed
810 811
          and curdir != olddir \
          and not repo:
812
    repo = os.path.join(curdir, repodir, REPO_MAIN)
813 814
    if not os.path.isfile(repo):
      repo = None
815 816 817
      olddir = curdir
      curdir = os.path.dirname(curdir)
  return (repo, os.path.join(curdir, repodir))
818 819


vapier's avatar
vapier committed
820
class _Options(object):
821
  help = False
822
  version = False
823 824 825 826 827 828 829


def _ParseArguments(args):
  cmd = None
  opt = _Options()
  arg = []

830
  for i in range(len(args)):
831 832 833
    a = args[i]
    if a == '-h' or a == '--help':
      opt.help = True
834 835
    elif a == '--version':
      opt.version = True
836 837 838 839 840 841 842 843
    elif not a.startswith('-'):
      cmd = a
      arg = args[i + 1:]
      break
  return cmd, opt, arg


def _Usage():
vapier's avatar
vapier committed
844 845 846 847
  gitc_usage = ""
  if get_gitc_manifest_dir():
    gitc_usage = "  gitc-init Initialize a GITC Client.\n"

848
  print(
vapier's avatar
vapier committed
849
      """usage: repo COMMAND [ARGS]
850 851 852 853 854 855

repo is not yet installed.  Use "repo init" to install it here.

The most commonly used repo commands are:

  init      Install repo in the current working directory
vapier's avatar
vapier committed
856 857
""" + gitc_usage +
      """  help      Display detailed help on a command
858 859

For access to the full online help, install repo ("repo init").
860 861
""")
  sys.exit(0)
862 863 864 865 866 867


def _Help(args):
  if args:
    if args[0] == 'init':
      init_optparse.print_help()
868
      sys.exit(0)
vapier's avatar
vapier committed
869 870 871 872
    elif args[0] == 'gitc-init':
      _GitcInitOptions(init_optparse)
      init_optparse.print_help()
      sys.exit(0)
873
    else:
874 875 876
      print("error: '%s' is not a bootstrap command.\n"
            '        For access to online help, install repo ("repo init").'
            % args[0], file=sys.stderr)
877 878 879 880 881
  else:
    _Usage()
  sys.exit(1)


882 883 884 885 886 887 888 889 890 891
def _Version():
  """Show version information."""
  print('<repo not installed>')
  print('repo launcher version %s' % ('.'.join(str(x) for x in VERSION),))
  print('       (from %s)' % (__file__,))
  print('git %s' % (ParseGitVersion().full,))
  print('Python %s' % sys.version)
  sys.exit(0)


892
def _NotInstalled():
893 894
  print('error: repo is not installed.  Use "repo init" to install it here.',
        file=sys.stderr)
895 896 897 898
  sys.exit(1)


def _NoCommands(cmd):
899 900
  print("""error: command '%s' requires repo to be installed first.
        Use "repo init" to install it here.""" % cmd, file=sys.stderr)
901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927
  sys.exit(1)


def _RunSelf(wrapper_path):
  my_dir = os.path.dirname(wrapper_path)
  my_main = os.path.join(my_dir, 'main.py')
  my_git = os.path.join(my_dir, '.git')

  if os.path.isfile(my_main) and os.path.isdir(my_git):
    for name in ['git_config.py',
                 'project.py',
                 'subcmds']:
      if not os.path.exists(os.path.join(my_dir, name)):
        return None, None
    return my_main, my_git
  return None, None


def _SetDefaultsTo(gitdir):
  global REPO_URL
  global REPO_REV

  REPO_URL = gitdir
  proc = subprocess.Popen([GIT,
                           '--git-dir=%s' % gitdir,
                           'symbolic-ref',
                           'HEAD'],
vapier's avatar
vapier committed
928 929
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
930
  REPO_REV = proc.stdout.read().strip().decode('utf-8')
931 932 933 934 935 936
  proc.stdout.close()

  proc.stderr.read()
  proc.stderr.close()

  if proc.wait() != 0:
937
    print('fatal: %s has no current branch' % gitdir, file=sys.stderr)
938 939 940 941 942 943
    sys.exit(1)


def main(orig_args):
  cmd, opt, args = _ParseArguments(orig_args)

944 945 946
  # We run this early as we run some git commands ourselves.
  SetGitTrace2ParentSid()

vapier's avatar
vapier committed
947 948 949 950 951
  repo_main, rel_repo_dir = None, None
  # Don't use the local repo copy, make sure to switch to the gitc client first.
  if cmd != 'gitc-init':
    repo_main, rel_repo_dir = _FindRepo()

952 953 954
  wrapper_path = os.path.abspath(__file__)
  my_main, my_git = _RunSelf(wrapper_path)

vapier's avatar
vapier committed
955 956
  cwd = os.getcwd()
  if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()):
957 958 959 960
    print('error: repo cannot be used in the GITC local manifest directory.'
          '\nIf you want to work on this GITC client please rerun this '
          'command from the corresponding client under /gitc/',
          file=sys.stderr)
vapier's avatar
vapier committed
961
    sys.exit(1)
962
  _InitParser()
963
  if not repo_main:
964 965 966 967
    if opt.help:
      _Usage()
    if cmd == 'help':
      _Help(args)
968 969
    if opt.version or cmd == 'version':
      _Version()
970 971
    if not cmd:
      _NotInstalled()
vapier's avatar
vapier committed
972
    if cmd == 'init' or cmd == 'gitc-init':
973 974 975
      if my_git:
        _SetDefaultsTo(my_git)
      try:
vapier's avatar
vapier committed
976
        _Init(args, gitc_init=(cmd == 'gitc-init'))
977
      except CloneFailure:
978
        path = os.path.join(repodir, S_repo)
979 980
        print("fatal: cloning the git-repo repository failed, will remove "
              "'%s' " % path, file=sys.stderr)
981
        shutil.rmtree(path, ignore_errors=True)
982
        sys.exit(1)
983
      repo_main, rel_repo_dir = _FindRepo()
984 985 986 987
    else:
      _NoCommands(cmd)

  if my_main:
988
    repo_main = my_main
989

990 991
  ver_str = '.'.join(map(str, VERSION))
  me = [sys.executable, repo_main,
992
        '--repo-dir=%s' % rel_repo_dir,
993 994 995 996 997
        '--wrapper-version=%s' % ver_str,
        '--wrapper-path=%s' % wrapper_path,
        '--']
  me.extend(orig_args)
  me.extend(extra_args)
998 999 1000
  exec_command(me)
  print("fatal: unable to start %s" % repo_main, file=sys.stderr)
  sys.exit(148)
1001 1002 1003 1004


if __name__ == '__main__':
  main(sys.argv[1:])