summaryrefslogtreecommitdiffstats
path: root/git_config.py
diff options
context:
space:
mode:
authorGavin Mak <gavinmak@google.com>2023-03-11 06:46:20 +0000
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-03-22 17:46:28 +0000
commitea2e330e43c182dc16b0111ebc69ee5a71ee4ce1 (patch)
treedc33ba0e56825b3e007d0589891756724725a465 /git_config.py
parent1604cf255f8c1786a23388db6d5277ac7949a24a (diff)
downloadgit-repo-ea2e330e43c182dc16b0111ebc69ee5a71ee4ce1.tar.gz
Format codebase with black and check formatting in CQ
Apply rules set by https://gerrit-review.googlesource.com/c/git-repo/+/362954/ across the codebase and fix any lingering errors caught by flake8. Also check black formatting in run_tests (and CQ). Bug: b/267675342 Change-Id: I972d77649dac351150dcfeb1cd1ad0ea2efc1956 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/363474 Reviewed-by: Mike Frysinger <vapier@google.com> Tested-by: Gavin Mak <gavinmak@google.com> Commit-Queue: Gavin Mak <gavinmak@google.com>
Diffstat (limited to 'git_config.py')
-rw-r--r--git_config.py1464
1 files changed, 742 insertions, 722 deletions
diff --git a/git_config.py b/git_config.py
index 9ad979ad..05b3c1ee 100644
--- a/git_config.py
+++ b/git_config.py
@@ -34,23 +34,23 @@ from git_refs import R_CHANGES, R_HEADS, R_TAGS
34 34
35# Prefix that is prepended to all the keys of SyncAnalysisState's data 35# Prefix that is prepended to all the keys of SyncAnalysisState's data
36# that is saved in the config. 36# that is saved in the config.
37SYNC_STATE_PREFIX = 'repo.syncstate.' 37SYNC_STATE_PREFIX = "repo.syncstate."
38 38
39ID_RE = re.compile(r'^[0-9a-f]{40}$') 39ID_RE = re.compile(r"^[0-9a-f]{40}$")
40 40
41REVIEW_CACHE = dict() 41REVIEW_CACHE = dict()
42 42
43 43
44def IsChange(rev): 44def IsChange(rev):
45 return rev.startswith(R_CHANGES) 45 return rev.startswith(R_CHANGES)
46 46
47 47
48def IsId(rev): 48def IsId(rev):
49 return ID_RE.match(rev) 49 return ID_RE.match(rev)
50 50
51 51
52def IsTag(rev): 52def IsTag(rev):
53 return rev.startswith(R_TAGS) 53 return rev.startswith(R_TAGS)
54 54
55 55
56def IsImmutable(rev): 56def IsImmutable(rev):
@@ -58,765 +58,785 @@ def IsImmutable(rev):
58 58
59 59
60def _key(name): 60def _key(name):
61 parts = name.split('.') 61 parts = name.split(".")
62 if len(parts) < 2: 62 if len(parts) < 2:
63 return name.lower() 63 return name.lower()
64 parts[0] = parts[0].lower() 64 parts[0] = parts[0].lower()
65 parts[-1] = parts[-1].lower() 65 parts[-1] = parts[-1].lower()
66 return '.'.join(parts) 66 return ".".join(parts)
67 67
68 68
69class GitConfig(object): 69class GitConfig(object):
70 _ForUser = None 70 _ForUser = None
71 71
72 _ForSystem = None 72 _ForSystem = None
73 _SYSTEM_CONFIG = '/etc/gitconfig' 73 _SYSTEM_CONFIG = "/etc/gitconfig"
74 74
75 @classmethod 75 @classmethod
76 def ForSystem(cls): 76 def ForSystem(cls):
77 if cls._ForSystem is None: 77 if cls._ForSystem is None:
78 cls._ForSystem = cls(configfile=cls._SYSTEM_CONFIG) 78 cls._ForSystem = cls(configfile=cls._SYSTEM_CONFIG)
79 return cls._ForSystem 79 return cls._ForSystem
80 80
81 @classmethod 81 @classmethod
82 def ForUser(cls): 82 def ForUser(cls):
83 if cls._ForUser is None: 83 if cls._ForUser is None:
84 cls._ForUser = cls(configfile=cls._getUserConfig()) 84 cls._ForUser = cls(configfile=cls._getUserConfig())
85 return cls._ForUser 85 return cls._ForUser
86 86
87 @staticmethod 87 @staticmethod
88 def _getUserConfig(): 88 def _getUserConfig():
89 return os.path.expanduser('~/.gitconfig') 89 return os.path.expanduser("~/.gitconfig")
90 90
91 @classmethod 91 @classmethod
92 def ForRepository(cls, gitdir, defaults=None): 92 def ForRepository(cls, gitdir, defaults=None):
93 return cls(configfile=os.path.join(gitdir, 'config'), 93 return cls(configfile=os.path.join(gitdir, "config"), defaults=defaults)
94 defaults=defaults) 94
95 95 def __init__(self, configfile, defaults=None, jsonFile=None):
96 def __init__(self, configfile, defaults=None, jsonFile=None): 96 self.file = configfile
97 self.file = configfile 97 self.defaults = defaults
98 self.defaults = defaults 98 self._cache_dict = None
99 self._cache_dict = None 99 self._section_dict = None
100 self._section_dict = None 100 self._remotes = {}
101 self._remotes = {} 101 self._branches = {}
102 self._branches = {} 102
103 103 self._json = jsonFile
104 self._json = jsonFile 104 if self._json is None:
105 if self._json is None: 105 self._json = os.path.join(
106 self._json = os.path.join( 106 os.path.dirname(self.file),
107 os.path.dirname(self.file), 107 ".repo_" + os.path.basename(self.file) + ".json",
108 '.repo_' + os.path.basename(self.file) + '.json') 108 )
109 109
110 def ClearCache(self): 110 def ClearCache(self):
111 """Clear the in-memory cache of config.""" 111 """Clear the in-memory cache of config."""
112 self._cache_dict = None 112 self._cache_dict = None
113 113
114 def Has(self, name, include_defaults=True): 114 def Has(self, name, include_defaults=True):
115 """Return true if this configuration file has the key. 115 """Return true if this configuration file has the key."""
116 """ 116 if _key(name) in self._cache:
117 if _key(name) in self._cache: 117 return True
118 return True 118 if include_defaults and self.defaults:
119 if include_defaults and self.defaults: 119 return self.defaults.Has(name, include_defaults=True)
120 return self.defaults.Has(name, include_defaults=True) 120 return False
121 return False 121
122 122 def GetInt(self, name: str) -> Union[int, None]:
123 def GetInt(self, name: str) -> Union[int, None]: 123 """Returns an integer from the configuration file.
124 """Returns an integer from the configuration file. 124
125 125 This follows the git config syntax.
126 This follows the git config syntax. 126
127 127 Args:
128 Args: 128 name: The key to lookup.
129 name: The key to lookup. 129
130 130 Returns:
131 Returns: 131 None if the value was not defined, or is not an int.
132 None if the value was not defined, or is not an int. 132 Otherwise, the number itself.
133 Otherwise, the number itself. 133 """
134 """ 134 v = self.GetString(name)
135 v = self.GetString(name) 135 if v is None:
136 if v is None: 136 return None
137 return None 137 v = v.strip()
138 v = v.strip() 138
139 139 mult = 1
140 mult = 1 140 if v.endswith("k"):
141 if v.endswith('k'): 141 v = v[:-1]
142 v = v[:-1] 142 mult = 1024
143 mult = 1024 143 elif v.endswith("m"):
144 elif v.endswith('m'): 144 v = v[:-1]
145 v = v[:-1] 145 mult = 1024 * 1024
146 mult = 1024 * 1024 146 elif v.endswith("g"):
147 elif v.endswith('g'): 147 v = v[:-1]
148 v = v[:-1] 148 mult = 1024 * 1024 * 1024
149 mult = 1024 * 1024 * 1024 149
150 150 base = 10
151 base = 10 151 if v.startswith("0x"):
152 if v.startswith('0x'): 152 base = 16
153 base = 16
154
155 try:
156 return int(v, base=base) * mult
157 except ValueError:
158 print(
159 f"warning: expected {name} to represent an integer, got {v} instead",
160 file=sys.stderr)
161 return None
162
163 def DumpConfigDict(self):
164 """Returns the current configuration dict.
165
166 Configuration data is information only (e.g. logging) and
167 should not be considered a stable data-source.
168
169 Returns:
170 dict of {<key>, <value>} for git configuration cache.
171 <value> are strings converted by GetString.
172 """
173 config_dict = {}
174 for key in self._cache:
175 config_dict[key] = self.GetString(key)
176 return config_dict
177
178 def GetBoolean(self, name: str) -> Union[str, None]:
179 """Returns a boolean from the configuration file.
180 None : The value was not defined, or is not a boolean.
181 True : The value was set to true or yes.
182 False: The value was set to false or no.
183 """
184 v = self.GetString(name)
185 if v is None:
186 return None
187 v = v.lower()
188 if v in ('true', 'yes'):
189 return True
190 if v in ('false', 'no'):
191 return False
192 print(f"warning: expected {name} to represent a boolean, got {v} instead",
193 file=sys.stderr)
194 return None
195 153
196 def SetBoolean(self, name, value): 154 try:
197 """Set the truthy value for a key.""" 155 return int(v, base=base) * mult
198 if value is not None: 156 except ValueError:
199 value = 'true' if value else 'false' 157 print(
200 self.SetString(name, value) 158 f"warning: expected {name} to represent an integer, got {v} "
159 "instead",
160 file=sys.stderr,
161 )
162 return None
163
164 def DumpConfigDict(self):
165 """Returns the current configuration dict.
166
167 Configuration data is information only (e.g. logging) and
168 should not be considered a stable data-source.
169
170 Returns:
171 dict of {<key>, <value>} for git configuration cache.
172 <value> are strings converted by GetString.
173 """
174 config_dict = {}
175 for key in self._cache:
176 config_dict[key] = self.GetString(key)
177 return config_dict
178
179 def GetBoolean(self, name: str) -> Union[str, None]:
180 """Returns a boolean from the configuration file.
181
182 Returns:
183 None: The value was not defined, or is not a boolean.
184 True: The value was set to true or yes.
185 False: The value was set to false or no.
186 """
187 v = self.GetString(name)
188 if v is None:
189 return None
190 v = v.lower()
191 if v in ("true", "yes"):
192 return True
193 if v in ("false", "no"):
194 return False
195 print(
196 f"warning: expected {name} to represent a boolean, got {v} instead",
197 file=sys.stderr,
198 )
199 return None
201 200
202 def GetString(self, name: str, all_keys: bool = False) -> Union[str, None]: 201 def SetBoolean(self, name, value):
203 """Get the first value for a key, or None if it is not defined. 202 """Set the truthy value for a key."""
203 if value is not None:
204 value = "true" if value else "false"
205 self.SetString(name, value)
204 206
205 This configuration file is used first, if the key is not 207 def GetString(self, name: str, all_keys: bool = False) -> Union[str, None]:
206 defined or all_keys = True then the defaults are also searched. 208 """Get the first value for a key, or None if it is not defined.
207 """
208 try:
209 v = self._cache[_key(name)]
210 except KeyError:
211 if self.defaults:
212 return self.defaults.GetString(name, all_keys=all_keys)
213 v = []
214
215 if not all_keys:
216 if v:
217 return v[0]
218 return None
219
220 r = []
221 r.extend(v)
222 if self.defaults:
223 r.extend(self.defaults.GetString(name, all_keys=True))
224 return r
225
226 def SetString(self, name, value):
227 """Set the value(s) for a key.
228 Only this configuration file is modified.
229
230 The supplied value should be either a string, or a list of strings (to
231 store multiple values), or None (to delete the key).
232 """
233 key = _key(name)
234 209
235 try: 210 This configuration file is used first, if the key is not
236 old = self._cache[key] 211 defined or all_keys = True then the defaults are also searched.
237 except KeyError: 212 """
238 old = [] 213 try:
214 v = self._cache[_key(name)]
215 except KeyError:
216 if self.defaults:
217 return self.defaults.GetString(name, all_keys=all_keys)
218 v = []
219
220 if not all_keys:
221 if v:
222 return v[0]
223 return None
224
225 r = []
226 r.extend(v)
227 if self.defaults:
228 r.extend(self.defaults.GetString(name, all_keys=True))
229 return r
230
231 def SetString(self, name, value):
232 """Set the value(s) for a key.
233 Only this configuration file is modified.
234
235 The supplied value should be either a string, or a list of strings (to
236 store multiple values), or None (to delete the key).
237 """
238 key = _key(name)
239 239
240 if value is None: 240 try:
241 if old: 241 old = self._cache[key]
242 del self._cache[key] 242 except KeyError:
243 self._do('--unset-all', name) 243 old = []
244
245 if value is None:
246 if old:
247 del self._cache[key]
248 self._do("--unset-all", name)
249
250 elif isinstance(value, list):
251 if len(value) == 0:
252 self.SetString(name, None)
253
254 elif len(value) == 1:
255 self.SetString(name, value[0])
256
257 elif old != value:
258 self._cache[key] = list(value)
259 self._do("--replace-all", name, value[0])
260 for i in range(1, len(value)):
261 self._do("--add", name, value[i])
262
263 elif len(old) != 1 or old[0] != value:
264 self._cache[key] = [value]
265 self._do("--replace-all", name, value)
266
267 def GetRemote(self, name):
268 """Get the remote.$name.* configuration values as an object."""
269 try:
270 r = self._remotes[name]
271 except KeyError:
272 r = Remote(self, name)
273 self._remotes[r.name] = r
274 return r
275
276 def GetBranch(self, name):
277 """Get the branch.$name.* configuration values as an object."""
278 try:
279 b = self._branches[name]
280 except KeyError:
281 b = Branch(self, name)
282 self._branches[b.name] = b
283 return b
284
285 def GetSyncAnalysisStateData(self):
286 """Returns data to be logged for the analysis of sync performance."""
287 return {
288 k: v
289 for k, v in self.DumpConfigDict().items()
290 if k.startswith(SYNC_STATE_PREFIX)
291 }
292
293 def UpdateSyncAnalysisState(self, options, superproject_logging_data):
294 """Update Config's SYNC_STATE_PREFIX* data with the latest sync data.
295
296 Args:
297 options: Options passed to sync returned from optparse. See
298 _Options().
299 superproject_logging_data: A dictionary of superproject data that is
300 to be logged.
301
302 Returns:
303 SyncAnalysisState object.
304 """
305 return SyncAnalysisState(self, options, superproject_logging_data)
306
307 def GetSubSections(self, section):
308 """List all subsection names matching $section.*.*"""
309 return self._sections.get(section, set())
310
311 def HasSection(self, section, subsection=""):
312 """Does at least one key in section.subsection exist?"""
313 try:
314 return subsection in self._sections[section]
315 except KeyError:
316 return False
317
318 def UrlInsteadOf(self, url):
319 """Resolve any url.*.insteadof references."""
320 for new_url in self.GetSubSections("url"):
321 for old_url in self.GetString("url.%s.insteadof" % new_url, True):
322 if old_url is not None and url.startswith(old_url):
323 return new_url + url[len(old_url) :]
324 return url
325
326 @property
327 def _sections(self):
328 d = self._section_dict
329 if d is None:
330 d = {}
331 for name in self._cache.keys():
332 p = name.split(".")
333 if 2 == len(p):
334 section = p[0]
335 subsect = ""
336 else:
337 section = p[0]
338 subsect = ".".join(p[1:-1])
339 if section not in d:
340 d[section] = set()
341 d[section].add(subsect)
342 self._section_dict = d
343 return d
344
345 @property
346 def _cache(self):
347 if self._cache_dict is None:
348 self._cache_dict = self._Read()
349 return self._cache_dict
350
351 def _Read(self):
352 d = self._ReadJson()
353 if d is None:
354 d = self._ReadGit()
355 self._SaveJson(d)
356 return d
357
358 def _ReadJson(self):
359 try:
360 if os.path.getmtime(self._json) <= os.path.getmtime(self.file):
361 platform_utils.remove(self._json)
362 return None
363 except OSError:
364 return None
365 try:
366 with Trace(": parsing %s", self.file):
367 with open(self._json) as fd:
368 return json.load(fd)
369 except (IOError, ValueError):
370 platform_utils.remove(self._json, missing_ok=True)
371 return None
372
373 def _SaveJson(self, cache):
374 try:
375 with open(self._json, "w") as fd:
376 json.dump(cache, fd, indent=2)
377 except (IOError, TypeError):
378 platform_utils.remove(self._json, missing_ok=True)
379
380 def _ReadGit(self):
381 """
382 Read configuration data from git.
383
384 This internal method populates the GitConfig cache.
385
386 """
387 c = {}
388 if not os.path.exists(self.file):
389 return c
390
391 d = self._do("--null", "--list")
392 for line in d.rstrip("\0").split("\0"):
393 if "\n" in line:
394 key, val = line.split("\n", 1)
395 else:
396 key = line
397 val = None
398
399 if key in c:
400 c[key].append(val)
401 else:
402 c[key] = [val]
403
404 return c
405
406 def _do(self, *args):
407 if self.file == self._SYSTEM_CONFIG:
408 command = ["config", "--system", "--includes"]
409 else:
410 command = ["config", "--file", self.file, "--includes"]
411 command.extend(args)
244 412
245 elif isinstance(value, list): 413 p = GitCommand(None, command, capture_stdout=True, capture_stderr=True)
246 if len(value) == 0: 414 if p.Wait() == 0:
247 self.SetString(name, None) 415 return p.stdout
416 else:
417 raise GitError("git config %s: %s" % (str(args), p.stderr))
248 418
249 elif len(value) == 1:
250 self.SetString(name, value[0])
251 419
252 elif old != value: 420class RepoConfig(GitConfig):
253 self._cache[key] = list(value) 421 """User settings for repo itself."""
254 self._do('--replace-all', name, value[0])
255 for i in range(1, len(value)):
256 self._do('--add', name, value[i])
257 422
258 elif len(old) != 1 or old[0] != value: 423 @staticmethod
259 self._cache[key] = [value] 424 def _getUserConfig():
260 self._do('--replace-all', name, value) 425 repo_config_dir = os.getenv("REPO_CONFIG_DIR", os.path.expanduser("~"))
426 return os.path.join(repo_config_dir, ".repoconfig/config")
261 427
262 def GetRemote(self, name):
263 """Get the remote.$name.* configuration values as an object.
264 """
265 try:
266 r = self._remotes[name]
267 except KeyError:
268 r = Remote(self, name)
269 self._remotes[r.name] = r
270 return r
271
272 def GetBranch(self, name):
273 """Get the branch.$name.* configuration values as an object.
274 """
275 try:
276 b = self._branches[name]
277 except KeyError:
278 b = Branch(self, name)
279 self._branches[b.name] = b
280 return b
281
282 def GetSyncAnalysisStateData(self):
283 """Returns data to be logged for the analysis of sync performance."""
284 return {k: v for k, v in self.DumpConfigDict().items() if k.startswith(SYNC_STATE_PREFIX)}
285
286 def UpdateSyncAnalysisState(self, options, superproject_logging_data):
287 """Update Config's SYNC_STATE_PREFIX* data with the latest sync data.
288
289 Args:
290 options: Options passed to sync returned from optparse. See _Options().
291 superproject_logging_data: A dictionary of superproject data that is to be logged.
292
293 Returns:
294 SyncAnalysisState object.
295 """
296 return SyncAnalysisState(self, options, superproject_logging_data)
297 428
298 def GetSubSections(self, section): 429class RefSpec(object):
299 """List all subsection names matching $section.*.* 430 """A Git refspec line, split into its components:
300 """
301 return self._sections.get(section, set())
302 431
303 def HasSection(self, section, subsection=''): 432 forced: True if the line starts with '+'
304 """Does at least one key in section.subsection exist? 433 src: Left side of the line
434 dst: Right side of the line
305 """ 435 """
306 try:
307 return subsection in self._sections[section]
308 except KeyError:
309 return False
310 436
311 def UrlInsteadOf(self, url): 437 @classmethod
312 """Resolve any url.*.insteadof references. 438 def FromString(cls, rs):
313 """ 439 lhs, rhs = rs.split(":", 2)
314 for new_url in self.GetSubSections('url'): 440 if lhs.startswith("+"):
315 for old_url in self.GetString('url.%s.insteadof' % new_url, True): 441 lhs = lhs[1:]
316 if old_url is not None and url.startswith(old_url): 442 forced = True
317 return new_url + url[len(old_url):]
318 return url
319
320 @property
321 def _sections(self):
322 d = self._section_dict
323 if d is None:
324 d = {}
325 for name in self._cache.keys():
326 p = name.split('.')
327 if 2 == len(p):
328 section = p[0]
329 subsect = ''
330 else: 443 else:
331 section = p[0] 444 forced = False
332 subsect = '.'.join(p[1:-1]) 445 return cls(forced, lhs, rhs)
333 if section not in d: 446
334 d[section] = set() 447 def __init__(self, forced, lhs, rhs):
335 d[section].add(subsect) 448 self.forced = forced
336 self._section_dict = d 449 self.src = lhs
337 return d 450 self.dst = rhs
338 451
339 @property 452 def SourceMatches(self, rev):
340 def _cache(self): 453 if self.src:
341 if self._cache_dict is None: 454 if rev == self.src:
342 self._cache_dict = self._Read() 455 return True
343 return self._cache_dict 456 if self.src.endswith("/*") and rev.startswith(self.src[:-1]):
344 457 return True
345 def _Read(self): 458 return False
346 d = self._ReadJson() 459
347 if d is None: 460 def DestMatches(self, ref):
348 d = self._ReadGit() 461 if self.dst:
349 self._SaveJson(d) 462 if ref == self.dst:
350 return d 463 return True
351 464 if self.dst.endswith("/*") and ref.startswith(self.dst[:-1]):
352 def _ReadJson(self): 465 return True
353 try: 466 return False
354 if os.path.getmtime(self._json) <= os.path.getmtime(self.file): 467
355 platform_utils.remove(self._json) 468 def MapSource(self, rev):
356 return None 469 if self.src.endswith("/*"):
357 except OSError: 470 return self.dst[:-1] + rev[len(self.src) - 1 :]
358 return None 471 return self.dst
359 try: 472
360 with Trace(': parsing %s', self.file): 473 def __str__(self):
361 with open(self._json) as fd: 474 s = ""
362 return json.load(fd) 475 if self.forced:
363 except (IOError, ValueError): 476 s += "+"
364 platform_utils.remove(self._json, missing_ok=True) 477 if self.src:
365 return None 478 s += self.src
366 479 if self.dst:
367 def _SaveJson(self, cache): 480 s += ":"
368 try: 481 s += self.dst
369 with open(self._json, 'w') as fd: 482 return s
370 json.dump(cache, fd, indent=2) 483
371 except (IOError, TypeError): 484
372 platform_utils.remove(self._json, missing_ok=True) 485URI_ALL = re.compile(r"^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/")
373
374 def _ReadGit(self):
375 """
376 Read configuration data from git.
377
378 This internal method populates the GitConfig cache.
379
380 """
381 c = {}
382 if not os.path.exists(self.file):
383 return c
384
385 d = self._do('--null', '--list')
386 for line in d.rstrip('\0').split('\0'):
387 if '\n' in line:
388 key, val = line.split('\n', 1)
389 else:
390 key = line
391 val = None
392
393 if key in c:
394 c[key].append(val)
395 else:
396 c[key] = [val]
397
398 return c
399
400 def _do(self, *args):
401 if self.file == self._SYSTEM_CONFIG:
402 command = ['config', '--system', '--includes']
403 else:
404 command = ['config', '--file', self.file, '--includes']
405 command.extend(args)
406
407 p = GitCommand(None,
408 command,
409 capture_stdout=True,
410 capture_stderr=True)
411 if p.Wait() == 0:
412 return p.stdout
413 else:
414 raise GitError('git config %s: %s' % (str(args), p.stderr))
415
416
417class RepoConfig(GitConfig):
418 """User settings for repo itself."""
419
420 @staticmethod
421 def _getUserConfig():
422 repo_config_dir = os.getenv('REPO_CONFIG_DIR', os.path.expanduser('~'))
423 return os.path.join(repo_config_dir, '.repoconfig/config')
424
425
426class RefSpec(object):
427 """A Git refspec line, split into its components:
428
429 forced: True if the line starts with '+'
430 src: Left side of the line
431 dst: Right side of the line
432 """
433
434 @classmethod
435 def FromString(cls, rs):
436 lhs, rhs = rs.split(':', 2)
437 if lhs.startswith('+'):
438 lhs = lhs[1:]
439 forced = True
440 else:
441 forced = False
442 return cls(forced, lhs, rhs)
443
444 def __init__(self, forced, lhs, rhs):
445 self.forced = forced
446 self.src = lhs
447 self.dst = rhs
448
449 def SourceMatches(self, rev):
450 if self.src:
451 if rev == self.src:
452 return True
453 if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
454 return True
455 return False
456
457 def DestMatches(self, ref):
458 if self.dst:
459 if ref == self.dst:
460 return True
461 if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
462 return True
463 return False
464
465 def MapSource(self, rev):
466 if self.src.endswith('/*'):
467 return self.dst[:-1] + rev[len(self.src) - 1:]
468 return self.dst
469
470 def __str__(self):
471 s = ''
472 if self.forced:
473 s += '+'
474 if self.src:
475 s += self.src
476 if self.dst:
477 s += ':'
478 s += self.dst
479 return s
480
481
482URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
483 486
484 487
485def GetSchemeFromUrl(url): 488def GetSchemeFromUrl(url):
486 m = URI_ALL.match(url) 489 m = URI_ALL.match(url)
487 if m: 490 if m:
488 return m.group(1) 491 return m.group(1)
489 return None 492 return None
490 493
491 494
492@contextlib.contextmanager 495@contextlib.contextmanager
493def GetUrlCookieFile(url, quiet): 496def GetUrlCookieFile(url, quiet):
494 if url.startswith('persistent-'): 497 if url.startswith("persistent-"):
495 try: 498 try:
496 p = subprocess.Popen( 499 p = subprocess.Popen(
497 ['git-remote-persistent-https', '-print_config', url], 500 ["git-remote-persistent-https", "-print_config", url],
498 stdin=subprocess.PIPE, stdout=subprocess.PIPE, 501 stdin=subprocess.PIPE,
499 stderr=subprocess.PIPE) 502 stdout=subprocess.PIPE,
500 try: 503 stderr=subprocess.PIPE,
501 cookieprefix = 'http.cookiefile=' 504 )
502 proxyprefix = 'http.proxy=' 505 try:
503 cookiefile = None 506 cookieprefix = "http.cookiefile="
504 proxy = None 507 proxyprefix = "http.proxy="
505 for line in p.stdout: 508 cookiefile = None
506 line = line.strip().decode('utf-8') 509 proxy = None
507 if line.startswith(cookieprefix): 510 for line in p.stdout:
508 cookiefile = os.path.expanduser(line[len(cookieprefix):]) 511 line = line.strip().decode("utf-8")
509 if line.startswith(proxyprefix): 512 if line.startswith(cookieprefix):
510 proxy = line[len(proxyprefix):] 513 cookiefile = os.path.expanduser(
511 # Leave subprocess open, as cookie file may be transient. 514 line[len(cookieprefix) :]
512 if cookiefile or proxy: 515 )
513 yield cookiefile, proxy 516 if line.startswith(proxyprefix):
514 return 517 proxy = line[len(proxyprefix) :]
515 finally: 518 # Leave subprocess open, as cookie file may be transient.
516 p.stdin.close() 519 if cookiefile or proxy:
517 if p.wait(): 520 yield cookiefile, proxy
518 err_msg = p.stderr.read().decode('utf-8') 521 return
519 if ' -print_config' in err_msg: 522 finally:
520 pass # Persistent proxy doesn't support -print_config. 523 p.stdin.close()
521 elif not quiet: 524 if p.wait():
522 print(err_msg, file=sys.stderr) 525 err_msg = p.stderr.read().decode("utf-8")
523 except OSError as e: 526 if " -print_config" in err_msg:
524 if e.errno == errno.ENOENT: 527 pass # Persistent proxy doesn't support -print_config.
525 pass # No persistent proxy. 528 elif not quiet:
526 raise 529 print(err_msg, file=sys.stderr)
527 cookiefile = GitConfig.ForUser().GetString('http.cookiefile') 530 except OSError as e:
528 if cookiefile: 531 if e.errno == errno.ENOENT:
529 cookiefile = os.path.expanduser(cookiefile) 532 pass # No persistent proxy.
530 yield cookiefile, None 533 raise
534 cookiefile = GitConfig.ForUser().GetString("http.cookiefile")
535 if cookiefile:
536 cookiefile = os.path.expanduser(cookiefile)
537 yield cookiefile, None
531 538
532 539
533class Remote(object): 540class Remote(object):
534 """Configuration options related to a remote. 541 """Configuration options related to a remote."""
535 """ 542
536 543 def __init__(self, config, name):
537 def __init__(self, config, name): 544 self._config = config
538 self._config = config 545 self.name = name
539 self.name = name 546 self.url = self._Get("url")
540 self.url = self._Get('url') 547 self.pushUrl = self._Get("pushurl")
541 self.pushUrl = self._Get('pushurl') 548 self.review = self._Get("review")
542 self.review = self._Get('review') 549 self.projectname = self._Get("projectname")
543 self.projectname = self._Get('projectname') 550 self.fetch = list(
544 self.fetch = list(map(RefSpec.FromString, 551 map(RefSpec.FromString, self._Get("fetch", all_keys=True))
545 self._Get('fetch', all_keys=True))) 552 )
546 self._review_url = None 553 self._review_url = None
547 554
548 def _InsteadOf(self): 555 def _InsteadOf(self):
549 globCfg = GitConfig.ForUser() 556 globCfg = GitConfig.ForUser()
550 urlList = globCfg.GetSubSections('url') 557 urlList = globCfg.GetSubSections("url")
551 longest = "" 558 longest = ""
552 longestUrl = "" 559 longestUrl = ""
553 560
554 for url in urlList: 561 for url in urlList:
555 key = "url." + url + ".insteadOf" 562 key = "url." + url + ".insteadOf"
556 insteadOfList = globCfg.GetString(key, all_keys=True) 563 insteadOfList = globCfg.GetString(key, all_keys=True)
557 564
558 for insteadOf in insteadOfList: 565 for insteadOf in insteadOfList:
559 if (self.url.startswith(insteadOf) 566 if self.url.startswith(insteadOf) and len(insteadOf) > len(
560 and len(insteadOf) > len(longest)): 567 longest
561 longest = insteadOf 568 ):
562 longestUrl = url 569 longest = insteadOf
563 570 longestUrl = url
564 if len(longest) == 0: 571
565 return self.url 572 if len(longest) == 0:
566 573 return self.url
567 return self.url.replace(longest, longestUrl, 1) 574
568 575 return self.url.replace(longest, longestUrl, 1)
569 def PreConnectFetch(self, ssh_proxy): 576
570 """Run any setup for this remote before we connect to it. 577 def PreConnectFetch(self, ssh_proxy):
571 578 """Run any setup for this remote before we connect to it.
572 In practice, if the remote is using SSH, we'll attempt to create a new 579
573 SSH master session to it for reuse across projects. 580 In practice, if the remote is using SSH, we'll attempt to create a new
574 581 SSH master session to it for reuse across projects.
575 Args: 582
576 ssh_proxy: The SSH settings for managing master sessions. 583 Args:
577 584 ssh_proxy: The SSH settings for managing master sessions.
578 Returns: 585
579 Whether the preconnect phase for this remote was successful. 586 Returns:
580 """ 587 Whether the preconnect phase for this remote was successful.
581 if not ssh_proxy: 588 """
582 return True 589 if not ssh_proxy:
583 590 return True
584 connectionUrl = self._InsteadOf() 591
585 return ssh_proxy.preconnect(connectionUrl) 592 connectionUrl = self._InsteadOf()
586 593 return ssh_proxy.preconnect(connectionUrl)
587 def ReviewUrl(self, userEmail, validate_certs): 594
588 if self._review_url is None: 595 def ReviewUrl(self, userEmail, validate_certs):
589 if self.review is None: 596 if self._review_url is None:
590 return None 597 if self.review is None:
591 598 return None
592 u = self.review 599
593 if u.startswith('persistent-'): 600 u = self.review
594 u = u[len('persistent-'):] 601 if u.startswith("persistent-"):
595 if u.split(':')[0] not in ('http', 'https', 'sso', 'ssh'): 602 u = u[len("persistent-") :]
596 u = 'http://%s' % u 603 if u.split(":")[0] not in ("http", "https", "sso", "ssh"):
597 if u.endswith('/Gerrit'): 604 u = "http://%s" % u
598 u = u[:len(u) - len('/Gerrit')] 605 if u.endswith("/Gerrit"):
599 if u.endswith('/ssh_info'): 606 u = u[: len(u) - len("/Gerrit")]
600 u = u[:len(u) - len('/ssh_info')] 607 if u.endswith("/ssh_info"):
601 if not u.endswith('/'): 608 u = u[: len(u) - len("/ssh_info")]
602 u += '/' 609 if not u.endswith("/"):
603 http_url = u 610 u += "/"
604 611 http_url = u
605 if u in REVIEW_CACHE: 612
606 self._review_url = REVIEW_CACHE[u] 613 if u in REVIEW_CACHE:
607 elif 'REPO_HOST_PORT_INFO' in os.environ: 614 self._review_url = REVIEW_CACHE[u]
608 host, port = os.environ['REPO_HOST_PORT_INFO'].split() 615 elif "REPO_HOST_PORT_INFO" in os.environ:
609 self._review_url = self._SshReviewUrl(userEmail, host, port) 616 host, port = os.environ["REPO_HOST_PORT_INFO"].split()
610 REVIEW_CACHE[u] = self._review_url 617 self._review_url = self._SshReviewUrl(userEmail, host, port)
611 elif u.startswith('sso:') or u.startswith('ssh:'): 618 REVIEW_CACHE[u] = self._review_url
612 self._review_url = u # Assume it's right 619 elif u.startswith("sso:") or u.startswith("ssh:"):
613 REVIEW_CACHE[u] = self._review_url 620 self._review_url = u # Assume it's right
614 elif 'REPO_IGNORE_SSH_INFO' in os.environ: 621 REVIEW_CACHE[u] = self._review_url
615 self._review_url = http_url 622 elif "REPO_IGNORE_SSH_INFO" in os.environ:
616 REVIEW_CACHE[u] = self._review_url 623 self._review_url = http_url
617 else: 624 REVIEW_CACHE[u] = self._review_url
618 try: 625 else:
619 info_url = u + 'ssh_info' 626 try:
620 if not validate_certs: 627 info_url = u + "ssh_info"
621 context = ssl._create_unverified_context() 628 if not validate_certs:
622 info = urllib.request.urlopen(info_url, context=context).read() 629 context = ssl._create_unverified_context()
623 else: 630 info = urllib.request.urlopen(
624 info = urllib.request.urlopen(info_url).read() 631 info_url, context=context
625 if info == b'NOT_AVAILABLE' or b'<' in info: 632 ).read()
626 # If `info` contains '<', we assume the server gave us some sort 633 else:
627 # of HTML response back, like maybe a login page. 634 info = urllib.request.urlopen(info_url).read()
628 # 635 if info == b"NOT_AVAILABLE" or b"<" in info:
629 # Assume HTTP if SSH is not enabled or ssh_info doesn't look right. 636 # If `info` contains '<', we assume the server gave us
630 self._review_url = http_url 637 # some sort of HTML response back, like maybe a login
631 else: 638 # page.
632 info = info.decode('utf-8') 639 #
633 host, port = info.split() 640 # Assume HTTP if SSH is not enabled or ssh_info doesn't
634 self._review_url = self._SshReviewUrl(userEmail, host, port) 641 # look right.
635 except urllib.error.HTTPError as e: 642 self._review_url = http_url
636 raise UploadError('%s: %s' % (self.review, str(e))) 643 else:
637 except urllib.error.URLError as e: 644 info = info.decode("utf-8")
638 raise UploadError('%s: %s' % (self.review, str(e))) 645 host, port = info.split()
639 except HTTPException as e: 646 self._review_url = self._SshReviewUrl(
640 raise UploadError('%s: %s' % (self.review, e.__class__.__name__)) 647 userEmail, host, port
641 648 )
642 REVIEW_CACHE[u] = self._review_url 649 except urllib.error.HTTPError as e:
643 return self._review_url + self.projectname 650 raise UploadError("%s: %s" % (self.review, str(e)))
644 651 except urllib.error.URLError as e:
645 def _SshReviewUrl(self, userEmail, host, port): 652 raise UploadError("%s: %s" % (self.review, str(e)))
646 username = self._config.GetString('review.%s.username' % self.review) 653 except HTTPException as e:
647 if username is None: 654 raise UploadError(
648 username = userEmail.split('@')[0] 655 "%s: %s" % (self.review, e.__class__.__name__)
649 return 'ssh://%s@%s:%s/' % (username, host, port) 656 )
650 657
651 def ToLocal(self, rev): 658 REVIEW_CACHE[u] = self._review_url
652 """Convert a remote revision string to something we have locally. 659 return self._review_url + self.projectname
653 """ 660
654 if self.name == '.' or IsId(rev): 661 def _SshReviewUrl(self, userEmail, host, port):
655 return rev 662 username = self._config.GetString("review.%s.username" % self.review)
663 if username is None:
664 username = userEmail.split("@")[0]
665 return "ssh://%s@%s:%s/" % (username, host, port)
666
667 def ToLocal(self, rev):
668 """Convert a remote revision string to something we have locally."""
669 if self.name == "." or IsId(rev):
670 return rev
671
672 if not rev.startswith("refs/"):
673 rev = R_HEADS + rev
674
675 for spec in self.fetch:
676 if spec.SourceMatches(rev):
677 return spec.MapSource(rev)
678
679 if not rev.startswith(R_HEADS):
680 return rev
681
682 raise GitError(
683 "%s: remote %s does not have %s"
684 % (self.projectname, self.name, rev)
685 )
686
687 def WritesTo(self, ref):
688 """True if the remote stores to the tracking ref."""
689 for spec in self.fetch:
690 if spec.DestMatches(ref):
691 return True
692 return False
693
694 def ResetFetch(self, mirror=False):
695 """Set the fetch refspec to its default value."""
696 if mirror:
697 dst = "refs/heads/*"
698 else:
699 dst = "refs/remotes/%s/*" % self.name
700 self.fetch = [RefSpec(True, "refs/heads/*", dst)]
701
702 def Save(self):
703 """Save this remote to the configuration."""
704 self._Set("url", self.url)
705 if self.pushUrl is not None:
706 self._Set("pushurl", self.pushUrl + "/" + self.projectname)
707 else:
708 self._Set("pushurl", self.pushUrl)
709 self._Set("review", self.review)
710 self._Set("projectname", self.projectname)
711 self._Set("fetch", list(map(str, self.fetch)))
656 712
657 if not rev.startswith('refs/'): 713 def _Set(self, key, value):
658 rev = R_HEADS + rev 714 key = "remote.%s.%s" % (self.name, key)
715 return self._config.SetString(key, value)
659 716
660 for spec in self.fetch: 717 def _Get(self, key, all_keys=False):
661 if spec.SourceMatches(rev): 718 key = "remote.%s.%s" % (self.name, key)
662 return spec.MapSource(rev) 719 return self._config.GetString(key, all_keys=all_keys)
663 720
664 if not rev.startswith(R_HEADS):
665 return rev
666 721
667 raise GitError('%s: remote %s does not have %s' % 722class Branch(object):
668 (self.projectname, self.name, rev)) 723 """Configuration options related to a single branch."""
669 724
670 def WritesTo(self, ref): 725 def __init__(self, config, name):
671 """True if the remote stores to the tracking ref. 726 self._config = config
672 """ 727 self.name = name
673 for spec in self.fetch: 728 self.merge = self._Get("merge")
674 if spec.DestMatches(ref):
675 return True
676 return False
677 729
678 def ResetFetch(self, mirror=False): 730 r = self._Get("remote")
679 """Set the fetch refspec to its default value. 731 if r:
680 """ 732 self.remote = self._config.GetRemote(r)
681 if mirror: 733 else:
682 dst = 'refs/heads/*' 734 self.remote = None
683 else:
684 dst = 'refs/remotes/%s/*' % self.name
685 self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
686
687 def Save(self):
688 """Save this remote to the configuration.
689 """
690 self._Set('url', self.url)
691 if self.pushUrl is not None:
692 self._Set('pushurl', self.pushUrl + '/' + self.projectname)
693 else:
694 self._Set('pushurl', self.pushUrl)
695 self._Set('review', self.review)
696 self._Set('projectname', self.projectname)
697 self._Set('fetch', list(map(str, self.fetch)))
698 735
699 def _Set(self, key, value): 736 @property
700 key = 'remote.%s.%s' % (self.name, key) 737 def LocalMerge(self):
701 return self._config.SetString(key, value) 738 """Convert the merge spec to a local name."""
739 if self.remote and self.merge:
740 return self.remote.ToLocal(self.merge)
741 return None
702 742
703 def _Get(self, key, all_keys=False): 743 def Save(self):
704 key = 'remote.%s.%s' % (self.name, key) 744 """Save this branch back into the configuration."""
705 return self._config.GetString(key, all_keys=all_keys) 745 if self._config.HasSection("branch", self.name):
746 if self.remote:
747 self._Set("remote", self.remote.name)
748 else:
749 self._Set("remote", None)
750 self._Set("merge", self.merge)
706 751
752 else:
753 with open(self._config.file, "a") as fd:
754 fd.write('[branch "%s"]\n' % self.name)
755 if self.remote:
756 fd.write("\tremote = %s\n" % self.remote.name)
757 if self.merge:
758 fd.write("\tmerge = %s\n" % self.merge)
707 759
708class Branch(object): 760 def _Set(self, key, value):
709 """Configuration options related to a single branch. 761 key = "branch.%s.%s" % (self.name, key)
710 """ 762 return self._config.SetString(key, value)
711
712 def __init__(self, config, name):
713 self._config = config
714 self.name = name
715 self.merge = self._Get('merge')
716
717 r = self._Get('remote')
718 if r:
719 self.remote = self._config.GetRemote(r)
720 else:
721 self.remote = None
722
723 @property
724 def LocalMerge(self):
725 """Convert the merge spec to a local name.
726 """
727 if self.remote and self.merge:
728 return self.remote.ToLocal(self.merge)
729 return None
730 763
731 def Save(self): 764 def _Get(self, key, all_keys=False):
732 """Save this branch back into the configuration. 765 key = "branch.%s.%s" % (self.name, key)
733 """ 766 return self._config.GetString(key, all_keys=all_keys)
734 if self._config.HasSection('branch', self.name):
735 if self.remote:
736 self._Set('remote', self.remote.name)
737 else:
738 self._Set('remote', None)
739 self._Set('merge', self.merge)
740
741 else:
742 with open(self._config.file, 'a') as fd:
743 fd.write('[branch "%s"]\n' % self.name)
744 if self.remote:
745 fd.write('\tremote = %s\n' % self.remote.name)
746 if self.merge:
747 fd.write('\tmerge = %s\n' % self.merge)
748
749 def _Set(self, key, value):
750 key = 'branch.%s.%s' % (self.name, key)
751 return self._config.SetString(key, value)
752
753 def _Get(self, key, all_keys=False):
754 key = 'branch.%s.%s' % (self.name, key)
755 return self._config.GetString(key, all_keys=all_keys)
756 767
757 768
758class SyncAnalysisState: 769class SyncAnalysisState:
759 """Configuration options related to logging of sync state for analysis. 770 """Configuration options related to logging of sync state for analysis.
760
761 This object is versioned.
762 """
763 def __init__(self, config, options, superproject_logging_data):
764 """Initializes SyncAnalysisState.
765
766 Saves the following data into the |config| object.
767 - sys.argv, options, superproject's logging data.
768 - repo.*, branch.* and remote.* parameters from config object.
769 - Current time as synctime.
770 - Version number of the object.
771 771
772 All the keys saved by this object are prepended with SYNC_STATE_PREFIX. 772 This object is versioned.
773
774 Args:
775 config: GitConfig object to store all options.
776 options: Options passed to sync returned from optparse. See _Options().
777 superproject_logging_data: A dictionary of superproject data that is to be logged.
778 """
779 self._config = config
780 now = datetime.datetime.utcnow()
781 self._Set('main.synctime', now.isoformat() + 'Z')
782 self._Set('main.version', '1')
783 self._Set('sys.argv', sys.argv)
784 for key, value in superproject_logging_data.items():
785 self._Set(f'superproject.{key}', value)
786 for key, value in options.__dict__.items():
787 self._Set(f'options.{key}', value)
788 config_items = config.DumpConfigDict().items()
789 EXTRACT_NAMESPACES = {'repo', 'branch', 'remote'}
790 self._SetDictionary({k: v for k, v in config_items
791 if not k.startswith(SYNC_STATE_PREFIX) and
792 k.split('.', 1)[0] in EXTRACT_NAMESPACES})
793
794 def _SetDictionary(self, data):
795 """Save all key/value pairs of |data| dictionary.
796
797 Args:
798 data: A dictionary whose key/value are to be saved.
799 """ 773 """
800 for key, value in data.items():
801 self._Set(key, value)
802 774
803 def _Set(self, key, value): 775 def __init__(self, config, options, superproject_logging_data):
804 """Set the |value| for a |key| in the |_config| member. 776 """Initializes SyncAnalysisState.
805 777
806 |key| is prepended with the value of SYNC_STATE_PREFIX constant. 778 Saves the following data into the |config| object.
807 779 - sys.argv, options, superproject's logging data.
808 Args: 780 - repo.*, branch.* and remote.* parameters from config object.
809 key: Name of the key. 781 - Current time as synctime.
810 value: |value| could be of any type. If it is 'bool', it will be saved 782 - Version number of the object.
811 as a Boolean and for all other types, it will be saved as a String. 783
812 """ 784 All the keys saved by this object are prepended with SYNC_STATE_PREFIX.
813 if value is None: 785
814 return 786 Args:
815 sync_key = f'{SYNC_STATE_PREFIX}{key}' 787 config: GitConfig object to store all options.
816 sync_key = sync_key.replace('_', '') 788 options: Options passed to sync returned from optparse. See
817 if isinstance(value, str): 789 _Options().
818 self._config.SetString(sync_key, value) 790 superproject_logging_data: A dictionary of superproject data that is
819 elif isinstance(value, bool): 791 to be logged.
820 self._config.SetBoolean(sync_key, value) 792 """
821 else: 793 self._config = config
822 self._config.SetString(sync_key, str(value)) 794 now = datetime.datetime.utcnow()
795 self._Set("main.synctime", now.isoformat() + "Z")
796 self._Set("main.version", "1")
797 self._Set("sys.argv", sys.argv)
798 for key, value in superproject_logging_data.items():
799 self._Set(f"superproject.{key}", value)
800 for key, value in options.__dict__.items():
801 self._Set(f"options.{key}", value)
802 config_items = config.DumpConfigDict().items()
803 EXTRACT_NAMESPACES = {"repo", "branch", "remote"}
804 self._SetDictionary(
805 {
806 k: v
807 for k, v in config_items
808 if not k.startswith(SYNC_STATE_PREFIX)
809 and k.split(".", 1)[0] in EXTRACT_NAMESPACES
810 }
811 )
812
813 def _SetDictionary(self, data):
814 """Save all key/value pairs of |data| dictionary.
815
816 Args:
817 data: A dictionary whose key/value are to be saved.
818 """
819 for key, value in data.items():
820 self._Set(key, value)
821
822 def _Set(self, key, value):
823 """Set the |value| for a |key| in the |_config| member.
824
825 |key| is prepended with the value of SYNC_STATE_PREFIX constant.
826
827 Args:
828 key: Name of the key.
829 value: |value| could be of any type. If it is 'bool', it will be
830 saved as a Boolean and for all other types, it will be saved as
831 a String.
832 """
833 if value is None:
834 return
835 sync_key = f"{SYNC_STATE_PREFIX}{key}"
836 sync_key = sync_key.replace("_", "")
837 if isinstance(value, str):
838 self._config.SetString(sync_key, value)
839 elif isinstance(value, bool):
840 self._config.SetBoolean(sync_key, value)
841 else:
842 self._config.SetString(sync_key, str(value))