diff options
Diffstat (limited to 'git_config.py')
| -rw-r--r-- | git_config.py | 426 |
1 files changed, 227 insertions, 199 deletions
diff --git a/git_config.py b/git_config.py index 8de3200c..3cd09391 100644 --- a/git_config.py +++ b/git_config.py | |||
| @@ -1,5 +1,3 @@ | |||
| 1 | # -*- coding:utf-8 -*- | ||
| 2 | # | ||
| 3 | # Copyright (C) 2008 The Android Open Source Project | 1 | # Copyright (C) 2008 The Android Open Source Project |
| 4 | # | 2 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| @@ -14,84 +12,83 @@ | |||
| 14 | # See the License for the specific language governing permissions and | 12 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. | 13 | # limitations under the License. |
| 16 | 14 | ||
| 17 | from __future__ import print_function | ||
| 18 | |||
| 19 | import contextlib | 15 | import contextlib |
| 16 | import datetime | ||
| 20 | import errno | 17 | import errno |
| 18 | from http.client import HTTPException | ||
| 21 | import json | 19 | import json |
| 22 | import os | 20 | import os |
| 23 | import re | 21 | import re |
| 24 | import ssl | 22 | import ssl |
| 25 | import subprocess | 23 | import subprocess |
| 26 | import sys | 24 | import sys |
| 27 | try: | 25 | import urllib.error |
| 28 | import threading as _threading | 26 | import urllib.request |
| 29 | except ImportError: | 27 | |
| 30 | import dummy_threading as _threading | ||
| 31 | import time | ||
| 32 | |||
| 33 | from pyversion import is_python3 | ||
| 34 | if is_python3(): | ||
| 35 | import urllib.request | ||
| 36 | import urllib.error | ||
| 37 | else: | ||
| 38 | import urllib2 | ||
| 39 | import imp | ||
| 40 | urllib = imp.new_module('urllib') | ||
| 41 | urllib.request = urllib2 | ||
| 42 | urllib.error = urllib2 | ||
| 43 | |||
| 44 | from signal import SIGTERM | ||
| 45 | from error import GitError, UploadError | 28 | from error import GitError, UploadError |
| 46 | import platform_utils | 29 | import platform_utils |
| 47 | from repo_trace import Trace | 30 | from repo_trace import Trace |
| 48 | if is_python3(): | ||
| 49 | from http.client import HTTPException | ||
| 50 | else: | ||
| 51 | from httplib import HTTPException | ||
| 52 | |||
| 53 | from git_command import GitCommand | 31 | from git_command import GitCommand |
| 54 | from git_command import ssh_sock | ||
| 55 | from git_command import terminate_ssh_clients | ||
| 56 | from git_refs import R_CHANGES, R_HEADS, R_TAGS | 32 | from git_refs import R_CHANGES, R_HEADS, R_TAGS |
| 57 | 33 | ||
| 34 | # Prefix that is prepended to all the keys of SyncAnalysisState's data | ||
| 35 | # that is saved in the config. | ||
| 36 | SYNC_STATE_PREFIX = 'repo.syncstate.' | ||
| 37 | |||
| 58 | ID_RE = re.compile(r'^[0-9a-f]{40}$') | 38 | ID_RE = re.compile(r'^[0-9a-f]{40}$') |
| 59 | 39 | ||
| 60 | REVIEW_CACHE = dict() | 40 | REVIEW_CACHE = dict() |
| 61 | 41 | ||
| 42 | |||
| 62 | def IsChange(rev): | 43 | def IsChange(rev): |
| 63 | return rev.startswith(R_CHANGES) | 44 | return rev.startswith(R_CHANGES) |
| 64 | 45 | ||
| 46 | |||
| 65 | def IsId(rev): | 47 | def IsId(rev): |
| 66 | return ID_RE.match(rev) | 48 | return ID_RE.match(rev) |
| 67 | 49 | ||
| 50 | |||
| 68 | def IsTag(rev): | 51 | def IsTag(rev): |
| 69 | return rev.startswith(R_TAGS) | 52 | return rev.startswith(R_TAGS) |
| 70 | 53 | ||
| 54 | |||
| 71 | def IsImmutable(rev): | 55 | def IsImmutable(rev): |
| 72 | return IsChange(rev) or IsId(rev) or IsTag(rev) | 56 | return IsChange(rev) or IsId(rev) or IsTag(rev) |
| 73 | 57 | ||
| 58 | |||
| 74 | def _key(name): | 59 | def _key(name): |
| 75 | parts = name.split('.') | 60 | parts = name.split('.') |
| 76 | if len(parts) < 2: | 61 | if len(parts) < 2: |
| 77 | return name.lower() | 62 | return name.lower() |
| 78 | parts[ 0] = parts[ 0].lower() | 63 | parts[0] = parts[0].lower() |
| 79 | parts[-1] = parts[-1].lower() | 64 | parts[-1] = parts[-1].lower() |
| 80 | return '.'.join(parts) | 65 | return '.'.join(parts) |
| 81 | 66 | ||
| 67 | |||
| 82 | class GitConfig(object): | 68 | class GitConfig(object): |
| 83 | _ForUser = None | 69 | _ForUser = None |
| 84 | 70 | ||
| 71 | _USER_CONFIG = '~/.gitconfig' | ||
| 72 | |||
| 73 | _ForSystem = None | ||
| 74 | _SYSTEM_CONFIG = '/etc/gitconfig' | ||
| 75 | |||
| 76 | @classmethod | ||
| 77 | def ForSystem(cls): | ||
| 78 | if cls._ForSystem is None: | ||
| 79 | cls._ForSystem = cls(configfile=cls._SYSTEM_CONFIG) | ||
| 80 | return cls._ForSystem | ||
| 81 | |||
| 85 | @classmethod | 82 | @classmethod |
| 86 | def ForUser(cls): | 83 | def ForUser(cls): |
| 87 | if cls._ForUser is None: | 84 | if cls._ForUser is None: |
| 88 | cls._ForUser = cls(configfile = os.path.expanduser('~/.gitconfig')) | 85 | cls._ForUser = cls(configfile=os.path.expanduser(cls._USER_CONFIG)) |
| 89 | return cls._ForUser | 86 | return cls._ForUser |
| 90 | 87 | ||
| 91 | @classmethod | 88 | @classmethod |
| 92 | def ForRepository(cls, gitdir, defaults=None): | 89 | def ForRepository(cls, gitdir, defaults=None): |
| 93 | return cls(configfile = os.path.join(gitdir, 'config'), | 90 | return cls(configfile=os.path.join(gitdir, 'config'), |
| 94 | defaults = defaults) | 91 | defaults=defaults) |
| 95 | 92 | ||
| 96 | def __init__(self, configfile, defaults=None, jsonFile=None): | 93 | def __init__(self, configfile, defaults=None, jsonFile=None): |
| 97 | self.file = configfile | 94 | self.file = configfile |
| @@ -104,18 +101,74 @@ class GitConfig(object): | |||
| 104 | self._json = jsonFile | 101 | self._json = jsonFile |
| 105 | if self._json is None: | 102 | if self._json is None: |
| 106 | self._json = os.path.join( | 103 | self._json = os.path.join( |
| 107 | os.path.dirname(self.file), | 104 | os.path.dirname(self.file), |
| 108 | '.repo_' + os.path.basename(self.file) + '.json') | 105 | '.repo_' + os.path.basename(self.file) + '.json') |
| 106 | |||
| 107 | def ClearCache(self): | ||
| 108 | """Clear the in-memory cache of config.""" | ||
| 109 | self._cache_dict = None | ||
| 109 | 110 | ||
| 110 | def Has(self, name, include_defaults = True): | 111 | def Has(self, name, include_defaults=True): |
| 111 | """Return true if this configuration file has the key. | 112 | """Return true if this configuration file has the key. |
| 112 | """ | 113 | """ |
| 113 | if _key(name) in self._cache: | 114 | if _key(name) in self._cache: |
| 114 | return True | 115 | return True |
| 115 | if include_defaults and self.defaults: | 116 | if include_defaults and self.defaults: |
| 116 | return self.defaults.Has(name, include_defaults = True) | 117 | return self.defaults.Has(name, include_defaults=True) |
| 117 | return False | 118 | return False |
| 118 | 119 | ||
| 120 | def GetInt(self, name): | ||
| 121 | """Returns an integer from the configuration file. | ||
| 122 | |||
| 123 | This follows the git config syntax. | ||
| 124 | |||
| 125 | Args: | ||
| 126 | name: The key to lookup. | ||
| 127 | |||
| 128 | Returns: | ||
| 129 | None if the value was not defined, or is not a boolean. | ||
| 130 | Otherwise, the number itself. | ||
| 131 | """ | ||
| 132 | v = self.GetString(name) | ||
| 133 | if v is None: | ||
| 134 | return None | ||
| 135 | v = v.strip() | ||
| 136 | |||
| 137 | mult = 1 | ||
| 138 | if v.endswith('k'): | ||
| 139 | v = v[:-1] | ||
| 140 | mult = 1024 | ||
| 141 | elif v.endswith('m'): | ||
| 142 | v = v[:-1] | ||
| 143 | mult = 1024 * 1024 | ||
| 144 | elif v.endswith('g'): | ||
| 145 | v = v[:-1] | ||
| 146 | mult = 1024 * 1024 * 1024 | ||
| 147 | |||
| 148 | base = 10 | ||
| 149 | if v.startswith('0x'): | ||
| 150 | base = 16 | ||
| 151 | |||
| 152 | try: | ||
| 153 | return int(v, base=base) * mult | ||
| 154 | except ValueError: | ||
| 155 | return None | ||
| 156 | |||
| 157 | def DumpConfigDict(self): | ||
| 158 | """Returns the current configuration dict. | ||
| 159 | |||
| 160 | Configuration data is information only (e.g. logging) and | ||
| 161 | should not be considered a stable data-source. | ||
| 162 | |||
| 163 | Returns: | ||
| 164 | dict of {<key>, <value>} for git configuration cache. | ||
| 165 | <value> are strings converted by GetString. | ||
| 166 | """ | ||
| 167 | config_dict = {} | ||
| 168 | for key in self._cache: | ||
| 169 | config_dict[key] = self.GetString(key) | ||
| 170 | return config_dict | ||
| 171 | |||
| 119 | def GetBoolean(self, name): | 172 | def GetBoolean(self, name): |
| 120 | """Returns a boolean from the configuration file. | 173 | """Returns a boolean from the configuration file. |
| 121 | None : The value was not defined, or is not a boolean. | 174 | None : The value was not defined, or is not a boolean. |
| @@ -132,6 +185,12 @@ class GitConfig(object): | |||
| 132 | return False | 185 | return False |
| 133 | return None | 186 | return None |
| 134 | 187 | ||
| 188 | def SetBoolean(self, name, value): | ||
| 189 | """Set the truthy value for a key.""" | ||
| 190 | if value is not None: | ||
| 191 | value = 'true' if value else 'false' | ||
| 192 | self.SetString(name, value) | ||
| 193 | |||
| 135 | def GetString(self, name, all_keys=False): | 194 | def GetString(self, name, all_keys=False): |
| 136 | """Get the first value for a key, or None if it is not defined. | 195 | """Get the first value for a key, or None if it is not defined. |
| 137 | 196 | ||
| @@ -142,7 +201,7 @@ class GitConfig(object): | |||
| 142 | v = self._cache[_key(name)] | 201 | v = self._cache[_key(name)] |
| 143 | except KeyError: | 202 | except KeyError: |
| 144 | if self.defaults: | 203 | if self.defaults: |
| 145 | return self.defaults.GetString(name, all_keys = all_keys) | 204 | return self.defaults.GetString(name, all_keys=all_keys) |
| 146 | v = [] | 205 | v = [] |
| 147 | 206 | ||
| 148 | if not all_keys: | 207 | if not all_keys: |
| @@ -153,7 +212,7 @@ class GitConfig(object): | |||
| 153 | r = [] | 212 | r = [] |
| 154 | r.extend(v) | 213 | r.extend(v) |
| 155 | if self.defaults: | 214 | if self.defaults: |
| 156 | r.extend(self.defaults.GetString(name, all_keys = True)) | 215 | r.extend(self.defaults.GetString(name, all_keys=True)) |
| 157 | return r | 216 | return r |
| 158 | 217 | ||
| 159 | def SetString(self, name, value): | 218 | def SetString(self, name, value): |
| @@ -212,12 +271,28 @@ class GitConfig(object): | |||
| 212 | self._branches[b.name] = b | 271 | self._branches[b.name] = b |
| 213 | return b | 272 | return b |
| 214 | 273 | ||
| 274 | def GetSyncAnalysisStateData(self): | ||
| 275 | """Returns data to be logged for the analysis of sync performance.""" | ||
| 276 | return {k: v for k, v in self.DumpConfigDict().items() if k.startswith(SYNC_STATE_PREFIX)} | ||
| 277 | |||
| 278 | def UpdateSyncAnalysisState(self, options, superproject_logging_data): | ||
| 279 | """Update Config's SYNC_STATE_PREFIX* data with the latest sync data. | ||
| 280 | |||
| 281 | Args: | ||
| 282 | options: Options passed to sync returned from optparse. See _Options(). | ||
| 283 | superproject_logging_data: A dictionary of superproject data that is to be logged. | ||
| 284 | |||
| 285 | Returns: | ||
| 286 | SyncAnalysisState object. | ||
| 287 | """ | ||
| 288 | return SyncAnalysisState(self, options, superproject_logging_data) | ||
| 289 | |||
| 215 | def GetSubSections(self, section): | 290 | def GetSubSections(self, section): |
| 216 | """List all subsection names matching $section.*.* | 291 | """List all subsection names matching $section.*.* |
| 217 | """ | 292 | """ |
| 218 | return self._sections.get(section, set()) | 293 | return self._sections.get(section, set()) |
| 219 | 294 | ||
| 220 | def HasSection(self, section, subsection = ''): | 295 | def HasSection(self, section, subsection=''): |
| 221 | """Does at least one key in section.subsection exist? | 296 | """Does at least one key in section.subsection exist? |
| 222 | """ | 297 | """ |
| 223 | try: | 298 | try: |
| @@ -268,8 +343,7 @@ class GitConfig(object): | |||
| 268 | 343 | ||
| 269 | def _ReadJson(self): | 344 | def _ReadJson(self): |
| 270 | try: | 345 | try: |
| 271 | if os.path.getmtime(self._json) \ | 346 | if os.path.getmtime(self._json) <= os.path.getmtime(self.file): |
| 272 | <= os.path.getmtime(self.file): | ||
| 273 | platform_utils.remove(self._json) | 347 | platform_utils.remove(self._json) |
| 274 | return None | 348 | return None |
| 275 | except OSError: | 349 | except OSError: |
| @@ -278,8 +352,8 @@ class GitConfig(object): | |||
| 278 | Trace(': parsing %s', self.file) | 352 | Trace(': parsing %s', self.file) |
| 279 | with open(self._json) as fd: | 353 | with open(self._json) as fd: |
| 280 | return json.load(fd) | 354 | return json.load(fd) |
| 281 | except (IOError, ValueError): | 355 | except (IOError, ValueErrorl): |
| 282 | platform_utils.remove(self._json) | 356 | platform_utils.remove(self._json, missing_ok=True) |
| 283 | return None | 357 | return None |
| 284 | 358 | ||
| 285 | def _SaveJson(self, cache): | 359 | def _SaveJson(self, cache): |
| @@ -287,8 +361,7 @@ class GitConfig(object): | |||
| 287 | with open(self._json, 'w') as fd: | 361 | with open(self._json, 'w') as fd: |
| 288 | json.dump(cache, fd, indent=2) | 362 | json.dump(cache, fd, indent=2) |
| 289 | except (IOError, TypeError): | 363 | except (IOError, TypeError): |
| 290 | if os.path.exists(self._json): | 364 | platform_utils.remove(self._json, missing_ok=True) |
| 291 | platform_utils.remove(self._json) | ||
| 292 | 365 | ||
| 293 | def _ReadGit(self): | 366 | def _ReadGit(self): |
| 294 | """ | 367 | """ |
| @@ -298,11 +371,10 @@ class GitConfig(object): | |||
| 298 | 371 | ||
| 299 | """ | 372 | """ |
| 300 | c = {} | 373 | c = {} |
| 301 | d = self._do('--null', '--list') | 374 | if not os.path.exists(self.file): |
| 302 | if d is None: | ||
| 303 | return c | 375 | return c |
| 304 | if not is_python3(): | 376 | |
| 305 | d = d.decode('utf-8') | 377 | d = self._do('--null', '--list') |
| 306 | for line in d.rstrip('\0').split('\0'): | 378 | for line in d.rstrip('\0').split('\0'): |
| 307 | if '\n' in line: | 379 | if '\n' in line: |
| 308 | key, val = line.split('\n', 1) | 380 | key, val = line.split('\n', 1) |
| @@ -318,17 +390,26 @@ class GitConfig(object): | |||
| 318 | return c | 390 | return c |
| 319 | 391 | ||
| 320 | def _do(self, *args): | 392 | def _do(self, *args): |
| 321 | command = ['config', '--file', self.file] | 393 | if self.file == self._SYSTEM_CONFIG: |
| 394 | command = ['config', '--system', '--includes'] | ||
| 395 | else: | ||
| 396 | command = ['config', '--file', self.file, '--includes'] | ||
| 322 | command.extend(args) | 397 | command.extend(args) |
| 323 | 398 | ||
| 324 | p = GitCommand(None, | 399 | p = GitCommand(None, |
| 325 | command, | 400 | command, |
| 326 | capture_stdout = True, | 401 | capture_stdout=True, |
| 327 | capture_stderr = True) | 402 | capture_stderr=True) |
| 328 | if p.Wait() == 0: | 403 | if p.Wait() == 0: |
| 329 | return p.stdout | 404 | return p.stdout |
| 330 | else: | 405 | else: |
| 331 | GitError('git config %s: %s' % (str(args), p.stderr)) | 406 | raise GitError('git config %s: %s' % (str(args), p.stderr)) |
| 407 | |||
| 408 | |||
| 409 | class RepoConfig(GitConfig): | ||
| 410 | """User settings for repo itself.""" | ||
| 411 | |||
| 412 | _USER_CONFIG = '~/.repoconfig/config' | ||
| 332 | 413 | ||
| 333 | 414 | ||
| 334 | class RefSpec(object): | 415 | class RefSpec(object): |
| @@ -387,133 +468,16 @@ class RefSpec(object): | |||
| 387 | return s | 468 | return s |
| 388 | 469 | ||
| 389 | 470 | ||
| 390 | _master_processes = [] | ||
| 391 | _master_keys = set() | ||
| 392 | _ssh_master = True | ||
| 393 | _master_keys_lock = None | ||
| 394 | |||
| 395 | def init_ssh(): | ||
| 396 | """Should be called once at the start of repo to init ssh master handling. | ||
| 397 | |||
| 398 | At the moment, all we do is to create our lock. | ||
| 399 | """ | ||
| 400 | global _master_keys_lock | ||
| 401 | assert _master_keys_lock is None, "Should only call init_ssh once" | ||
| 402 | _master_keys_lock = _threading.Lock() | ||
| 403 | |||
| 404 | def _open_ssh(host, port=None): | ||
| 405 | global _ssh_master | ||
| 406 | |||
| 407 | # Acquire the lock. This is needed to prevent opening multiple masters for | ||
| 408 | # the same host when we're running "repo sync -jN" (for N > 1) _and_ the | ||
| 409 | # manifest <remote fetch="ssh://xyz"> specifies a different host from the | ||
| 410 | # one that was passed to repo init. | ||
| 411 | _master_keys_lock.acquire() | ||
| 412 | try: | ||
| 413 | |||
| 414 | # Check to see whether we already think that the master is running; if we | ||
| 415 | # think it's already running, return right away. | ||
| 416 | if port is not None: | ||
| 417 | key = '%s:%s' % (host, port) | ||
| 418 | else: | ||
| 419 | key = host | ||
| 420 | |||
| 421 | if key in _master_keys: | ||
| 422 | return True | ||
| 423 | |||
| 424 | if not _ssh_master \ | ||
| 425 | or 'GIT_SSH' in os.environ \ | ||
| 426 | or sys.platform in ('win32', 'cygwin'): | ||
| 427 | # failed earlier, or cygwin ssh can't do this | ||
| 428 | # | ||
| 429 | return False | ||
| 430 | |||
| 431 | # We will make two calls to ssh; this is the common part of both calls. | ||
| 432 | command_base = ['ssh', | ||
| 433 | '-o','ControlPath %s' % ssh_sock(), | ||
| 434 | host] | ||
| 435 | if port is not None: | ||
| 436 | command_base[1:1] = ['-p', str(port)] | ||
| 437 | |||
| 438 | # Since the key wasn't in _master_keys, we think that master isn't running. | ||
| 439 | # ...but before actually starting a master, we'll double-check. This can | ||
| 440 | # be important because we can't tell that that 'git@myhost.com' is the same | ||
| 441 | # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file. | ||
| 442 | check_command = command_base + ['-O','check'] | ||
| 443 | try: | ||
| 444 | Trace(': %s', ' '.join(check_command)) | ||
| 445 | check_process = subprocess.Popen(check_command, | ||
| 446 | stdout=subprocess.PIPE, | ||
| 447 | stderr=subprocess.PIPE) | ||
| 448 | check_process.communicate() # read output, but ignore it... | ||
| 449 | isnt_running = check_process.wait() | ||
| 450 | |||
| 451 | if not isnt_running: | ||
| 452 | # Our double-check found that the master _was_ infact running. Add to | ||
| 453 | # the list of keys. | ||
| 454 | _master_keys.add(key) | ||
| 455 | return True | ||
| 456 | except Exception: | ||
| 457 | # Ignore excpetions. We we will fall back to the normal command and print | ||
| 458 | # to the log there. | ||
| 459 | pass | ||
| 460 | |||
| 461 | command = command_base[:1] + \ | ||
| 462 | ['-M', '-N'] + \ | ||
| 463 | command_base[1:] | ||
| 464 | try: | ||
| 465 | Trace(': %s', ' '.join(command)) | ||
| 466 | p = subprocess.Popen(command) | ||
| 467 | except Exception as e: | ||
| 468 | _ssh_master = False | ||
| 469 | print('\nwarn: cannot enable ssh control master for %s:%s\n%s' | ||
| 470 | % (host,port, str(e)), file=sys.stderr) | ||
| 471 | return False | ||
| 472 | |||
| 473 | time.sleep(1) | ||
| 474 | ssh_died = (p.poll() is not None) | ||
| 475 | if ssh_died: | ||
| 476 | return False | ||
| 477 | |||
| 478 | _master_processes.append(p) | ||
| 479 | _master_keys.add(key) | ||
| 480 | return True | ||
| 481 | finally: | ||
| 482 | _master_keys_lock.release() | ||
| 483 | |||
| 484 | def close_ssh(): | ||
| 485 | global _master_keys_lock | ||
| 486 | |||
| 487 | terminate_ssh_clients() | ||
| 488 | |||
| 489 | for p in _master_processes: | ||
| 490 | try: | ||
| 491 | os.kill(p.pid, SIGTERM) | ||
| 492 | p.wait() | ||
| 493 | except OSError: | ||
| 494 | pass | ||
| 495 | del _master_processes[:] | ||
| 496 | _master_keys.clear() | ||
| 497 | |||
| 498 | d = ssh_sock(create=False) | ||
| 499 | if d: | ||
| 500 | try: | ||
| 501 | platform_utils.rmdir(os.path.dirname(d)) | ||
| 502 | except OSError: | ||
| 503 | pass | ||
| 504 | |||
| 505 | # We're done with the lock, so we can delete it. | ||
| 506 | _master_keys_lock = None | ||
| 507 | |||
| 508 | URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):') | ||
| 509 | URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') | 471 | URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') |
| 510 | 472 | ||
| 473 | |||
| 511 | def GetSchemeFromUrl(url): | 474 | def GetSchemeFromUrl(url): |
| 512 | m = URI_ALL.match(url) | 475 | m = URI_ALL.match(url) |
| 513 | if m: | 476 | if m: |
| 514 | return m.group(1) | 477 | return m.group(1) |
| 515 | return None | 478 | return None |
| 516 | 479 | ||
| 480 | |||
| 517 | @contextlib.contextmanager | 481 | @contextlib.contextmanager |
| 518 | def GetUrlCookieFile(url, quiet): | 482 | def GetUrlCookieFile(url, quiet): |
| 519 | if url.startswith('persistent-'): | 483 | if url.startswith('persistent-'): |
| @@ -554,29 +518,11 @@ def GetUrlCookieFile(url, quiet): | |||
| 554 | cookiefile = os.path.expanduser(cookiefile) | 518 | cookiefile = os.path.expanduser(cookiefile) |
| 555 | yield cookiefile, None | 519 | yield cookiefile, None |
| 556 | 520 | ||
| 557 | def _preconnect(url): | ||
| 558 | m = URI_ALL.match(url) | ||
| 559 | if m: | ||
| 560 | scheme = m.group(1) | ||
| 561 | host = m.group(2) | ||
| 562 | if ':' in host: | ||
| 563 | host, port = host.split(':') | ||
| 564 | else: | ||
| 565 | port = None | ||
| 566 | if scheme in ('ssh', 'git+ssh', 'ssh+git'): | ||
| 567 | return _open_ssh(host, port) | ||
| 568 | return False | ||
| 569 | |||
| 570 | m = URI_SCP.match(url) | ||
| 571 | if m: | ||
| 572 | host = m.group(1) | ||
| 573 | return _open_ssh(host) | ||
| 574 | |||
| 575 | return False | ||
| 576 | 521 | ||
| 577 | class Remote(object): | 522 | class Remote(object): |
| 578 | """Configuration options related to a remote. | 523 | """Configuration options related to a remote. |
| 579 | """ | 524 | """ |
| 525 | |||
| 580 | def __init__(self, config, name): | 526 | def __init__(self, config, name): |
| 581 | self._config = config | 527 | self._config = config |
| 582 | self.name = name | 528 | self.name = name |
| @@ -585,7 +531,7 @@ class Remote(object): | |||
| 585 | self.review = self._Get('review') | 531 | self.review = self._Get('review') |
| 586 | self.projectname = self._Get('projectname') | 532 | self.projectname = self._Get('projectname') |
| 587 | self.fetch = list(map(RefSpec.FromString, | 533 | self.fetch = list(map(RefSpec.FromString, |
| 588 | self._Get('fetch', all_keys=True))) | 534 | self._Get('fetch', all_keys=True))) |
| 589 | self._review_url = None | 535 | self._review_url = None |
| 590 | 536 | ||
| 591 | def _InsteadOf(self): | 537 | def _InsteadOf(self): |
| @@ -599,8 +545,8 @@ class Remote(object): | |||
| 599 | insteadOfList = globCfg.GetString(key, all_keys=True) | 545 | insteadOfList = globCfg.GetString(key, all_keys=True) |
| 600 | 546 | ||
| 601 | for insteadOf in insteadOfList: | 547 | for insteadOf in insteadOfList: |
| 602 | if self.url.startswith(insteadOf) \ | 548 | if (self.url.startswith(insteadOf) |
| 603 | and len(insteadOf) > len(longest): | 549 | and len(insteadOf) > len(longest)): |
| 604 | longest = insteadOf | 550 | longest = insteadOf |
| 605 | longestUrl = url | 551 | longestUrl = url |
| 606 | 552 | ||
| @@ -609,9 +555,23 @@ class Remote(object): | |||
| 609 | 555 | ||
| 610 | return self.url.replace(longest, longestUrl, 1) | 556 | return self.url.replace(longest, longestUrl, 1) |
| 611 | 557 | ||
| 612 | def PreConnectFetch(self): | 558 | def PreConnectFetch(self, ssh_proxy): |
| 559 | """Run any setup for this remote before we connect to it. | ||
| 560 | |||
| 561 | In practice, if the remote is using SSH, we'll attempt to create a new | ||
| 562 | SSH master session to it for reuse across projects. | ||
| 563 | |||
| 564 | Args: | ||
| 565 | ssh_proxy: The SSH settings for managing master sessions. | ||
| 566 | |||
| 567 | Returns: | ||
| 568 | Whether the preconnect phase for this remote was successful. | ||
| 569 | """ | ||
| 570 | if not ssh_proxy: | ||
| 571 | return True | ||
| 572 | |||
| 613 | connectionUrl = self._InsteadOf() | 573 | connectionUrl = self._InsteadOf() |
| 614 | return _preconnect(connectionUrl) | 574 | return ssh_proxy.preconnect(connectionUrl) |
| 615 | 575 | ||
| 616 | def ReviewUrl(self, userEmail, validate_certs): | 576 | def ReviewUrl(self, userEmail, validate_certs): |
| 617 | if self._review_url is None: | 577 | if self._review_url is None: |
| @@ -731,12 +691,13 @@ class Remote(object): | |||
| 731 | 691 | ||
| 732 | def _Get(self, key, all_keys=False): | 692 | def _Get(self, key, all_keys=False): |
| 733 | key = 'remote.%s.%s' % (self.name, key) | 693 | key = 'remote.%s.%s' % (self.name, key) |
| 734 | return self._config.GetString(key, all_keys = all_keys) | 694 | return self._config.GetString(key, all_keys=all_keys) |
| 735 | 695 | ||
| 736 | 696 | ||
| 737 | class Branch(object): | 697 | class Branch(object): |
| 738 | """Configuration options related to a single branch. | 698 | """Configuration options related to a single branch. |
| 739 | """ | 699 | """ |
| 700 | |||
| 740 | def __init__(self, config, name): | 701 | def __init__(self, config, name): |
| 741 | self._config = config | 702 | self._config = config |
| 742 | self.name = name | 703 | self.name = name |
| @@ -780,4 +741,71 @@ class Branch(object): | |||
| 780 | 741 | ||
| 781 | def _Get(self, key, all_keys=False): | 742 | def _Get(self, key, all_keys=False): |
| 782 | key = 'branch.%s.%s' % (self.name, key) | 743 | key = 'branch.%s.%s' % (self.name, key) |
| 783 | return self._config.GetString(key, all_keys = all_keys) | 744 | return self._config.GetString(key, all_keys=all_keys) |
| 745 | |||
| 746 | |||
| 747 | class SyncAnalysisState: | ||
| 748 | """Configuration options related to logging of sync state for analysis. | ||
| 749 | |||
| 750 | This object is versioned. | ||
| 751 | """ | ||
| 752 | def __init__(self, config, options, superproject_logging_data): | ||
| 753 | """Initializes SyncAnalysisState. | ||
| 754 | |||
| 755 | Saves the following data into the |config| object. | ||
| 756 | - sys.argv, options, superproject's logging data. | ||
| 757 | - repo.*, branch.* and remote.* parameters from config object. | ||
| 758 | - Current time as synctime. | ||
| 759 | - Version number of the object. | ||
| 760 | |||
| 761 | All the keys saved by this object are prepended with SYNC_STATE_PREFIX. | ||
| 762 | |||
| 763 | Args: | ||
| 764 | config: GitConfig object to store all options. | ||
| 765 | options: Options passed to sync returned from optparse. See _Options(). | ||
| 766 | superproject_logging_data: A dictionary of superproject data that is to be logged. | ||
| 767 | """ | ||
| 768 | self._config = config | ||
| 769 | now = datetime.datetime.utcnow() | ||
| 770 | self._Set('main.synctime', now.isoformat() + 'Z') | ||
| 771 | self._Set('main.version', '1') | ||
| 772 | self._Set('sys.argv', sys.argv) | ||
| 773 | for key, value in superproject_logging_data.items(): | ||
| 774 | self._Set(f'superproject.{key}', value) | ||
| 775 | for key, value in options.__dict__.items(): | ||
| 776 | self._Set(f'options.{key}', value) | ||
| 777 | config_items = config.DumpConfigDict().items() | ||
| 778 | EXTRACT_NAMESPACES = {'repo', 'branch', 'remote'} | ||
| 779 | self._SetDictionary({k: v for k, v in config_items | ||
| 780 | if not k.startswith(SYNC_STATE_PREFIX) and | ||
| 781 | k.split('.', 1)[0] in EXTRACT_NAMESPACES}) | ||
| 782 | |||
| 783 | def _SetDictionary(self, data): | ||
| 784 | """Save all key/value pairs of |data| dictionary. | ||
| 785 | |||
| 786 | Args: | ||
| 787 | data: A dictionary whose key/value are to be saved. | ||
| 788 | """ | ||
| 789 | for key, value in data.items(): | ||
| 790 | self._Set(key, value) | ||
| 791 | |||
| 792 | def _Set(self, key, value): | ||
| 793 | """Set the |value| for a |key| in the |_config| member. | ||
| 794 | |||
| 795 | |key| is prepended with the value of SYNC_STATE_PREFIX constant. | ||
| 796 | |||
| 797 | Args: | ||
| 798 | key: Name of the key. | ||
| 799 | value: |value| could be of any type. If it is 'bool', it will be saved | ||
| 800 | as a Boolean and for all other types, it will be saved as a String. | ||
| 801 | """ | ||
| 802 | if value is None: | ||
| 803 | return | ||
| 804 | sync_key = f'{SYNC_STATE_PREFIX}{key}' | ||
| 805 | sync_key = sync_key.replace('_', '') | ||
| 806 | if isinstance(value, str): | ||
| 807 | self._config.SetString(sync_key, value) | ||
| 808 | elif isinstance(value, bool): | ||
| 809 | self._config.SetBoolean(sync_key, value) | ||
| 810 | else: | ||
| 811 | self._config.SetString(sync_key, str(value)) | ||
