1#!/usr/bin/env python 2# -*- coding:utf-8 -*- 3# 4# Copyright (C) 2008 The Android Open Source Project 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17 18"""Repo launcher. 19 20This is a standalone tool that people may copy to anywhere in their system. 21It is used to get an initial repo client checkout, and after that it runs the 22copy of repo in the checkout. 23""" 24 25from __future__ import print_function 26 27import datetime 28import os 29import platform 30import shlex 31import subprocess 32import sys 33 34 35# Keep basic logic in sync with repo_trace.py. 36class Trace(object): 37 """Trace helper logic.""" 38 39 REPO_TRACE = 'REPO_TRACE' 40 41 def __init__(self): 42 self.set(os.environ.get(self.REPO_TRACE) == '1') 43 44 def set(self, value): 45 self.enabled = bool(value) 46 47 def print(self, *args, **kwargs): 48 if self.enabled: 49 print(*args, **kwargs) 50 51 52trace = Trace() 53 54 55def exec_command(cmd): 56 """Execute |cmd| or return None on failure.""" 57 trace.print(':', ' '.join(cmd)) 58 try: 59 if platform.system() == 'Windows': 60 ret = subprocess.call(cmd) 61 sys.exit(ret) 62 else: 63 os.execvp(cmd[0], cmd) 64 except Exception: 65 pass 66 67 68def check_python_version(): 69 """Make sure the active Python version is recent enough.""" 70 def reexec(prog): 71 exec_command([prog] + sys.argv) 72 73 MIN_PYTHON_VERSION = (3, 6) 74 75 ver = sys.version_info 76 major = ver.major 77 minor = ver.minor 78 79 # Abort on very old Python 2 versions. 80 if (major, minor) < (2, 7): 81 print('repo: error: Your Python version is too old. ' 82 'Please use Python {}.{} or newer instead.'.format( 83 *MIN_PYTHON_VERSION), file=sys.stderr) 84 sys.exit(1) 85 86 # Try to re-exec the version specific Python 3 if needed. 87 if (major, minor) < MIN_PYTHON_VERSION: 88 # Python makes releases ~once a year, so try our min version +10 to help 89 # bridge the gap. This is the fallback anyways so perf isn't critical. 90 min_major, min_minor = MIN_PYTHON_VERSION 91 for inc in range(0, 10): 92 reexec('python{}.{}'.format(min_major, min_minor + inc)) 93 94 # Try the generic Python 3 wrapper, but only if it's new enough. We don't 95 # want to go from (still supported) Python 2.7 to (unsupported) Python 3.5. 96 try: 97 proc = subprocess.Popen( 98 ['python3', '-c', 'import sys; ' 99 'print(sys.version_info.major, sys.version_info.minor)'], 100 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 101 (output, _) = proc.communicate() 102 python3_ver = tuple(int(x) for x in output.decode('utf-8').split()) 103 except (OSError, subprocess.CalledProcessError): 104 python3_ver = None 105 106 # The python3 version looks like it's new enough, so give it a try. 107 if python3_ver and python3_ver >= MIN_PYTHON_VERSION: 108 reexec('python3') 109 110 # We're still here, so diagnose things for the user. 111 if major < 3: 112 print('repo: warning: Python 2 is no longer supported; ' 113 'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION), 114 file=sys.stderr) 115 else: 116 print('repo: error: Python 3 version is too old; ' 117 'Please use Python {}.{} or newer.'.format(*MIN_PYTHON_VERSION), 118 file=sys.stderr) 119 sys.exit(1) 120 121 122if __name__ == '__main__': 123 check_python_version() 124 125 126# repo default configuration 127# 128REPO_URL = os.environ.get('REPO_URL', None) 129if not REPO_URL: 130 REPO_URL = 'https://gitee.com/oschina/repo.git' 131REPO_REV = os.environ.get('REPO_REV') 132if not REPO_REV: 133 REPO_REV = 'fork_flow' 134 135# increment this whenever we make important changes to this script 136VERSION = (2, 8) 137 138# increment this if the MAINTAINER_KEYS block is modified 139KEYRING_VERSION = (2, 3) 140 141# Each individual key entry is created by using: 142# gpg --armor --export keyid 143MAINTAINER_KEYS = """ 144 145 Repo Maintainer <repo@android.kernel.org> 146-----BEGIN PGP PUBLIC KEY BLOCK----- 147 148-----END PGP PUBLIC KEY BLOCK----- 149""" 150 151GIT = 'git' # our git command 152# NB: The version of git that the repo launcher requires may be much older than 153# the version of git that the main repo source tree requires. Keeping this at 154# an older version also makes it easier for users to upgrade/rollback as needed. 155# 156# git-1.7 is in (EOL) Ubuntu Precise. 157MIN_GIT_VERSION = (1, 7, 2) # minimum supported git version 158repodir = '.repo' # name of repo's private directory 159S_repo = 'repo' # special repo repository 160S_manifests = 'manifests' # special manifest repository 161REPO_MAIN = S_repo + '/main.py' # main script 162GITC_CONFIG_FILE = '/gitc/.config' 163GITC_FS_ROOT_DIR = '/gitc/manifest-rw/' 164 165 166import collections 167import errno 168import optparse 169import re 170import shutil 171import stat 172 173if sys.version_info[0] == 3: 174 import urllib.request 175 import urllib.error 176else: 177 import imp 178 import urllib2 179 urllib = imp.new_module('urllib') 180 urllib.request = urllib2 181 urllib.error = urllib2 182 183 184home_dot_repo = os.path.expanduser('~/.repoconfig') 185gpg_dir = os.path.join(home_dot_repo, 'gnupg') 186 187 188def GetParser(gitc_init=False): 189 """Setup the CLI parser.""" 190 if gitc_init: 191 usage = 'repo gitc-init -u url -c client [options]' 192 else: 193 usage = 'repo init -u url [options]' 194 195 parser = optparse.OptionParser(usage=usage) 196 197 # Logging. 198 group = parser.add_option_group('Logging options') 199 group.add_option('-v', '--verbose', 200 dest='output_mode', action='store_true', 201 help='show all output') 202 group.add_option('-q', '--quiet', 203 dest='output_mode', action='store_false', 204 help='only show errors') 205 206 # Manifest. 207 group = parser.add_option_group('Manifest options') 208 group.add_option('-u', '--manifest-url', 209 help='manifest repository location', metavar='URL') 210 group.add_option('-b', '--manifest-branch', 211 help='manifest branch or revision', metavar='REVISION') 212 group.add_option('-m', '--manifest-name', 213 help='initial manifest file', metavar='NAME.xml') 214 cbr_opts = ['--current-branch'] 215 # The gitc-init subcommand allocates -c itself, but a lot of init users 216 # want -c, so try to satisfy both as best we can. 217 if not gitc_init: 218 cbr_opts += ['-c'] 219 group.add_option(*cbr_opts, 220 dest='current_branch_only', action='store_true', 221 help='fetch only current manifest branch from server') 222 group.add_option('--mirror', action='store_true', 223 help='create a replica of the remote repositories ' 224 'rather than a client working directory') 225 group.add_option('--reference', 226 help='location of mirror directory', metavar='DIR') 227 group.add_option('--dissociate', action='store_true', 228 help='dissociate from reference mirrors after clone') 229 group.add_option('--depth', type='int', default=None, 230 help='create a shallow clone with given depth; ' 231 'see git clone') 232 group.add_option('--partial-clone', action='store_true', 233 help='perform partial clone (https://git-scm.com/' 234 'docs/gitrepository-layout#_code_partialclone_code)') 235 group.add_option('--clone-filter', action='store', default='blob:none', 236 help='filter for use with --partial-clone ' 237 '[default: %default]') 238 group.add_option('--worktree', action='store_true', 239 help=optparse.SUPPRESS_HELP) 240 group.add_option('--archive', action='store_true', 241 help='checkout an archive instead of a git repository for ' 242 'each project. See git archive.') 243 group.add_option('--submodules', action='store_true', 244 help='sync any submodules associated with the manifest repo') 245 group.add_option('-g', '--groups', default='default', 246 help='restrict manifest projects to ones with specified ' 247 'group(s) [default|all|G1,G2,G3|G4,-G5,-G6]', 248 metavar='GROUP') 249 group.add_option('-p', '--platform', default='auto', 250 help='restrict manifest projects to ones with a specified ' 251 'platform group [auto|all|none|linux|darwin|...]', 252 metavar='PLATFORM') 253 group.add_option('--clone-bundle', action='store_false', 254 help='enable use of /clone.bundle on HTTP/HTTPS (default if not --partial-clone). ' 255 'WARNING: Not currently supported') 256 group.add_option('--no-clone-bundle', 257 dest='clone_bundle', action='store_false', default=False, 258 help='disable use of /clone.bundle on HTTP/HTTPS (default if --partial-clone). ' 259 'WARNING: Not currently supported') 260 group.add_option('--no-tags', 261 dest='tags', default=True, action='store_false', 262 help="don't fetch tags in the manifest") 263 264 # Tool. 265 group = parser.add_option_group('repo Version options') 266 group.add_option('--repo-url', metavar='URL', 267 help='repo repository location ($REPO_URL)') 268 group.add_option('--repo-rev', metavar='REV', 269 help='repo branch or revision ($REPO_REV)') 270 group.add_option('--repo-branch', dest='repo_rev', 271 help=optparse.SUPPRESS_HELP) 272 group.add_option('--no-repo-verify', 273 dest='repo_verify', default=False, action='store_true', 274 help='do not verify repo source code') 275 276 # Other. 277 group = parser.add_option_group('Other options') 278 group.add_option('--config-name', 279 action='store_true', default=False, 280 help='Always prompt for name/e-mail') 281 282 # gitc-init specific settings. 283 if gitc_init: 284 group = parser.add_option_group('GITC options') 285 group.add_option('-f', '--manifest-file', 286 help='Optional manifest file to use for this GITC client.') 287 group.add_option('-c', '--gitc-client', 288 help='Name of the gitc_client instance to create or modify.') 289 290 return parser 291 292 293# This is a poor replacement for subprocess.run until we require Python 3.6+. 294RunResult = collections.namedtuple( 295 'RunResult', ('returncode', 'stdout', 'stderr')) 296 297 298class RunError(Exception): 299 """Error when running a command failed.""" 300 301 302def run_command(cmd, **kwargs): 303 """Run |cmd| and return its output.""" 304 check = kwargs.pop('check', False) 305 if kwargs.pop('capture_output', False): 306 kwargs.setdefault('stdout', subprocess.PIPE) 307 kwargs.setdefault('stderr', subprocess.PIPE) 308 cmd_input = kwargs.pop('input', None) 309 310 def decode(output): 311 """Decode |output| to text.""" 312 if output is None: 313 return output 314 try: 315 return output.decode('utf-8') 316 except UnicodeError: 317 print('repo: warning: Invalid UTF-8 output:\ncmd: %r\n%r' % (cmd, output), 318 file=sys.stderr) 319 # TODO(vapier): Once we require Python 3, use 'backslashreplace'. 320 return output.decode('utf-8', 'replace') 321 322 # Run & package the results. 323 proc = subprocess.Popen(cmd, **kwargs) 324 (stdout, stderr) = proc.communicate(input=cmd_input) 325 dbg = ': ' + ' '.join(cmd) 326 if cmd_input is not None: 327 dbg += ' 0<|' 328 if stdout == subprocess.PIPE: 329 dbg += ' 1>|' 330 if stderr == subprocess.PIPE: 331 dbg += ' 2>|' 332 elif stderr == subprocess.STDOUT: 333 dbg += ' 2>&1' 334 trace.print(dbg) 335 ret = RunResult(proc.returncode, decode(stdout), decode(stderr)) 336 337 # If things failed, print useful debugging output. 338 if check and ret.returncode: 339 print('repo: error: "%s" failed with exit status %s' % 340 (cmd[0], ret.returncode), file=sys.stderr) 341 print(' cwd: %s\n cmd: %r' % 342 (kwargs.get('cwd', os.getcwd()), cmd), file=sys.stderr) 343 344 def _print_output(name, output): 345 if output: 346 print(' %s:\n >> %s' % (name, '\n >> '.join(output.splitlines())), 347 file=sys.stderr) 348 349 _print_output('stdout', ret.stdout) 350 _print_output('stderr', ret.stderr) 351 raise RunError(ret) 352 353 return ret 354 355 356_gitc_manifest_dir = None 357 358 359def get_gitc_manifest_dir(): 360 global _gitc_manifest_dir 361 if _gitc_manifest_dir is None: 362 _gitc_manifest_dir = '' 363 try: 364 with open(GITC_CONFIG_FILE, 'r') as gitc_config: 365 for line in gitc_config: 366 match = re.match('gitc_dir=(?P<gitc_manifest_dir>.*)', line) 367 if match: 368 _gitc_manifest_dir = match.group('gitc_manifest_dir') 369 except IOError: 370 pass 371 return _gitc_manifest_dir 372 373 374def gitc_parse_clientdir(gitc_fs_path): 375 """Parse a path in the GITC FS and return its client name. 376 377 @param gitc_fs_path: A subdirectory path within the GITC_FS_ROOT_DIR. 378 379 @returns: The GITC client name 380 """ 381 if gitc_fs_path == GITC_FS_ROOT_DIR: 382 return None 383 if not gitc_fs_path.startswith(GITC_FS_ROOT_DIR): 384 manifest_dir = get_gitc_manifest_dir() 385 if manifest_dir == '': 386 return None 387 if manifest_dir[-1] != '/': 388 manifest_dir += '/' 389 if gitc_fs_path == manifest_dir: 390 return None 391 if not gitc_fs_path.startswith(manifest_dir): 392 return None 393 return gitc_fs_path.split(manifest_dir)[1].split('/')[0] 394 return gitc_fs_path.split(GITC_FS_ROOT_DIR)[1].split('/')[0] 395 396 397class CloneFailure(Exception): 398 399 """Indicate the remote clone of repo itself failed. 400 """ 401 402 403def check_repo_verify(repo_verify, quiet=False): 404 """Check the --repo-verify state.""" 405 if not repo_verify: 406 print('repo: warning: verification of repo code has been disabled;\n' 407 'repo will not be able to verify the integrity of itself.\n', 408 file=sys.stderr) 409 return False 410 411 if NeedSetupGnuPG(): 412 return SetupGnuPG(quiet) 413 414 return True 415 416 417def check_repo_rev(dst, rev, repo_verify=False, quiet=False): 418 """Check that |rev| is valid.""" 419 do_verify = check_repo_verify(repo_verify, quiet=quiet) 420 remote_ref, local_rev = resolve_repo_rev(dst, rev) 421 if not quiet and not remote_ref.startswith('refs/heads/'): 422 print('warning: repo is not tracking a remote branch, so it will not ' 423 'receive updates', file=sys.stderr) 424 if do_verify: 425 rev = verify_rev(dst, remote_ref, local_rev, quiet) 426 else: 427 rev = local_rev 428 return (remote_ref, rev) 429 430 431def _Init(args, gitc_init=False): 432 """Installs repo by cloning it over the network. 433 """ 434 parser = GetParser(gitc_init=gitc_init) 435 opt, args = parser.parse_args(args) 436 if args: 437 parser.print_usage() 438 sys.exit(1) 439 opt.quiet = opt.output_mode is False 440 opt.verbose = opt.output_mode is True 441 442 if opt.clone_bundle is None: 443 opt.clone_bundle = False if opt.partial_clone else True 444 445 url = opt.repo_url or REPO_URL 446 rev = opt.repo_rev or REPO_REV 447 448 try: 449 if gitc_init: 450 gitc_manifest_dir = get_gitc_manifest_dir() 451 if not gitc_manifest_dir: 452 print('fatal: GITC filesystem is not available. Exiting...', 453 file=sys.stderr) 454 sys.exit(1) 455 gitc_client = opt.gitc_client 456 if not gitc_client: 457 gitc_client = gitc_parse_clientdir(os.getcwd()) 458 if not gitc_client: 459 print('fatal: GITC client (-c) is required.', file=sys.stderr) 460 sys.exit(1) 461 client_dir = os.path.join(gitc_manifest_dir, gitc_client) 462 if not os.path.exists(client_dir): 463 os.makedirs(client_dir) 464 os.chdir(client_dir) 465 if os.path.exists(repodir): 466 # This GITC Client has already initialized repo so continue. 467 return 468 469 os.mkdir(repodir) 470 except OSError as e: 471 if e.errno != errno.EEXIST: 472 print('fatal: cannot make %s directory: %s' 473 % (repodir, e.strerror), file=sys.stderr) 474 # Don't raise CloneFailure; that would delete the 475 # name. Instead exit immediately. 476 # 477 sys.exit(1) 478 479 _CheckGitVersion() 480 try: 481 if not opt.quiet: 482 print('Downloading Repo source from', url) 483 dst = os.path.abspath(os.path.join(repodir, S_repo)) 484 _Clone(url, dst, opt.clone_bundle, opt.quiet, opt.verbose) 485 486 remote_ref, rev = check_repo_rev(dst, rev, False, quiet=opt.quiet) 487 _Checkout(dst, remote_ref, rev, opt.quiet) 488 489 if not os.path.isfile(os.path.join(dst, 'repo')): 490 print("warning: '%s' does not look like a git-repo repository, is " 491 "REPO_URL set correctly?" % url, file=sys.stderr) 492 493 except CloneFailure: 494 if opt.quiet: 495 print('fatal: repo init failed; run without --quiet to see why', 496 file=sys.stderr) 497 raise 498 499 500def run_git(*args, **kwargs): 501 """Run git and return execution details.""" 502 kwargs.setdefault('capture_output', True) 503 kwargs.setdefault('check', True) 504 try: 505 return run_command([GIT] + list(args), **kwargs) 506 except OSError as e: 507 print(file=sys.stderr) 508 print('repo: error: "%s" is not available' % GIT, file=sys.stderr) 509 print('repo: error: %s' % e, file=sys.stderr) 510 print(file=sys.stderr) 511 print('Please make sure %s is installed and in your path.' % GIT, 512 file=sys.stderr) 513 sys.exit(1) 514 except RunError: 515 raise CloneFailure() 516 517def _CheckRequests(): 518 try: 519 import requests 520 except: 521 print('No module named requests') 522 print('confirm installation requests[y/N]? ', end='') 523 sys.stdout.flush() 524 a = sys.stdin.readline().strip().lower() 525 if a in ('yes', 'y', 't', 'true'): 526 _InstallRequests('-i' ,'https://pypi.tuna.tsinghua.edu.cn/simple') 527 sys.exit(1) 528 529 530class InstallRequestsError(Exception): 531 "pip install requests error" 532 533def _InstallRequests(*args, **kwargs): 534 """Run pip install requests and return execution details.""" 535 # kwargs.setdefault('capture_output', True) 536 kwargs.setdefault('check', True) 537 pip, version = ('pip3', '2.24.0') if sys.version_info[0] == 3 else ('pip', '2.18.4') 538 try: 539 return run_command([pip, 'install', 'requests==%s' % version] + list(args), **kwargs) 540 except OSError as e: 541 print(file=sys.stderr) 542 print('repo: error: "%s" is not available' % pip, file=sys.stderr) 543 print('repo: error: %s' % e, file=sys.stderr) 544 print(file=sys.stderr) 545 print('Please make sure %s is installed and in your path.' % pip, 546 file=sys.stderr) 547 sys.exit(1) 548 except RunError: 549 raise InstallRequestsError() 550 551 552# The git version info broken down into components for easy analysis. 553# Similar to Python's sys.version_info. 554GitVersion = collections.namedtuple( 555 'GitVersion', ('major', 'minor', 'micro', 'full')) 556 557 558def ParseGitVersion(ver_str=None): 559 if ver_str is None: 560 # Load the version ourselves. 561 ver_str = run_git('--version').stdout 562 563 if not ver_str.startswith('git version '): 564 return None 565 566 full_version = ver_str[len('git version '):].strip() 567 num_ver_str = full_version.split('-')[0] 568 to_tuple = [] 569 for num_str in num_ver_str.split('.')[:3]: 570 if num_str.isdigit(): 571 to_tuple.append(int(num_str)) 572 else: 573 to_tuple.append(0) 574 to_tuple.append(full_version) 575 return GitVersion(*to_tuple) 576 577 578def _CheckGitVersion(): 579 ver_act = ParseGitVersion() 580 if ver_act is None: 581 print('fatal: unable to detect git version', file=sys.stderr) 582 raise CloneFailure() 583 584 if ver_act < MIN_GIT_VERSION: 585 need = '.'.join(map(str, MIN_GIT_VERSION)) 586 print('fatal: git %s or later required; found %s' % (need, ver_act.full), 587 file=sys.stderr) 588 raise CloneFailure() 589 590 591def SetGitTrace2ParentSid(env=None): 592 """Set up GIT_TRACE2_PARENT_SID for git tracing.""" 593 # We roughly follow the format git itself uses in trace2/tr2_sid.c. 594 # (1) Be unique (2) be valid filename (3) be fixed length. 595 # 596 # Since we always export this variable, we try to avoid more expensive calls. 597 # e.g. We don't attempt hostname lookups or hashing the results. 598 if env is None: 599 env = os.environ 600 601 KEY = 'GIT_TRACE2_PARENT_SID' 602 603 now = datetime.datetime.utcnow() 604 value = 'repo-%s-P%08x' % (now.strftime('%Y%m%dT%H%M%SZ'), os.getpid()) 605 606 # If it's already set, then append ourselves. 607 if KEY in env: 608 value = env[KEY] + '/' + value 609 610 _setenv(KEY, value, env=env) 611 612 613def _setenv(key, value, env=None): 614 """Set |key| in the OS environment |env| to |value|.""" 615 if env is None: 616 env = os.environ 617 # Environment handling across systems is messy. 618 try: 619 env[key] = value 620 except UnicodeEncodeError: 621 env[key] = value.encode() 622 623 624def NeedSetupGnuPG(): 625 if not os.path.isdir(home_dot_repo): 626 return True 627 628 kv = os.path.join(home_dot_repo, 'keyring-version') 629 if not os.path.exists(kv): 630 return True 631 632 kv = open(kv).read() 633 if not kv: 634 return True 635 636 kv = tuple(map(int, kv.split('.'))) 637 if kv < KEYRING_VERSION: 638 return True 639 return False 640 641 642def SetupGnuPG(quiet): 643 try: 644 os.mkdir(home_dot_repo) 645 except OSError as e: 646 if e.errno != errno.EEXIST: 647 print('fatal: cannot make %s directory: %s' 648 % (home_dot_repo, e.strerror), file=sys.stderr) 649 sys.exit(1) 650 651 try: 652 os.mkdir(gpg_dir, stat.S_IRWXU) 653 except OSError as e: 654 if e.errno != errno.EEXIST: 655 print('fatal: cannot make %s directory: %s' % (gpg_dir, e.strerror), 656 file=sys.stderr) 657 sys.exit(1) 658 659 if not quiet: 660 print('repo: Updating release signing keys to keyset ver %s' % 661 ('.'.join(str(x) for x in KEYRING_VERSION),)) 662 # NB: We use --homedir (and cwd below) because some environments (Windows) do 663 # not correctly handle full native paths. We avoid the issue by changing to 664 # the right dir with cwd=gpg_dir before executing gpg, and then telling gpg to 665 # use the cwd (.) as its homedir which leaves the path resolution logic to it. 666 cmd = ['gpg', '--homedir', '.', '--import'] 667 try: 668 # gpg can be pretty chatty. Always capture the output and if something goes 669 # wrong, the builtin check failure will dump stdout & stderr for debugging. 670 run_command(cmd, stdin=subprocess.PIPE, capture_output=True, 671 cwd=gpg_dir, check=True, 672 input=MAINTAINER_KEYS.encode('utf-8')) 673 except OSError: 674 if not quiet: 675 print('warning: gpg (GnuPG) is not available.', file=sys.stderr) 676 print('warning: Installing it is strongly encouraged.', file=sys.stderr) 677 print(file=sys.stderr) 678 return False 679 680 with open(os.path.join(home_dot_repo, 'keyring-version'), 'w') as fd: 681 fd.write('.'.join(map(str, KEYRING_VERSION)) + '\n') 682 return True 683 684 685def _SetConfig(cwd, name, value): 686 """Set a git configuration option to the specified value. 687 """ 688 run_git('config', name, value, cwd=cwd) 689 690 691def _GetRepoConfig(name): 692 """Read a repo configuration option.""" 693 config = os.path.join(home_dot_repo, 'config') 694 if not os.path.exists(config): 695 return None 696 697 cmd = ['config', '--file', config, '--get', name] 698 ret = run_git(*cmd, check=False) 699 if ret.returncode == 0: 700 return ret.stdout 701 elif ret.returncode == 1: 702 return None 703 else: 704 print('repo: error: git %s failed:\n%s' % (' '.join(cmd), ret.stderr), 705 file=sys.stderr) 706 raise RunError() 707 708 709def _InitHttp(): 710 handlers = [] 711 712 mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() 713 try: 714 import netrc 715 n = netrc.netrc() 716 for host in n.hosts: 717 p = n.hosts[host] 718 mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2]) 719 mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2]) 720 except Exception: 721 pass 722 handlers.append(urllib.request.HTTPBasicAuthHandler(mgr)) 723 handlers.append(urllib.request.HTTPDigestAuthHandler(mgr)) 724 725 if 'http_proxy' in os.environ: 726 url = os.environ['http_proxy'] 727 handlers.append(urllib.request.ProxyHandler({'http': url, 'https': url})) 728 if 'REPO_CURL_VERBOSE' in os.environ: 729 handlers.append(urllib.request.HTTPHandler(debuglevel=1)) 730 handlers.append(urllib.request.HTTPSHandler(debuglevel=1)) 731 urllib.request.install_opener(urllib.request.build_opener(*handlers)) 732 733 734def _Fetch(url, cwd, src, quiet, verbose): 735 cmd = ['fetch'] 736 if not verbose: 737 cmd.append('--quiet') 738 err = None 739 if not quiet and sys.stdout.isatty(): 740 cmd.append('--progress') 741 elif not verbose: 742 err = subprocess.PIPE 743 cmd.append(src) 744 cmd.append('+refs/heads/*:refs/remotes/origin/*') 745 cmd.append('+refs/tags/*:refs/tags/*') 746 run_git(*cmd, stderr=err, capture_output=False, cwd=cwd) 747 748 749def _DownloadBundle(url, cwd, quiet, verbose): 750 if not url.endswith('/'): 751 url += '/' 752 url += 'clone.bundle' 753 754 ret = run_git('config', '--get-regexp', 'url.*.insteadof', cwd=cwd, 755 check=False) 756 for line in ret.stdout.splitlines(): 757 m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line) 758 if m: 759 new_url = m.group(1) 760 old_url = m.group(2) 761 if url.startswith(old_url): 762 url = new_url + url[len(old_url):] 763 break 764 765 if not url.startswith('http:') and not url.startswith('https:'): 766 return False 767 768 dest = open(os.path.join(cwd, '.git', 'clone.bundle'), 'w+b') 769 try: 770 try: 771 r = urllib.request.urlopen(url) 772 except urllib.error.HTTPError as e: 773 if e.code in [401, 403, 404, 501]: 774 return False 775 print('fatal: Cannot get %s' % url, file=sys.stderr) 776 print('fatal: HTTP error %s' % e.code, file=sys.stderr) 777 raise CloneFailure() 778 except urllib.error.URLError as e: 779 print('fatal: Cannot get %s' % url, file=sys.stderr) 780 print('fatal: error %s' % e.reason, file=sys.stderr) 781 raise CloneFailure() 782 try: 783 if verbose: 784 print('Downloading clone bundle %s' % url, file=sys.stderr) 785 while True: 786 buf = r.read(8192) 787 if not buf: 788 return True 789 dest.write(buf) 790 finally: 791 r.close() 792 finally: 793 dest.close() 794 795 796def _ImportBundle(cwd): 797 path = os.path.join(cwd, '.git', 'clone.bundle') 798 try: 799 _Fetch(cwd, cwd, path, True, False) 800 finally: 801 os.remove(path) 802 803 804def _Clone(url, cwd, clone_bundle, quiet, verbose): 805 """Clones a git repository to a new subdirectory of repodir 806 """ 807 if verbose: 808 print('Cloning git repository', url) 809 810 try: 811 os.mkdir(cwd) 812 except OSError as e: 813 print('fatal: cannot make %s directory: %s' % (cwd, e.strerror), 814 file=sys.stderr) 815 raise CloneFailure() 816 817 run_git('init', '--quiet', cwd=cwd) 818 819 _InitHttp() 820 _SetConfig(cwd, 'remote.origin.url', url) 821 _SetConfig(cwd, 822 'remote.origin.fetch', 823 '+refs/heads/*:refs/remotes/origin/*') 824 if clone_bundle and _DownloadBundle(url, cwd, quiet, verbose): 825 _ImportBundle(cwd) 826 _Fetch(url, cwd, 'origin', quiet, verbose) 827 828 829def resolve_repo_rev(cwd, committish): 830 """Figure out what REPO_REV represents. 831 832 We support: 833 * refs/heads/xxx: Branch. 834 * refs/tags/xxx: Tag. 835 * xxx: Branch or tag or commit. 836 837 Args: 838 cwd: The git checkout to run in. 839 committish: The REPO_REV argument to resolve. 840 841 Returns: 842 A tuple of (remote ref, commit) as makes sense for the committish. 843 For branches, this will look like ('refs/heads/stable', <revision>). 844 For tags, this will look like ('refs/tags/v1.0', <revision>). 845 For commits, this will be (<revision>, <revision>). 846 """ 847 def resolve(committish): 848 ret = run_git('rev-parse', '--verify', '%s^{commit}' % (committish,), 849 cwd=cwd, check=False) 850 return None if ret.returncode else ret.stdout.strip() 851 852 # An explicit branch. 853 if committish.startswith('refs/heads/'): 854 remote_ref = committish 855 committish = committish[len('refs/heads/'):] 856 rev = resolve('refs/remotes/origin/%s' % committish) 857 if rev is None: 858 print('repo: error: unknown branch "%s"' % (committish,), 859 file=sys.stderr) 860 raise CloneFailure() 861 return (remote_ref, rev) 862 863 # An explicit tag. 864 if committish.startswith('refs/tags/'): 865 remote_ref = committish 866 committish = committish[len('refs/tags/'):] 867 rev = resolve(remote_ref) 868 if rev is None: 869 print('repo: error: unknown tag "%s"' % (committish,), 870 file=sys.stderr) 871 raise CloneFailure() 872 return (remote_ref, rev) 873 874 # See if it's a short branch name. 875 rev = resolve('refs/remotes/origin/%s' % committish) 876 if rev: 877 return ('refs/heads/%s' % (committish,), rev) 878 879 # See if it's a tag. 880 rev = resolve('refs/tags/%s' % committish) 881 if rev: 882 return ('refs/tags/%s' % (committish,), rev) 883 884 # See if it's a commit. 885 rev = resolve(committish) 886 if rev and rev.lower().startswith(committish.lower()): 887 return (rev, rev) 888 889 # Give up! 890 print('repo: error: unable to resolve "%s"' % (committish,), file=sys.stderr) 891 raise CloneFailure() 892 893 894def verify_rev(cwd, remote_ref, rev, quiet): 895 """Verify the commit has been signed by a tag.""" 896 ret = run_git('describe', rev, cwd=cwd) 897 cur = ret.stdout.strip() 898 899 m = re.compile(r'^(.*)-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur) 900 if m: 901 cur = m.group(1) 902 if not quiet: 903 print(file=sys.stderr) 904 print("warning: '%s' is not signed; falling back to signed release '%s'" 905 % (remote_ref, cur), file=sys.stderr) 906 print(file=sys.stderr) 907 908 env = os.environ.copy() 909 _setenv('GNUPGHOME', gpg_dir, env) 910 run_git('tag', '-v', cur, cwd=cwd, env=env) 911 return '%s^0' % cur 912 913 914def _Checkout(cwd, remote_ref, rev, quiet): 915 """Checkout an upstream branch into the repository and track it. 916 """ 917 run_git('update-ref', 'refs/heads/default', rev, cwd=cwd) 918 919 _SetConfig(cwd, 'branch.default.remote', 'origin') 920 _SetConfig(cwd, 'branch.default.merge', remote_ref) 921 922 run_git('symbolic-ref', 'HEAD', 'refs/heads/default', cwd=cwd) 923 924 cmd = ['read-tree', '--reset', '-u'] 925 if not quiet: 926 cmd.append('-v') 927 cmd.append('HEAD') 928 run_git(*cmd, cwd=cwd) 929 930 931def _FindRepo(): 932 """Look for a repo installation, starting at the current directory. 933 """ 934 curdir = os.getcwd() 935 repo = None 936 937 olddir = None 938 while curdir != '/' \ 939 and curdir != olddir \ 940 and not repo: 941 repo = os.path.join(curdir, repodir, REPO_MAIN) 942 if not os.path.isfile(repo): 943 repo = None 944 olddir = curdir 945 curdir = os.path.dirname(curdir) 946 return (repo, os.path.join(curdir, repodir)) 947 948 949class _Options(object): 950 help = False 951 version = False 952 953 954def _ExpandAlias(name): 955 """Look up user registered aliases.""" 956 # We don't resolve aliases for existing subcommands. This matches git. 957 if name in {'gitc-init', 'help', 'init'}: 958 return name, [] 959 960 alias = _GetRepoConfig('alias.%s' % (name,)) 961 if alias is None: 962 return name, [] 963 964 args = alias.strip().split(' ', 1) 965 name = args[0] 966 if len(args) == 2: 967 args = shlex.split(args[1]) 968 else: 969 args = [] 970 return name, args 971 972 973def _ParseArguments(args): 974 cmd = None 975 opt = _Options() 976 arg = [] 977 978 for i in range(len(args)): 979 a = args[i] 980 if a == '-h' or a == '--help': 981 opt.help = True 982 elif a == '--version': 983 opt.version = True 984 elif a == '--trace': 985 trace.set(True) 986 elif not a.startswith('-'): 987 cmd = a 988 arg = args[i + 1:] 989 break 990 return cmd, opt, arg 991 992 993def _Usage(): 994 gitc_usage = "" 995 if get_gitc_manifest_dir(): 996 gitc_usage = " gitc-init Initialize a GITC Client.\n" 997 998 print( 999 """usage: repo COMMAND [ARGS] 1000 1001repo is not yet installed. Use "repo init" to install it here. 1002 1003The most commonly used repo commands are: 1004 1005 init Install repo in the current working directory 1006""" + gitc_usage + 1007 """ help Display detailed help on a command 1008 1009For access to the full online help, install repo ("repo init"). 1010""") 1011 sys.exit(0) 1012 1013 1014def _Help(args): 1015 if args: 1016 if args[0] in {'init', 'gitc-init'}: 1017 parser = GetParser(gitc_init=args[0] == 'gitc-init') 1018 parser.print_help() 1019 sys.exit(0) 1020 else: 1021 print("error: '%s' is not a bootstrap command.\n" 1022 ' For access to online help, install repo ("repo init").' 1023 % args[0], file=sys.stderr) 1024 else: 1025 _Usage() 1026 sys.exit(1) 1027 1028 1029def _Version(): 1030 """Show version information.""" 1031 print('<repo not installed>') 1032 print('repo launcher version %s' % ('.'.join(str(x) for x in VERSION),)) 1033 print(' (from %s)' % (__file__,)) 1034 print('git %s' % (ParseGitVersion().full,)) 1035 print('Python %s' % sys.version) 1036 uname = platform.uname() 1037 if sys.version_info.major < 3: 1038 # Python 3 returns a named tuple, but Python 2 is simpler. 1039 print(uname) 1040 else: 1041 print('OS %s %s (%s)' % (uname.system, uname.release, uname.version)) 1042 print('CPU %s (%s)' % 1043 (uname.machine, uname.processor if uname.processor else 'unknown')) 1044 sys.exit(0) 1045 1046 1047def _NotInstalled(): 1048 print('error: repo is not installed. Use "repo init" to install it here.', 1049 file=sys.stderr) 1050 sys.exit(1) 1051 1052 1053def _NoCommands(cmd): 1054 print("""error: command '%s' requires repo to be installed first. 1055 Use "repo init" to install it here.""" % cmd, file=sys.stderr) 1056 sys.exit(1) 1057 1058 1059def _RunSelf(wrapper_path): 1060 my_dir = os.path.dirname(wrapper_path) 1061 my_main = os.path.join(my_dir, 'main.py') 1062 my_git = os.path.join(my_dir, '.git') 1063 1064 if os.path.isfile(my_main) and os.path.isdir(my_git): 1065 for name in ['git_config.py', 1066 'project.py', 1067 'subcmds']: 1068 if not os.path.exists(os.path.join(my_dir, name)): 1069 return None, None 1070 return my_main, my_git 1071 return None, None 1072 1073 1074def _SetDefaultsTo(gitdir): 1075 global REPO_URL 1076 global REPO_REV 1077 1078 REPO_URL = gitdir 1079 ret = run_git('--git-dir=%s' % gitdir, 'symbolic-ref', 'HEAD', check=False) 1080 if ret.returncode: 1081 # If we're not tracking a branch (bisect/etc...), then fall back to commit. 1082 print('repo: warning: %s has no current branch; using HEAD' % gitdir, 1083 file=sys.stderr) 1084 try: 1085 ret = run_git('rev-parse', 'HEAD', cwd=gitdir) 1086 except CloneFailure: 1087 print('fatal: %s has invalid HEAD' % gitdir, file=sys.stderr) 1088 sys.exit(1) 1089 1090 REPO_REV = ret.stdout.strip() 1091 1092 1093def main(orig_args): 1094 _CheckRequests() 1095 cmd, opt, args = _ParseArguments(orig_args) 1096 # We run this early as we run some git commands ourselves. 1097 SetGitTrace2ParentSid() 1098 1099 repo_main, rel_repo_dir = None, None 1100 # Don't use the local repo copy, make sure to switch to the gitc client first. 1101 if cmd != 'gitc-init': 1102 repo_main, rel_repo_dir = _FindRepo() 1103 1104 wrapper_path = os.path.abspath(__file__) 1105 my_main, my_git = _RunSelf(wrapper_path) 1106 1107 cwd = os.getcwd() 1108 if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()): 1109 print('error: repo cannot be used in the GITC local manifest directory.' 1110 '\nIf you want to work on this GITC client please rerun this ' 1111 'command from the corresponding client under /gitc/', 1112 file=sys.stderr) 1113 sys.exit(1) 1114 if not repo_main: 1115 # Only expand aliases here since we'll be parsing the CLI ourselves. 1116 # If we had repo_main, alias expansion would happen in main.py. 1117 cmd, alias_args = _ExpandAlias(cmd) 1118 args = alias_args + args 1119 1120 if opt.help: 1121 _Usage() 1122 if cmd == 'help': 1123 _Help(args) 1124 if opt.version or cmd == 'version': 1125 _Version() 1126 if not cmd: 1127 _NotInstalled() 1128 if cmd == 'init' or cmd == 'gitc-init': 1129 if my_git: 1130 _SetDefaultsTo(my_git) 1131 try: 1132 _Init(args, gitc_init=(cmd == 'gitc-init')) 1133 except CloneFailure: 1134 path = os.path.join(repodir, S_repo) 1135 print("fatal: cloning the git-repo repository failed, will remove " 1136 "'%s' " % path, file=sys.stderr) 1137 shutil.rmtree(path, ignore_errors=True) 1138 sys.exit(1) 1139 repo_main, rel_repo_dir = _FindRepo() 1140 else: 1141 _NoCommands(cmd) 1142 1143 if my_main: 1144 repo_main = my_main 1145 1146 if not repo_main: 1147 print("fatal: unable to find repo entry point", file=sys.stderr) 1148 sys.exit(1) 1149 1150 ver_str = '.'.join(map(str, VERSION)) 1151 me = [sys.executable, repo_main, 1152 '--repo-dir=%s' % rel_repo_dir, 1153 '--wrapper-version=%s' % ver_str, 1154 '--wrapper-path=%s' % wrapper_path, 1155 '--'] 1156 me.extend(orig_args) 1157 exec_command(me) 1158 print("fatal: unable to start %s" % repo_main, file=sys.stderr) 1159 sys.exit(148) 1160 1161 1162if __name__ == '__main__': 1163 main(sys.argv[1:]) 1164