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)) | ||