blob: 87ed97656d2b7a3fe47632bacdf7c78a9fa2e31c [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Dan Willemsen0745bb22015-08-17 13:41:45 -070015import contextlib
Raman Tenneti7954de12021-07-28 14:36:49 -070016import datetime
Dan Willemsen0745bb22015-08-17 13:41:45 -070017import errno
Mike Frysingeracf63b22019-06-13 02:24:21 -040018from http.client import HTTPException
Anthony King85b24ac2014-05-06 15:57:48 +010019import json
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070020import os
21import re
Łukasz Gardońbed59ce2017-08-08 10:18:11 +020022import ssl
Shawn O. Pearcefb231612009-04-10 18:53:46 -070023import subprocess
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070024import sys
Joanna Wangea5239d2022-12-02 09:47:29 -050025from typing import Union
Mike Frysingeracf63b22019-06-13 02:24:21 -040026import urllib.error
27import urllib.request
Shawn O. Pearcef00e0ce2009-08-22 18:39:49 -070028
Shawn O. Pearceb54a3922009-01-05 16:18:58 -080029from error import GitError, UploadError
Renaud Paquay010fed72016-11-11 14:25:29 -080030import platform_utils
Mike Frysinger8a11f6f2019-08-27 00:26:15 -040031from repo_trace import Trace
Shawn O. Pearceca8c32c2010-05-11 18:21:33 -070032from git_command import GitCommand
Zac Livingston9ead97b2017-06-13 08:29:04 -060033from git_refs import R_CHANGES, R_HEADS, R_TAGS
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070034
Raman Tenneti7954de12021-07-28 14:36:49 -070035# Prefix that is prepended to all the keys of SyncAnalysisState's data
36# that is saved in the config.
Gavin Makea2e3302023-03-11 06:46:20 +000037SYNC_STATE_PREFIX = "repo.syncstate."
Raman Tenneti7954de12021-07-28 14:36:49 -070038
Gavin Makea2e3302023-03-11 06:46:20 +000039ID_RE = re.compile(r"^[0-9a-f]{40}$")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070040
Shawn O. Pearce146fe902009-03-25 14:06:43 -070041REVIEW_CACHE = dict()
42
David Pursehouse819827a2020-02-12 15:20:19 +090043
Zac Livingston9ead97b2017-06-13 08:29:04 -060044def IsChange(rev):
Gavin Makea2e3302023-03-11 06:46:20 +000045 return rev.startswith(R_CHANGES)
Zac Livingston9ead97b2017-06-13 08:29:04 -060046
David Pursehouse819827a2020-02-12 15:20:19 +090047
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070048def IsId(rev):
Gavin Makea2e3302023-03-11 06:46:20 +000049 return ID_RE.match(rev)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070050
David Pursehouse819827a2020-02-12 15:20:19 +090051
Zac Livingston9ead97b2017-06-13 08:29:04 -060052def IsTag(rev):
Gavin Makea2e3302023-03-11 06:46:20 +000053 return rev.startswith(R_TAGS)
Zac Livingston9ead97b2017-06-13 08:29:04 -060054
David Pursehouse819827a2020-02-12 15:20:19 +090055
Zac Livingston9ead97b2017-06-13 08:29:04 -060056def IsImmutable(rev):
57 return IsChange(rev) or IsId(rev) or IsTag(rev)
58
David Pursehouse819827a2020-02-12 15:20:19 +090059
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070060def _key(name):
Gavin Makea2e3302023-03-11 06:46:20 +000061 parts = name.split(".")
62 if len(parts) < 2:
63 return name.lower()
64 parts[0] = parts[0].lower()
65 parts[-1] = parts[-1].lower()
66 return ".".join(parts)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070067
David Pursehouse819827a2020-02-12 15:20:19 +090068
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070069class GitConfig(object):
Gavin Makea2e3302023-03-11 06:46:20 +000070 _ForUser = None
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070071
Gavin Makea2e3302023-03-11 06:46:20 +000072 _ForSystem = None
73 _SYSTEM_CONFIG = "/etc/gitconfig"
Xin Li0cb6e922021-06-16 10:19:00 -070074
Gavin Makea2e3302023-03-11 06:46:20 +000075 @classmethod
76 def ForSystem(cls):
77 if cls._ForSystem is None:
78 cls._ForSystem = cls(configfile=cls._SYSTEM_CONFIG)
79 return cls._ForSystem
Xin Li0cb6e922021-06-16 10:19:00 -070080
Gavin Makea2e3302023-03-11 06:46:20 +000081 @classmethod
82 def ForUser(cls):
83 if cls._ForUser is None:
84 cls._ForUser = cls(configfile=cls._getUserConfig())
85 return cls._ForUser
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070086
Gavin Makea2e3302023-03-11 06:46:20 +000087 @staticmethod
88 def _getUserConfig():
89 return os.path.expanduser("~/.gitconfig")
Gavin Mak7e3b65b2023-01-26 23:27:51 +000090
Gavin Makea2e3302023-03-11 06:46:20 +000091 @classmethod
92 def ForRepository(cls, gitdir, defaults=None):
93 return cls(configfile=os.path.join(gitdir, "config"), defaults=defaults)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070094
Gavin Makea2e3302023-03-11 06:46:20 +000095 def __init__(self, configfile, defaults=None, jsonFile=None):
96 self.file = configfile
97 self.defaults = defaults
98 self._cache_dict = None
99 self._section_dict = None
100 self._remotes = {}
101 self._branches = {}
Shawn O. Pearce1b34c912009-05-21 18:52:49 -0700102
Gavin Makea2e3302023-03-11 06:46:20 +0000103 self._json = jsonFile
104 if self._json is None:
105 self._json = os.path.join(
106 os.path.dirname(self.file),
107 ".repo_" + os.path.basename(self.file) + ".json",
108 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700109
Gavin Makea2e3302023-03-11 06:46:20 +0000110 def ClearCache(self):
111 """Clear the in-memory cache of config."""
112 self._cache_dict = None
Jack Neusc474c9c2021-07-26 23:08:54 +0000113
Gavin Makea2e3302023-03-11 06:46:20 +0000114 def Has(self, name, include_defaults=True):
115 """Return true if this configuration file has the key."""
116 if _key(name) in self._cache:
117 return True
118 if include_defaults and self.defaults:
119 return self.defaults.Has(name, include_defaults=True)
120 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700121
Gavin Makea2e3302023-03-11 06:46:20 +0000122 def GetInt(self, name: str) -> Union[int, None]:
123 """Returns an integer from the configuration file.
Mike Frysinger77b43972020-02-19 17:55:22 -0500124
Gavin Makea2e3302023-03-11 06:46:20 +0000125 This follows the git config syntax.
Mike Frysinger77b43972020-02-19 17:55:22 -0500126
Gavin Makea2e3302023-03-11 06:46:20 +0000127 Args:
128 name: The key to lookup.
Mike Frysinger77b43972020-02-19 17:55:22 -0500129
Gavin Makea2e3302023-03-11 06:46:20 +0000130 Returns:
131 None if the value was not defined, or is not an int.
132 Otherwise, the number itself.
133 """
134 v = self.GetString(name)
135 if v is None:
136 return None
137 v = v.strip()
Mike Frysinger77b43972020-02-19 17:55:22 -0500138
Gavin Makea2e3302023-03-11 06:46:20 +0000139 mult = 1
140 if v.endswith("k"):
141 v = v[:-1]
142 mult = 1024
143 elif v.endswith("m"):
144 v = v[:-1]
145 mult = 1024 * 1024
146 elif v.endswith("g"):
147 v = v[:-1]
148 mult = 1024 * 1024 * 1024
Mike Frysinger77b43972020-02-19 17:55:22 -0500149
Gavin Makea2e3302023-03-11 06:46:20 +0000150 base = 10
151 if v.startswith("0x"):
152 base = 16
Mike Frysinger77b43972020-02-19 17:55:22 -0500153
Gavin Makea2e3302023-03-11 06:46:20 +0000154 try:
155 return int(v, base=base) * mult
156 except ValueError:
157 print(
158 f"warning: expected {name} to represent an integer, got {v} "
159 "instead",
160 file=sys.stderr,
161 )
162 return None
Mike Frysinger77b43972020-02-19 17:55:22 -0500163
Gavin Makea2e3302023-03-11 06:46:20 +0000164 def DumpConfigDict(self):
165 """Returns the current configuration dict.
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800166
Gavin Makea2e3302023-03-11 06:46:20 +0000167 Configuration data is information only (e.g. logging) and
168 should not be considered a stable data-source.
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800169
Gavin Makea2e3302023-03-11 06:46:20 +0000170 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
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800178
Gavin Makea2e3302023-03-11 06:46:20 +0000179 def GetBoolean(self, name: str) -> Union[str, None]:
180 """Returns a boolean from the configuration file.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700181
Gavin Makea2e3302023-03-11 06:46:20 +0000182 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 )
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700199 return None
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700200
Gavin Makea2e3302023-03-11 06:46:20 +0000201 def SetBoolean(self, name, value):
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)
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700206
Gavin Makea2e3302023-03-11 06:46:20 +0000207 def GetString(self, name: str, all_keys: bool = False) -> Union[str, None]:
208 """Get the first value for a key, or None if it is not defined.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700209
Gavin Makea2e3302023-03-11 06:46:20 +0000210 This configuration file is used first, if the key is not
211 defined or all_keys = True then the defaults are also searched.
212 """
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 = []
David Aguilar438c5472009-06-28 15:09:16 -0700219
Gavin Makea2e3302023-03-11 06:46:20 +0000220 if not all_keys:
221 if v:
222 return v[0]
223 return None
Mike Frysingerf88282c2021-09-28 15:59:40 -0400224
Gavin Makea2e3302023-03-11 06:46:20 +0000225 r = []
226 r.extend(v)
227 if self.defaults:
228 r.extend(self.defaults.GetString(name, all_keys=True))
229 return r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700230
Gavin Makea2e3302023-03-11 06:46:20 +0000231 def SetString(self, name, value):
232 """Set the value(s) for a key.
233 Only this configuration file is modified.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700234
Gavin Makea2e3302023-03-11 06:46:20 +0000235 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)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700239
Gavin Makea2e3302023-03-11 06:46:20 +0000240 try:
241 old = self._cache[key]
242 except KeyError:
243 old = []
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700244
Gavin Makea2e3302023-03-11 06:46:20 +0000245 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)
412
413 p = GitCommand(None, command, capture_stdout=True, capture_stderr=True)
414 if p.Wait() == 0:
415 return p.stdout
416 else:
417 raise GitError("git config %s: %s" % (str(args), p.stderr))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700418
419
Mike Frysingerf841ca42020-02-18 21:31:51 -0500420class RepoConfig(GitConfig):
Gavin Makea2e3302023-03-11 06:46:20 +0000421 """User settings for repo itself."""
Mike Frysingerf841ca42020-02-18 21:31:51 -0500422
Gavin Makea2e3302023-03-11 06:46:20 +0000423 @staticmethod
424 def _getUserConfig():
425 repo_config_dir = os.getenv("REPO_CONFIG_DIR", os.path.expanduser("~"))
426 return os.path.join(repo_config_dir, ".repoconfig/config")
Mike Frysingerf841ca42020-02-18 21:31:51 -0500427
428
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700429class RefSpec(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000430 """A Git refspec line, split into its components:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700431
Gavin Makea2e3302023-03-11 06:46:20 +0000432 forced: True if the line starts with '+'
433 src: Left side of the line
434 dst: Right side of the line
435 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700436
Gavin Makea2e3302023-03-11 06:46:20 +0000437 @classmethod
438 def FromString(cls, rs):
439 lhs, rhs = rs.split(":", 2)
440 if lhs.startswith("+"):
441 lhs = lhs[1:]
442 forced = True
443 else:
444 forced = False
445 return cls(forced, lhs, rhs)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700446
Gavin Makea2e3302023-03-11 06:46:20 +0000447 def __init__(self, forced, lhs, rhs):
448 self.forced = forced
449 self.src = lhs
450 self.dst = rhs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700451
Gavin Makea2e3302023-03-11 06:46:20 +0000452 def SourceMatches(self, rev):
453 if self.src:
454 if rev == self.src:
455 return True
456 if self.src.endswith("/*") and rev.startswith(self.src[:-1]):
457 return True
458 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700459
Gavin Makea2e3302023-03-11 06:46:20 +0000460 def DestMatches(self, ref):
461 if self.dst:
462 if ref == self.dst:
463 return True
464 if self.dst.endswith("/*") and ref.startswith(self.dst[:-1]):
465 return True
466 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700467
Gavin Makea2e3302023-03-11 06:46:20 +0000468 def MapSource(self, rev):
469 if self.src.endswith("/*"):
470 return self.dst[:-1] + rev[len(self.src) - 1 :]
471 return self.dst
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700472
Gavin Makea2e3302023-03-11 06:46:20 +0000473 def __str__(self):
474 s = ""
475 if self.forced:
476 s += "+"
477 if self.src:
478 s += self.src
479 if self.dst:
480 s += ":"
481 s += self.dst
482 return s
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700483
484
Gavin Makea2e3302023-03-11 06:46:20 +0000485URI_ALL = re.compile(r"^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/")
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700486
David Pursehouse819827a2020-02-12 15:20:19 +0900487
Shawn O. Pearcef322b9a2011-09-19 14:50:58 -0700488def GetSchemeFromUrl(url):
Gavin Makea2e3302023-03-11 06:46:20 +0000489 m = URI_ALL.match(url)
490 if m:
491 return m.group(1)
492 return None
Shawn O. Pearcef322b9a2011-09-19 14:50:58 -0700493
David Pursehouse819827a2020-02-12 15:20:19 +0900494
Dan Willemsen0745bb22015-08-17 13:41:45 -0700495@contextlib.contextmanager
496def GetUrlCookieFile(url, quiet):
Gavin Makea2e3302023-03-11 06:46:20 +0000497 if url.startswith("persistent-"):
498 try:
499 p = subprocess.Popen(
500 ["git-remote-persistent-https", "-print_config", url],
501 stdin=subprocess.PIPE,
502 stdout=subprocess.PIPE,
503 stderr=subprocess.PIPE,
504 )
505 try:
506 cookieprefix = "http.cookiefile="
507 proxyprefix = "http.proxy="
508 cookiefile = None
509 proxy = None
510 for line in p.stdout:
511 line = line.strip().decode("utf-8")
512 if line.startswith(cookieprefix):
513 cookiefile = os.path.expanduser(
514 line[len(cookieprefix) :]
515 )
516 if line.startswith(proxyprefix):
517 proxy = line[len(proxyprefix) :]
518 # Leave subprocess open, as cookie file may be transient.
519 if cookiefile or proxy:
520 yield cookiefile, proxy
521 return
522 finally:
523 p.stdin.close()
524 if p.wait():
525 err_msg = p.stderr.read().decode("utf-8")
526 if " -print_config" in err_msg:
527 pass # Persistent proxy doesn't support -print_config.
528 elif not quiet:
529 print(err_msg, file=sys.stderr)
530 except OSError as e:
531 if e.errno == errno.ENOENT:
532 pass # No persistent proxy.
533 raise
534 cookiefile = GitConfig.ForUser().GetString("http.cookiefile")
535 if cookiefile:
536 cookiefile = os.path.expanduser(cookiefile)
537 yield cookiefile, None
Dan Willemsen0745bb22015-08-17 13:41:45 -0700538
David Pursehouse819827a2020-02-12 15:20:19 +0900539
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700540class Remote(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000541 """Configuration options related to a remote."""
David Pursehouse819827a2020-02-12 15:20:19 +0900542
Gavin Makea2e3302023-03-11 06:46:20 +0000543 def __init__(self, config, name):
544 self._config = config
545 self.name = name
546 self.url = self._Get("url")
547 self.pushUrl = self._Get("pushurl")
548 self.review = self._Get("review")
549 self.projectname = self._Get("projectname")
550 self.fetch = list(
551 map(RefSpec.FromString, self._Get("fetch", all_keys=True))
552 )
553 self._review_url = None
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800554
Gavin Makea2e3302023-03-11 06:46:20 +0000555 def _InsteadOf(self):
556 globCfg = GitConfig.ForUser()
557 urlList = globCfg.GetSubSections("url")
558 longest = ""
559 longestUrl = ""
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100560
Gavin Makea2e3302023-03-11 06:46:20 +0000561 for url in urlList:
562 key = "url." + url + ".insteadOf"
563 insteadOfList = globCfg.GetString(key, all_keys=True)
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100564
Gavin Makea2e3302023-03-11 06:46:20 +0000565 for insteadOf in insteadOfList:
566 if self.url.startswith(insteadOf) and len(insteadOf) > len(
567 longest
568 ):
569 longest = insteadOf
570 longestUrl = url
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100571
Gavin Makea2e3302023-03-11 06:46:20 +0000572 if len(longest) == 0:
573 return self.url
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100574
Gavin Makea2e3302023-03-11 06:46:20 +0000575 return self.url.replace(longest, longestUrl, 1)
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100576
Gavin Makea2e3302023-03-11 06:46:20 +0000577 def PreConnectFetch(self, ssh_proxy):
578 """Run any setup for this remote before we connect to it.
Mike Frysinger19e409c2021-05-05 19:44:35 -0400579
Gavin Makea2e3302023-03-11 06:46:20 +0000580 In practice, if the remote is using SSH, we'll attempt to create a new
581 SSH master session to it for reuse across projects.
Mike Frysinger19e409c2021-05-05 19:44:35 -0400582
Gavin Makea2e3302023-03-11 06:46:20 +0000583 Args:
584 ssh_proxy: The SSH settings for managing master sessions.
Mike Frysinger339f2df2021-05-06 00:44:42 -0400585
Gavin Makea2e3302023-03-11 06:46:20 +0000586 Returns:
587 Whether the preconnect phase for this remote was successful.
588 """
589 if not ssh_proxy:
590 return True
Mike Frysinger339f2df2021-05-06 00:44:42 -0400591
Gavin Makea2e3302023-03-11 06:46:20 +0000592 connectionUrl = self._InsteadOf()
593 return ssh_proxy.preconnect(connectionUrl)
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700594
Gavin Makea2e3302023-03-11 06:46:20 +0000595 def ReviewUrl(self, userEmail, validate_certs):
596 if self._review_url is None:
597 if self.review is None:
598 return None
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800599
Gavin Makea2e3302023-03-11 06:46:20 +0000600 u = self.review
601 if u.startswith("persistent-"):
602 u = u[len("persistent-") :]
603 if u.split(":")[0] not in ("http", "https", "sso", "ssh"):
604 u = "http://%s" % u
605 if u.endswith("/Gerrit"):
606 u = u[: len(u) - len("/Gerrit")]
607 if u.endswith("/ssh_info"):
608 u = u[: len(u) - len("/ssh_info")]
609 if not u.endswith("/"):
610 u += "/"
611 http_url = u
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800612
Gavin Makea2e3302023-03-11 06:46:20 +0000613 if u in REVIEW_CACHE:
614 self._review_url = REVIEW_CACHE[u]
615 elif "REPO_HOST_PORT_INFO" in os.environ:
616 host, port = os.environ["REPO_HOST_PORT_INFO"].split()
617 self._review_url = self._SshReviewUrl(userEmail, host, port)
618 REVIEW_CACHE[u] = self._review_url
619 elif u.startswith("sso:") or u.startswith("ssh:"):
620 self._review_url = u # Assume it's right
621 REVIEW_CACHE[u] = self._review_url
622 elif "REPO_IGNORE_SSH_INFO" in os.environ:
623 self._review_url = http_url
624 REVIEW_CACHE[u] = self._review_url
625 else:
626 try:
627 info_url = u + "ssh_info"
628 if not validate_certs:
629 context = ssl._create_unverified_context()
630 info = urllib.request.urlopen(
631 info_url, context=context
632 ).read()
633 else:
634 info = urllib.request.urlopen(info_url).read()
635 if info == b"NOT_AVAILABLE" or b"<" in info:
636 # If `info` contains '<', we assume the server gave us
637 # some sort of HTML response back, like maybe a login
638 # page.
639 #
640 # Assume HTTP if SSH is not enabled or ssh_info doesn't
641 # look right.
642 self._review_url = http_url
643 else:
644 info = info.decode("utf-8")
645 host, port = info.split()
646 self._review_url = self._SshReviewUrl(
647 userEmail, host, port
648 )
649 except urllib.error.HTTPError as e:
650 raise UploadError("%s: %s" % (self.review, str(e)))
651 except urllib.error.URLError as e:
652 raise UploadError("%s: %s" % (self.review, str(e)))
653 except HTTPException as e:
654 raise UploadError(
655 "%s: %s" % (self.review, e.__class__.__name__)
656 )
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800657
Gavin Makea2e3302023-03-11 06:46:20 +0000658 REVIEW_CACHE[u] = self._review_url
659 return self._review_url + self.projectname
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800660
Gavin Makea2e3302023-03-11 06:46:20 +0000661 def _SshReviewUrl(self, userEmail, host, port):
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)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700666
Gavin Makea2e3302023-03-11 06:46:20 +0000667 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
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700671
Gavin Makea2e3302023-03-11 06:46:20 +0000672 if not rev.startswith("refs/"):
673 rev = R_HEADS + rev
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700674
Gavin Makea2e3302023-03-11 06:46:20 +0000675 for spec in self.fetch:
676 if spec.SourceMatches(rev):
677 return spec.MapSource(rev)
Nasser Grainawi909d58b2014-09-19 12:13:04 -0600678
Gavin Makea2e3302023-03-11 06:46:20 +0000679 if not rev.startswith(R_HEADS):
680 return rev
Nasser Grainawi909d58b2014-09-19 12:13:04 -0600681
Gavin Makea2e3302023-03-11 06:46:20 +0000682 raise GitError(
683 "%s: remote %s does not have %s"
684 % (self.projectname, self.name, rev)
685 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700686
Gavin Makea2e3302023-03-11 06:46:20 +0000687 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
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700693
Gavin Makea2e3302023-03-11 06:46:20 +0000694 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)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700701
Gavin Makea2e3302023-03-11 06:46:20 +0000702 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)))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700712
Gavin Makea2e3302023-03-11 06:46:20 +0000713 def _Set(self, key, value):
714 key = "remote.%s.%s" % (self.name, key)
715 return self._config.SetString(key, value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700716
Gavin Makea2e3302023-03-11 06:46:20 +0000717 def _Get(self, key, all_keys=False):
718 key = "remote.%s.%s" % (self.name, key)
719 return self._config.GetString(key, all_keys=all_keys)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700720
721
722class Branch(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000723 """Configuration options related to a single branch."""
David Pursehouse819827a2020-02-12 15:20:19 +0900724
Gavin Makea2e3302023-03-11 06:46:20 +0000725 def __init__(self, config, name):
726 self._config = config
727 self.name = name
728 self.merge = self._Get("merge")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700729
Gavin Makea2e3302023-03-11 06:46:20 +0000730 r = self._Get("remote")
731 if r:
732 self.remote = self._config.GetRemote(r)
733 else:
734 self.remote = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700735
Gavin Makea2e3302023-03-11 06:46:20 +0000736 @property
737 def LocalMerge(self):
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
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700742
Gavin Makea2e3302023-03-11 06:46:20 +0000743 def Save(self):
744 """Save this branch back into the configuration."""
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)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700751
Gavin Makea2e3302023-03-11 06:46:20 +0000752 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)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700759
Gavin Makea2e3302023-03-11 06:46:20 +0000760 def _Set(self, key, value):
761 key = "branch.%s.%s" % (self.name, key)
762 return self._config.SetString(key, value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700763
Gavin Makea2e3302023-03-11 06:46:20 +0000764 def _Get(self, key, all_keys=False):
765 key = "branch.%s.%s" % (self.name, key)
766 return self._config.GetString(key, all_keys=all_keys)
Raman Tenneti7954de12021-07-28 14:36:49 -0700767
768
769class SyncAnalysisState:
Gavin Makea2e3302023-03-11 06:46:20 +0000770 """Configuration options related to logging of sync state for analysis.
Raman Tenneti7954de12021-07-28 14:36:49 -0700771
Gavin Makea2e3302023-03-11 06:46:20 +0000772 This object is versioned.
Raman Tenneti7954de12021-07-28 14:36:49 -0700773 """
Raman Tenneti7954de12021-07-28 14:36:49 -0700774
Gavin Makea2e3302023-03-11 06:46:20 +0000775 def __init__(self, config, options, superproject_logging_data):
776 """Initializes SyncAnalysisState.
Raman Tenneti7954de12021-07-28 14:36:49 -0700777
Gavin Makea2e3302023-03-11 06:46:20 +0000778 Saves the following data into the |config| object.
779 - sys.argv, options, superproject's logging data.
780 - repo.*, branch.* and remote.* parameters from config object.
781 - Current time as synctime.
782 - Version number of the object.
Raman Tenneti7954de12021-07-28 14:36:49 -0700783
Gavin Makea2e3302023-03-11 06:46:20 +0000784 All the keys saved by this object are prepended with SYNC_STATE_PREFIX.
Raman Tenneti7954de12021-07-28 14:36:49 -0700785
Gavin Makea2e3302023-03-11 06:46:20 +0000786 Args:
787 config: GitConfig object to store all options.
788 options: Options passed to sync returned from optparse. See
789 _Options().
790 superproject_logging_data: A dictionary of superproject data that is
791 to be logged.
792 """
793 self._config = config
794 now = datetime.datetime.utcnow()
Josip Sokcevic7ef5b462023-04-27 10:45:04 -0700795 self._Set("main.synctime", now.isoformat(timespec="microseconds") + "Z")
Gavin Makea2e3302023-03-11 06:46:20 +0000796 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 )
Raman Tenneti7954de12021-07-28 14:36:49 -0700812
Gavin Makea2e3302023-03-11 06:46:20 +0000813 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))