summaryrefslogtreecommitdiffstats
path: root/git_config.py
diff options
context:
space:
mode:
Diffstat (limited to 'git_config.py')
-rw-r--r--git_config.py426
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
17from __future__ import print_function
18
19import contextlib 15import contextlib
16import datetime
20import errno 17import errno
18from http.client import HTTPException
21import json 19import json
22import os 20import os
23import re 21import re
24import ssl 22import ssl
25import subprocess 23import subprocess
26import sys 24import sys
27try: 25import urllib.error
28 import threading as _threading 26import urllib.request
29except ImportError: 27
30 import dummy_threading as _threading
31import time
32
33from pyversion import is_python3
34if is_python3():
35 import urllib.request
36 import urllib.error
37else:
38 import urllib2
39 import imp
40 urllib = imp.new_module('urllib')
41 urllib.request = urllib2
42 urllib.error = urllib2
43
44from signal import SIGTERM
45from error import GitError, UploadError 28from error import GitError, UploadError
46import platform_utils 29import platform_utils
47from repo_trace import Trace 30from repo_trace import Trace
48if is_python3():
49 from http.client import HTTPException
50else:
51 from httplib import HTTPException
52
53from git_command import GitCommand 31from git_command import GitCommand
54from git_command import ssh_sock
55from git_command import terminate_ssh_clients
56from git_refs import R_CHANGES, R_HEADS, R_TAGS 32from 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.
36SYNC_STATE_PREFIX = 'repo.syncstate.'
37
58ID_RE = re.compile(r'^[0-9a-f]{40}$') 38ID_RE = re.compile(r'^[0-9a-f]{40}$')
59 39
60REVIEW_CACHE = dict() 40REVIEW_CACHE = dict()
61 41
42
62def IsChange(rev): 43def IsChange(rev):
63 return rev.startswith(R_CHANGES) 44 return rev.startswith(R_CHANGES)
64 45
46
65def IsId(rev): 47def IsId(rev):
66 return ID_RE.match(rev) 48 return ID_RE.match(rev)
67 49
50
68def IsTag(rev): 51def IsTag(rev):
69 return rev.startswith(R_TAGS) 52 return rev.startswith(R_TAGS)
70 53
54
71def IsImmutable(rev): 55def 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
74def _key(name): 59def _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
82class GitConfig(object): 68class 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
409class RepoConfig(GitConfig):
410 """User settings for repo itself."""
411
412 _USER_CONFIG = '~/.repoconfig/config'
332 413
333 414
334class RefSpec(object): 415class 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
395def 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
404def _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
484def 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
508URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
509URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') 471URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
510 472
473
511def GetSchemeFromUrl(url): 474def 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
518def GetUrlCookieFile(url, quiet): 482def 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
557def _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
577class Remote(object): 522class 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
737class Branch(object): 697class 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
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))