summaryrefslogtreecommitdiffstats
path: root/git_config.py
diff options
context:
space:
mode:
Diffstat (limited to 'git_config.py')
-rw-r--r--git_config.py291
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
15import contextlib 15import contextlib
16import datetime
16import errno 17import errno
17from http.client import HTTPException 18from http.client import HTTPException
18import json 19import json
19import os 20import os
20import re 21import re
21import signal
22import ssl 22import ssl
23import subprocess 23import subprocess
24import sys 24import sys
25try:
26 import threading as _threading
27except ImportError:
28 import dummy_threading as _threading
29import time
30import urllib.error 25import urllib.error
31import urllib.request 26import urllib.request
32 27
33from error import GitError, UploadError 28from error import GitError, UploadError
34import platform_utils 29import platform_utils
35from repo_trace import Trace 30from repo_trace import Trace
36
37from git_command import GitCommand 31from git_command import GitCommand
38from git_command import ssh_sock
39from git_command import terminate_ssh_clients
40from git_refs import R_CHANGES, R_HEADS, R_TAGS 32from 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.
36SYNC_STATE_PREFIX = 'repo.syncstate.'
37
42ID_RE = re.compile(r'^[0-9a-f]{40}$') 38ID_RE = re.compile(r'^[0-9a-f]{40}$')
43 39
44REVIEW_CACHE = dict() 40REVIEW_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
381class RepoConfig(GitConfig): 409class 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
449def 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
459def _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
540def 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
565URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
566URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') 471URI_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
617def _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
638class Remote(object): 522class 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
747class 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))