diff options
Diffstat (limited to 'git_config.py')
| -rw-r--r-- | git_config.py | 291 |
1 files changed, 128 insertions, 163 deletions
diff --git a/git_config.py b/git_config.py index fcd0446c..3cd09391 100644 --- a/git_config.py +++ b/git_config.py | |||
| @@ -13,32 +13,28 @@ | |||
| 13 | # limitations under the License. | 13 | # limitations under the License. |
| 14 | 14 | ||
| 15 | import contextlib | 15 | import contextlib |
| 16 | import datetime | ||
| 16 | import errno | 17 | import errno |
| 17 | from http.client import HTTPException | 18 | from http.client import HTTPException |
| 18 | import json | 19 | import json |
| 19 | import os | 20 | import os |
| 20 | import re | 21 | import re |
| 21 | import signal | ||
| 22 | import ssl | 22 | import ssl |
| 23 | import subprocess | 23 | import subprocess |
| 24 | import sys | 24 | import sys |
| 25 | try: | ||
| 26 | import threading as _threading | ||
| 27 | except ImportError: | ||
| 28 | import dummy_threading as _threading | ||
| 29 | import time | ||
| 30 | import urllib.error | 25 | import urllib.error |
| 31 | import urllib.request | 26 | import urllib.request |
| 32 | 27 | ||
| 33 | from error import GitError, UploadError | 28 | from error import GitError, UploadError |
| 34 | import platform_utils | 29 | import platform_utils |
| 35 | from repo_trace import Trace | 30 | from repo_trace import Trace |
| 36 | |||
| 37 | from git_command import GitCommand | 31 | from git_command import GitCommand |
| 38 | from git_command import ssh_sock | ||
| 39 | from git_command import terminate_ssh_clients | ||
| 40 | from git_refs import R_CHANGES, R_HEADS, R_TAGS | 32 | from git_refs import R_CHANGES, R_HEADS, R_TAGS |
| 41 | 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 | |||
| 42 | ID_RE = re.compile(r'^[0-9a-f]{40}$') | 38 | ID_RE = re.compile(r'^[0-9a-f]{40}$') |
| 43 | 39 | ||
| 44 | REVIEW_CACHE = dict() | 40 | REVIEW_CACHE = dict() |
| @@ -74,6 +70,15 @@ class GitConfig(object): | |||
| 74 | 70 | ||
| 75 | _USER_CONFIG = '~/.gitconfig' | 71 | _USER_CONFIG = '~/.gitconfig' |
| 76 | 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 | |||
| 77 | @classmethod | 82 | @classmethod |
| 78 | def ForUser(cls): | 83 | def ForUser(cls): |
| 79 | if cls._ForUser is None: | 84 | if cls._ForUser is None: |
| @@ -99,6 +104,10 @@ class GitConfig(object): | |||
| 99 | os.path.dirname(self.file), | 104 | os.path.dirname(self.file), |
| 100 | '.repo_' + os.path.basename(self.file) + '.json') | 105 | '.repo_' + os.path.basename(self.file) + '.json') |
| 101 | 106 | ||
| 107 | def ClearCache(self): | ||
| 108 | """Clear the in-memory cache of config.""" | ||
| 109 | self._cache_dict = None | ||
| 110 | |||
| 102 | def Has(self, name, include_defaults=True): | 111 | def Has(self, name, include_defaults=True): |
| 103 | """Return true if this configuration file has the key. | 112 | """Return true if this configuration file has the key. |
| 104 | """ | 113 | """ |
| @@ -262,6 +271,22 @@ class GitConfig(object): | |||
| 262 | self._branches[b.name] = b | 271 | self._branches[b.name] = b |
| 263 | return b | 272 | return b |
| 264 | 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 | |||
| 265 | def GetSubSections(self, section): | 290 | def GetSubSections(self, section): |
| 266 | """List all subsection names matching $section.*.* | 291 | """List all subsection names matching $section.*.* |
| 267 | """ | 292 | """ |
| @@ -327,8 +352,8 @@ class GitConfig(object): | |||
| 327 | Trace(': parsing %s', self.file) | 352 | Trace(': parsing %s', self.file) |
| 328 | with open(self._json) as fd: | 353 | with open(self._json) as fd: |
| 329 | return json.load(fd) | 354 | return json.load(fd) |
| 330 | except (IOError, ValueError): | 355 | except (IOError, ValueErrorl): |
| 331 | platform_utils.remove(self._json) | 356 | platform_utils.remove(self._json, missing_ok=True) |
| 332 | return None | 357 | return None |
| 333 | 358 | ||
| 334 | def _SaveJson(self, cache): | 359 | def _SaveJson(self, cache): |
| @@ -336,8 +361,7 @@ class GitConfig(object): | |||
| 336 | with open(self._json, 'w') as fd: | 361 | with open(self._json, 'w') as fd: |
| 337 | json.dump(cache, fd, indent=2) | 362 | json.dump(cache, fd, indent=2) |
| 338 | except (IOError, TypeError): | 363 | except (IOError, TypeError): |
| 339 | if os.path.exists(self._json): | 364 | platform_utils.remove(self._json, missing_ok=True) |
| 340 | platform_utils.remove(self._json) | ||
| 341 | 365 | ||
| 342 | def _ReadGit(self): | 366 | def _ReadGit(self): |
| 343 | """ | 367 | """ |
| @@ -347,9 +371,10 @@ class GitConfig(object): | |||
| 347 | 371 | ||
| 348 | """ | 372 | """ |
| 349 | c = {} | 373 | c = {} |
| 350 | d = self._do('--null', '--list') | 374 | if not os.path.exists(self.file): |
| 351 | if d is None: | ||
| 352 | return c | 375 | return c |
| 376 | |||
| 377 | d = self._do('--null', '--list') | ||
| 353 | for line in d.rstrip('\0').split('\0'): | 378 | for line in d.rstrip('\0').split('\0'): |
| 354 | if '\n' in line: | 379 | if '\n' in line: |
| 355 | key, val = line.split('\n', 1) | 380 | key, val = line.split('\n', 1) |
| @@ -365,7 +390,10 @@ class GitConfig(object): | |||
| 365 | return c | 390 | return c |
| 366 | 391 | ||
| 367 | def _do(self, *args): | 392 | def _do(self, *args): |
| 368 | command = ['config', '--file', self.file, '--includes'] | 393 | if self.file == self._SYSTEM_CONFIG: |
| 394 | command = ['config', '--system', '--includes'] | ||
| 395 | else: | ||
| 396 | command = ['config', '--file', self.file, '--includes'] | ||
| 369 | command.extend(args) | 397 | command.extend(args) |
| 370 | 398 | ||
| 371 | p = GitCommand(None, | 399 | p = GitCommand(None, |
| @@ -375,7 +403,7 @@ class GitConfig(object): | |||
| 375 | if p.Wait() == 0: | 403 | if p.Wait() == 0: |
| 376 | return p.stdout | 404 | return p.stdout |
| 377 | else: | 405 | else: |
| 378 | GitError('git config %s: %s' % (str(args), p.stderr)) | 406 | raise GitError('git config %s: %s' % (str(args), p.stderr)) |
| 379 | 407 | ||
| 380 | 408 | ||
| 381 | class RepoConfig(GitConfig): | 409 | class RepoConfig(GitConfig): |
| @@ -440,129 +468,6 @@ class RefSpec(object): | |||
| 440 | return s | 468 | return s |
| 441 | 469 | ||
| 442 | 470 | ||
| 443 | _master_processes = [] | ||
| 444 | _master_keys = set() | ||
| 445 | _ssh_master = True | ||
| 446 | _master_keys_lock = None | ||
| 447 | |||
| 448 | |||
| 449 | def init_ssh(): | ||
| 450 | """Should be called once at the start of repo to init ssh master handling. | ||
| 451 | |||
| 452 | At the moment, all we do is to create our lock. | ||
| 453 | """ | ||
| 454 | global _master_keys_lock | ||
| 455 | assert _master_keys_lock is None, "Should only call init_ssh once" | ||
| 456 | _master_keys_lock = _threading.Lock() | ||
| 457 | |||
| 458 | |||
| 459 | def _open_ssh(host, port=None): | ||
| 460 | global _ssh_master | ||
| 461 | |||
| 462 | # Bail before grabbing the lock if we already know that we aren't going to | ||
| 463 | # try creating new masters below. | ||
| 464 | if sys.platform in ('win32', 'cygwin'): | ||
| 465 | return False | ||
| 466 | |||
| 467 | # Acquire the lock. This is needed to prevent opening multiple masters for | ||
| 468 | # the same host when we're running "repo sync -jN" (for N > 1) _and_ the | ||
| 469 | # manifest <remote fetch="ssh://xyz"> specifies a different host from the | ||
| 470 | # one that was passed to repo init. | ||
| 471 | _master_keys_lock.acquire() | ||
| 472 | try: | ||
| 473 | |||
| 474 | # Check to see whether we already think that the master is running; if we | ||
| 475 | # think it's already running, return right away. | ||
| 476 | if port is not None: | ||
| 477 | key = '%s:%s' % (host, port) | ||
| 478 | else: | ||
| 479 | key = host | ||
| 480 | |||
| 481 | if key in _master_keys: | ||
| 482 | return True | ||
| 483 | |||
| 484 | if not _ssh_master or 'GIT_SSH' in os.environ: | ||
| 485 | # Failed earlier, so don't retry. | ||
| 486 | return False | ||
| 487 | |||
| 488 | # We will make two calls to ssh; this is the common part of both calls. | ||
| 489 | command_base = ['ssh', | ||
| 490 | '-o', 'ControlPath %s' % ssh_sock(), | ||
| 491 | host] | ||
| 492 | if port is not None: | ||
| 493 | command_base[1:1] = ['-p', str(port)] | ||
| 494 | |||
| 495 | # Since the key wasn't in _master_keys, we think that master isn't running. | ||
| 496 | # ...but before actually starting a master, we'll double-check. This can | ||
| 497 | # be important because we can't tell that that 'git@myhost.com' is the same | ||
| 498 | # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file. | ||
| 499 | check_command = command_base + ['-O', 'check'] | ||
| 500 | try: | ||
| 501 | Trace(': %s', ' '.join(check_command)) | ||
| 502 | check_process = subprocess.Popen(check_command, | ||
| 503 | stdout=subprocess.PIPE, | ||
| 504 | stderr=subprocess.PIPE) | ||
| 505 | check_process.communicate() # read output, but ignore it... | ||
| 506 | isnt_running = check_process.wait() | ||
| 507 | |||
| 508 | if not isnt_running: | ||
| 509 | # Our double-check found that the master _was_ infact running. Add to | ||
| 510 | # the list of keys. | ||
| 511 | _master_keys.add(key) | ||
| 512 | return True | ||
| 513 | except Exception: | ||
| 514 | # Ignore excpetions. We we will fall back to the normal command and print | ||
| 515 | # to the log there. | ||
| 516 | pass | ||
| 517 | |||
| 518 | command = command_base[:1] + ['-M', '-N'] + command_base[1:] | ||
| 519 | try: | ||
| 520 | Trace(': %s', ' '.join(command)) | ||
| 521 | p = subprocess.Popen(command) | ||
| 522 | except Exception as e: | ||
| 523 | _ssh_master = False | ||
| 524 | print('\nwarn: cannot enable ssh control master for %s:%s\n%s' | ||
| 525 | % (host, port, str(e)), file=sys.stderr) | ||
| 526 | return False | ||
| 527 | |||
| 528 | time.sleep(1) | ||
| 529 | ssh_died = (p.poll() is not None) | ||
| 530 | if ssh_died: | ||
| 531 | return False | ||
| 532 | |||
| 533 | _master_processes.append(p) | ||
| 534 | _master_keys.add(key) | ||
| 535 | return True | ||
| 536 | finally: | ||
| 537 | _master_keys_lock.release() | ||
| 538 | |||
| 539 | |||
| 540 | def close_ssh(): | ||
| 541 | global _master_keys_lock | ||
| 542 | |||
| 543 | terminate_ssh_clients() | ||
| 544 | |||
| 545 | for p in _master_processes: | ||
| 546 | try: | ||
| 547 | os.kill(p.pid, signal.SIGTERM) | ||
| 548 | p.wait() | ||
| 549 | except OSError: | ||
| 550 | pass | ||
| 551 | del _master_processes[:] | ||
| 552 | _master_keys.clear() | ||
| 553 | |||
| 554 | d = ssh_sock(create=False) | ||
| 555 | if d: | ||
| 556 | try: | ||
| 557 | platform_utils.rmdir(os.path.dirname(d)) | ||
| 558 | except OSError: | ||
| 559 | pass | ||
| 560 | |||
| 561 | # We're done with the lock, so we can delete it. | ||
| 562 | _master_keys_lock = None | ||
| 563 | |||
| 564 | |||
| 565 | URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):') | ||
| 566 | URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') | 471 | URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') |
| 567 | 472 | ||
| 568 | 473 | ||
| @@ -614,27 +519,6 @@ def GetUrlCookieFile(url, quiet): | |||
| 614 | yield cookiefile, None | 519 | yield cookiefile, None |
| 615 | 520 | ||
| 616 | 521 | ||
| 617 | def _preconnect(url): | ||
| 618 | m = URI_ALL.match(url) | ||
| 619 | if m: | ||
| 620 | scheme = m.group(1) | ||
| 621 | host = m.group(2) | ||
| 622 | if ':' in host: | ||
| 623 | host, port = host.split(':') | ||
| 624 | else: | ||
| 625 | port = None | ||
| 626 | if scheme in ('ssh', 'git+ssh', 'ssh+git'): | ||
| 627 | return _open_ssh(host, port) | ||
| 628 | return False | ||
| 629 | |||
| 630 | m = URI_SCP.match(url) | ||
| 631 | if m: | ||
| 632 | host = m.group(1) | ||
| 633 | return _open_ssh(host) | ||
| 634 | |||
| 635 | return False | ||
| 636 | |||
| 637 | |||
| 638 | class Remote(object): | 522 | class Remote(object): |
| 639 | """Configuration options related to a remote. | 523 | """Configuration options related to a remote. |
| 640 | """ | 524 | """ |
| @@ -671,9 +555,23 @@ class Remote(object): | |||
| 671 | 555 | ||
| 672 | return self.url.replace(longest, longestUrl, 1) | 556 | return self.url.replace(longest, longestUrl, 1) |
| 673 | 557 | ||
| 674 | 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 | |||
| 675 | connectionUrl = self._InsteadOf() | 573 | connectionUrl = self._InsteadOf() |
| 676 | return _preconnect(connectionUrl) | 574 | return ssh_proxy.preconnect(connectionUrl) |
| 677 | 575 | ||
| 678 | def ReviewUrl(self, userEmail, validate_certs): | 576 | def ReviewUrl(self, userEmail, validate_certs): |
| 679 | if self._review_url is None: | 577 | if self._review_url is None: |
| @@ -844,3 +742,70 @@ class Branch(object): | |||
| 844 | def _Get(self, key, all_keys=False): | 742 | def _Get(self, key, all_keys=False): |
| 845 | key = 'branch.%s.%s' % (self.name, key) | 743 | key = 'branch.%s.%s' % (self.name, key) |
| 846 | 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)) | ||
