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