blob: 68016fffde685f093719bad8db82f7184e8734f8 [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 Frysinger06ddc8c2023-08-21 21:26:51 -040018import http.client
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
Mike Frysinger64477332023-08-21 21:20:32 -040029from error import GitError
30from error import UploadError
31from git_command import GitCommand
32from git_refs import R_CHANGES
33from git_refs import R_HEADS
34from git_refs import R_TAGS
Renaud Paquay010fed72016-11-11 14:25:29 -080035import platform_utils
Mike Frysinger8a11f6f2019-08-27 00:26:15 -040036from repo_trace import Trace
Mike Frysinger64477332023-08-21 21:20:32 -040037
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070038
Raman Tenneti7954de12021-07-28 14:36:49 -070039# Prefix that is prepended to all the keys of SyncAnalysisState's data
40# that is saved in the config.
Gavin Makea2e3302023-03-11 06:46:20 +000041SYNC_STATE_PREFIX = "repo.syncstate."
Raman Tenneti7954de12021-07-28 14:36:49 -070042
Gavin Makea2e3302023-03-11 06:46:20 +000043ID_RE = re.compile(r"^[0-9a-f]{40}$")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070044
Shawn O. Pearce146fe902009-03-25 14:06:43 -070045REVIEW_CACHE = dict()
46
David Pursehouse819827a2020-02-12 15:20:19 +090047
Zac Livingston9ead97b2017-06-13 08:29:04 -060048def IsChange(rev):
Gavin Makea2e3302023-03-11 06:46:20 +000049 return rev.startswith(R_CHANGES)
Zac Livingston9ead97b2017-06-13 08:29:04 -060050
David Pursehouse819827a2020-02-12 15:20:19 +090051
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070052def IsId(rev):
Gavin Makea2e3302023-03-11 06:46:20 +000053 return ID_RE.match(rev)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070054
David Pursehouse819827a2020-02-12 15:20:19 +090055
Zac Livingston9ead97b2017-06-13 08:29:04 -060056def IsTag(rev):
Gavin Makea2e3302023-03-11 06:46:20 +000057 return rev.startswith(R_TAGS)
Zac Livingston9ead97b2017-06-13 08:29:04 -060058
David Pursehouse819827a2020-02-12 15:20:19 +090059
Zac Livingston9ead97b2017-06-13 08:29:04 -060060def IsImmutable(rev):
61 return IsChange(rev) or IsId(rev) or IsTag(rev)
62
David Pursehouse819827a2020-02-12 15:20:19 +090063
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070064def _key(name):
Gavin Makea2e3302023-03-11 06:46:20 +000065 parts = name.split(".")
66 if len(parts) < 2:
67 return name.lower()
68 parts[0] = parts[0].lower()
69 parts[-1] = parts[-1].lower()
70 return ".".join(parts)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070071
David Pursehouse819827a2020-02-12 15:20:19 +090072
Mike Frysingerd4aee652023-10-19 05:13:32 -040073class GitConfig:
Gavin Makea2e3302023-03-11 06:46:20 +000074 _ForUser = None
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070075
Gavin Makea2e3302023-03-11 06:46:20 +000076 _ForSystem = None
77 _SYSTEM_CONFIG = "/etc/gitconfig"
Xin Li0cb6e922021-06-16 10:19:00 -070078
Gavin Makea2e3302023-03-11 06:46:20 +000079 @classmethod
80 def ForSystem(cls):
81 if cls._ForSystem is None:
82 cls._ForSystem = cls(configfile=cls._SYSTEM_CONFIG)
83 return cls._ForSystem
Xin Li0cb6e922021-06-16 10:19:00 -070084
Gavin Makea2e3302023-03-11 06:46:20 +000085 @classmethod
86 def ForUser(cls):
87 if cls._ForUser is None:
88 cls._ForUser = cls(configfile=cls._getUserConfig())
89 return cls._ForUser
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070090
Gavin Makea2e3302023-03-11 06:46:20 +000091 @staticmethod
92 def _getUserConfig():
93 return os.path.expanduser("~/.gitconfig")
Gavin Mak7e3b65b2023-01-26 23:27:51 +000094
Gavin Makea2e3302023-03-11 06:46:20 +000095 @classmethod
96 def ForRepository(cls, gitdir, defaults=None):
97 return cls(configfile=os.path.join(gitdir, "config"), defaults=defaults)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070098
Gavin Makea2e3302023-03-11 06:46:20 +000099 def __init__(self, configfile, defaults=None, jsonFile=None):
100 self.file = configfile
101 self.defaults = defaults
102 self._cache_dict = None
103 self._section_dict = None
104 self._remotes = {}
105 self._branches = {}
Shawn O. Pearce1b34c912009-05-21 18:52:49 -0700106
Gavin Makea2e3302023-03-11 06:46:20 +0000107 self._json = jsonFile
108 if self._json is None:
109 self._json = os.path.join(
110 os.path.dirname(self.file),
111 ".repo_" + os.path.basename(self.file) + ".json",
112 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700113
Gavin Makea2e3302023-03-11 06:46:20 +0000114 def ClearCache(self):
115 """Clear the in-memory cache of config."""
116 self._cache_dict = None
Jack Neusc474c9c2021-07-26 23:08:54 +0000117
Gavin Makea2e3302023-03-11 06:46:20 +0000118 def Has(self, name, include_defaults=True):
119 """Return true if this configuration file has the key."""
120 if _key(name) in self._cache:
121 return True
122 if include_defaults and self.defaults:
123 return self.defaults.Has(name, include_defaults=True)
124 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700125
Gavin Makea2e3302023-03-11 06:46:20 +0000126 def GetInt(self, name: str) -> Union[int, None]:
127 """Returns an integer from the configuration file.
Mike Frysinger77b43972020-02-19 17:55:22 -0500128
Gavin Makea2e3302023-03-11 06:46:20 +0000129 This follows the git config syntax.
Mike Frysinger77b43972020-02-19 17:55:22 -0500130
Gavin Makea2e3302023-03-11 06:46:20 +0000131 Args:
132 name: The key to lookup.
Mike Frysinger77b43972020-02-19 17:55:22 -0500133
Gavin Makea2e3302023-03-11 06:46:20 +0000134 Returns:
135 None if the value was not defined, or is not an int.
136 Otherwise, the number itself.
137 """
138 v = self.GetString(name)
139 if v is None:
140 return None
141 v = v.strip()
Mike Frysinger77b43972020-02-19 17:55:22 -0500142
Gavin Makea2e3302023-03-11 06:46:20 +0000143 mult = 1
144 if v.endswith("k"):
145 v = v[:-1]
146 mult = 1024
147 elif v.endswith("m"):
148 v = v[:-1]
149 mult = 1024 * 1024
150 elif v.endswith("g"):
151 v = v[:-1]
152 mult = 1024 * 1024 * 1024
Mike Frysinger77b43972020-02-19 17:55:22 -0500153
Gavin Makea2e3302023-03-11 06:46:20 +0000154 base = 10
155 if v.startswith("0x"):
156 base = 16
Mike Frysinger77b43972020-02-19 17:55:22 -0500157
Gavin Makea2e3302023-03-11 06:46:20 +0000158 try:
159 return int(v, base=base) * mult
160 except ValueError:
161 print(
162 f"warning: expected {name} to represent an integer, got {v} "
163 "instead",
164 file=sys.stderr,
165 )
166 return None
Mike Frysinger77b43972020-02-19 17:55:22 -0500167
Gavin Makea2e3302023-03-11 06:46:20 +0000168 def DumpConfigDict(self):
169 """Returns the current configuration dict.
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800170
Gavin Makea2e3302023-03-11 06:46:20 +0000171 Configuration data is information only (e.g. logging) and
172 should not be considered a stable data-source.
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800173
Gavin Makea2e3302023-03-11 06:46:20 +0000174 Returns:
175 dict of {<key>, <value>} for git configuration cache.
176 <value> are strings converted by GetString.
177 """
178 config_dict = {}
179 for key in self._cache:
180 config_dict[key] = self.GetString(key)
181 return config_dict
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800182
Daniel Kutik49c9b062023-10-20 18:25:25 +0200183 def GetBoolean(self, name: str) -> Union[bool, None]:
Gavin Makea2e3302023-03-11 06:46:20 +0000184 """Returns a boolean from the configuration file.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700185
Gavin Makea2e3302023-03-11 06:46:20 +0000186 Returns:
187 None: The value was not defined, or is not a boolean.
188 True: The value was set to true or yes.
189 False: The value was set to false or no.
190 """
191 v = self.GetString(name)
192 if v is None:
193 return None
194 v = v.lower()
195 if v in ("true", "yes"):
196 return True
197 if v in ("false", "no"):
198 return False
199 print(
200 f"warning: expected {name} to represent a boolean, got {v} instead",
201 file=sys.stderr,
202 )
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700203 return None
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700204
Gavin Makea2e3302023-03-11 06:46:20 +0000205 def SetBoolean(self, name, value):
206 """Set the truthy value for a key."""
207 if value is not None:
208 value = "true" if value else "false"
209 self.SetString(name, value)
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700210
Gavin Makea2e3302023-03-11 06:46:20 +0000211 def GetString(self, name: str, all_keys: bool = False) -> Union[str, None]:
212 """Get the first value for a key, or None if it is not defined.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700213
Gavin Makea2e3302023-03-11 06:46:20 +0000214 This configuration file is used first, if the key is not
215 defined or all_keys = True then the defaults are also searched.
216 """
217 try:
218 v = self._cache[_key(name)]
219 except KeyError:
220 if self.defaults:
221 return self.defaults.GetString(name, all_keys=all_keys)
222 v = []
David Aguilar438c5472009-06-28 15:09:16 -0700223
Gavin Makea2e3302023-03-11 06:46:20 +0000224 if not all_keys:
225 if v:
226 return v[0]
227 return None
Mike Frysingerf88282c2021-09-28 15:59:40 -0400228
Gavin Makea2e3302023-03-11 06:46:20 +0000229 r = []
230 r.extend(v)
231 if self.defaults:
232 r.extend(self.defaults.GetString(name, all_keys=True))
233 return r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700234
Gavin Makea2e3302023-03-11 06:46:20 +0000235 def SetString(self, name, value):
236 """Set the value(s) for a key.
237 Only this configuration file is modified.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700238
Gavin Makea2e3302023-03-11 06:46:20 +0000239 The supplied value should be either a string, or a list of strings (to
240 store multiple values), or None (to delete the key).
241 """
242 key = _key(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700243
Gavin Makea2e3302023-03-11 06:46:20 +0000244 try:
245 old = self._cache[key]
246 except KeyError:
247 old = []
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700248
Gavin Makea2e3302023-03-11 06:46:20 +0000249 if value is None:
250 if old:
251 del self._cache[key]
252 self._do("--unset-all", name)
253
254 elif isinstance(value, list):
255 if len(value) == 0:
256 self.SetString(name, None)
257
258 elif len(value) == 1:
259 self.SetString(name, value[0])
260
261 elif old != value:
262 self._cache[key] = list(value)
263 self._do("--replace-all", name, value[0])
264 for i in range(1, len(value)):
265 self._do("--add", name, value[i])
266
267 elif len(old) != 1 or old[0] != value:
268 self._cache[key] = [value]
269 self._do("--replace-all", name, value)
270
271 def GetRemote(self, name):
272 """Get the remote.$name.* configuration values as an object."""
273 try:
274 r = self._remotes[name]
275 except KeyError:
276 r = Remote(self, name)
277 self._remotes[r.name] = r
278 return r
279
280 def GetBranch(self, name):
281 """Get the branch.$name.* configuration values as an object."""
282 try:
283 b = self._branches[name]
284 except KeyError:
285 b = Branch(self, name)
286 self._branches[b.name] = b
287 return b
288
289 def GetSyncAnalysisStateData(self):
290 """Returns data to be logged for the analysis of sync performance."""
291 return {
292 k: v
293 for k, v in self.DumpConfigDict().items()
294 if k.startswith(SYNC_STATE_PREFIX)
295 }
296
297 def UpdateSyncAnalysisState(self, options, superproject_logging_data):
298 """Update Config's SYNC_STATE_PREFIX* data with the latest sync data.
299
300 Args:
301 options: Options passed to sync returned from optparse. See
302 _Options().
303 superproject_logging_data: A dictionary of superproject data that is
304 to be logged.
305
306 Returns:
307 SyncAnalysisState object.
308 """
309 return SyncAnalysisState(self, options, superproject_logging_data)
310
311 def GetSubSections(self, section):
312 """List all subsection names matching $section.*.*"""
313 return self._sections.get(section, set())
314
315 def HasSection(self, section, subsection=""):
316 """Does at least one key in section.subsection exist?"""
317 try:
318 return subsection in self._sections[section]
319 except KeyError:
320 return False
321
322 def UrlInsteadOf(self, url):
323 """Resolve any url.*.insteadof references."""
324 for new_url in self.GetSubSections("url"):
325 for old_url in self.GetString("url.%s.insteadof" % new_url, True):
326 if old_url is not None and url.startswith(old_url):
327 return new_url + url[len(old_url) :]
328 return url
329
330 @property
331 def _sections(self):
332 d = self._section_dict
333 if d is None:
334 d = {}
335 for name in self._cache.keys():
336 p = name.split(".")
337 if 2 == len(p):
338 section = p[0]
339 subsect = ""
340 else:
341 section = p[0]
342 subsect = ".".join(p[1:-1])
343 if section not in d:
344 d[section] = set()
345 d[section].add(subsect)
346 self._section_dict = d
347 return d
348
349 @property
350 def _cache(self):
351 if self._cache_dict is None:
352 self._cache_dict = self._Read()
353 return self._cache_dict
354
355 def _Read(self):
356 d = self._ReadJson()
357 if d is None:
358 d = self._ReadGit()
359 self._SaveJson(d)
360 return d
361
362 def _ReadJson(self):
363 try:
364 if os.path.getmtime(self._json) <= os.path.getmtime(self.file):
365 platform_utils.remove(self._json)
366 return None
367 except OSError:
368 return None
369 try:
370 with Trace(": parsing %s", self.file):
371 with open(self._json) as fd:
372 return json.load(fd)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545373 except (OSError, ValueError):
Gavin Makea2e3302023-03-11 06:46:20 +0000374 platform_utils.remove(self._json, missing_ok=True)
375 return None
376
377 def _SaveJson(self, cache):
378 try:
379 with open(self._json, "w") as fd:
380 json.dump(cache, fd, indent=2)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545381 except (OSError, TypeError):
Gavin Makea2e3302023-03-11 06:46:20 +0000382 platform_utils.remove(self._json, missing_ok=True)
383
384 def _ReadGit(self):
385 """
386 Read configuration data from git.
387
388 This internal method populates the GitConfig cache.
389
390 """
391 c = {}
392 if not os.path.exists(self.file):
393 return c
394
395 d = self._do("--null", "--list")
396 for line in d.rstrip("\0").split("\0"):
397 if "\n" in line:
398 key, val = line.split("\n", 1)
399 else:
400 key = line
401 val = None
402
403 if key in c:
404 c[key].append(val)
405 else:
406 c[key] = [val]
407
408 return c
409
410 def _do(self, *args):
411 if self.file == self._SYSTEM_CONFIG:
412 command = ["config", "--system", "--includes"]
413 else:
414 command = ["config", "--file", self.file, "--includes"]
415 command.extend(args)
416
417 p = GitCommand(None, command, capture_stdout=True, capture_stderr=True)
418 if p.Wait() == 0:
419 return p.stdout
420 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400421 raise GitError(f"git config {str(args)}: {p.stderr}")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700422
423
Mike Frysingerf841ca42020-02-18 21:31:51 -0500424class RepoConfig(GitConfig):
Gavin Makea2e3302023-03-11 06:46:20 +0000425 """User settings for repo itself."""
Mike Frysingerf841ca42020-02-18 21:31:51 -0500426
Gavin Makea2e3302023-03-11 06:46:20 +0000427 @staticmethod
428 def _getUserConfig():
429 repo_config_dir = os.getenv("REPO_CONFIG_DIR", os.path.expanduser("~"))
430 return os.path.join(repo_config_dir, ".repoconfig/config")
Mike Frysingerf841ca42020-02-18 21:31:51 -0500431
432
Mike Frysingerd4aee652023-10-19 05:13:32 -0400433class RefSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000434 """A Git refspec line, split into its components:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700435
Gavin Makea2e3302023-03-11 06:46:20 +0000436 forced: True if the line starts with '+'
437 src: Left side of the line
438 dst: Right side of the line
439 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700440
Gavin Makea2e3302023-03-11 06:46:20 +0000441 @classmethod
442 def FromString(cls, rs):
443 lhs, rhs = rs.split(":", 2)
444 if lhs.startswith("+"):
445 lhs = lhs[1:]
446 forced = True
447 else:
448 forced = False
449 return cls(forced, lhs, rhs)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700450
Gavin Makea2e3302023-03-11 06:46:20 +0000451 def __init__(self, forced, lhs, rhs):
452 self.forced = forced
453 self.src = lhs
454 self.dst = rhs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700455
Gavin Makea2e3302023-03-11 06:46:20 +0000456 def SourceMatches(self, rev):
457 if self.src:
458 if rev == self.src:
459 return True
460 if self.src.endswith("/*") and rev.startswith(self.src[:-1]):
461 return True
462 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700463
Gavin Makea2e3302023-03-11 06:46:20 +0000464 def DestMatches(self, ref):
465 if self.dst:
466 if ref == self.dst:
467 return True
468 if self.dst.endswith("/*") and ref.startswith(self.dst[:-1]):
469 return True
470 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700471
Gavin Makea2e3302023-03-11 06:46:20 +0000472 def MapSource(self, rev):
473 if self.src.endswith("/*"):
474 return self.dst[:-1] + rev[len(self.src) - 1 :]
475 return self.dst
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700476
Gavin Makea2e3302023-03-11 06:46:20 +0000477 def __str__(self):
478 s = ""
479 if self.forced:
480 s += "+"
481 if self.src:
482 s += self.src
483 if self.dst:
484 s += ":"
485 s += self.dst
486 return s
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700487
488
Gavin Makea2e3302023-03-11 06:46:20 +0000489URI_ALL = re.compile(r"^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/")
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700490
David Pursehouse819827a2020-02-12 15:20:19 +0900491
Shawn O. Pearcef322b9a2011-09-19 14:50:58 -0700492def GetSchemeFromUrl(url):
Gavin Makea2e3302023-03-11 06:46:20 +0000493 m = URI_ALL.match(url)
494 if m:
495 return m.group(1)
496 return None
Shawn O. Pearcef322b9a2011-09-19 14:50:58 -0700497
David Pursehouse819827a2020-02-12 15:20:19 +0900498
Dan Willemsen0745bb22015-08-17 13:41:45 -0700499@contextlib.contextmanager
500def GetUrlCookieFile(url, quiet):
Gavin Makea2e3302023-03-11 06:46:20 +0000501 if url.startswith("persistent-"):
502 try:
503 p = subprocess.Popen(
504 ["git-remote-persistent-https", "-print_config", url],
505 stdin=subprocess.PIPE,
506 stdout=subprocess.PIPE,
507 stderr=subprocess.PIPE,
508 )
509 try:
510 cookieprefix = "http.cookiefile="
511 proxyprefix = "http.proxy="
512 cookiefile = None
513 proxy = None
514 for line in p.stdout:
515 line = line.strip().decode("utf-8")
516 if line.startswith(cookieprefix):
517 cookiefile = os.path.expanduser(
518 line[len(cookieprefix) :]
519 )
520 if line.startswith(proxyprefix):
521 proxy = line[len(proxyprefix) :]
522 # Leave subprocess open, as cookie file may be transient.
523 if cookiefile or proxy:
524 yield cookiefile, proxy
525 return
526 finally:
527 p.stdin.close()
528 if p.wait():
529 err_msg = p.stderr.read().decode("utf-8")
530 if " -print_config" in err_msg:
531 pass # Persistent proxy doesn't support -print_config.
532 elif not quiet:
533 print(err_msg, file=sys.stderr)
534 except OSError as e:
535 if e.errno == errno.ENOENT:
536 pass # No persistent proxy.
537 raise
538 cookiefile = GitConfig.ForUser().GetString("http.cookiefile")
539 if cookiefile:
540 cookiefile = os.path.expanduser(cookiefile)
541 yield cookiefile, None
Dan Willemsen0745bb22015-08-17 13:41:45 -0700542
David Pursehouse819827a2020-02-12 15:20:19 +0900543
Mike Frysingerd4aee652023-10-19 05:13:32 -0400544class Remote:
Gavin Makea2e3302023-03-11 06:46:20 +0000545 """Configuration options related to a remote."""
David Pursehouse819827a2020-02-12 15:20:19 +0900546
Gavin Makea2e3302023-03-11 06:46:20 +0000547 def __init__(self, config, name):
548 self._config = config
549 self.name = name
550 self.url = self._Get("url")
551 self.pushUrl = self._Get("pushurl")
552 self.review = self._Get("review")
553 self.projectname = self._Get("projectname")
554 self.fetch = list(
555 map(RefSpec.FromString, self._Get("fetch", all_keys=True))
556 )
557 self._review_url = None
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800558
Gavin Makea2e3302023-03-11 06:46:20 +0000559 def _InsteadOf(self):
560 globCfg = GitConfig.ForUser()
561 urlList = globCfg.GetSubSections("url")
562 longest = ""
563 longestUrl = ""
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100564
Gavin Makea2e3302023-03-11 06:46:20 +0000565 for url in urlList:
566 key = "url." + url + ".insteadOf"
567 insteadOfList = globCfg.GetString(key, all_keys=True)
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100568
Gavin Makea2e3302023-03-11 06:46:20 +0000569 for insteadOf in insteadOfList:
570 if self.url.startswith(insteadOf) and len(insteadOf) > len(
571 longest
572 ):
573 longest = insteadOf
574 longestUrl = url
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100575
Gavin Makea2e3302023-03-11 06:46:20 +0000576 if len(longest) == 0:
577 return self.url
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100578
Gavin Makea2e3302023-03-11 06:46:20 +0000579 return self.url.replace(longest, longestUrl, 1)
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100580
Gavin Makea2e3302023-03-11 06:46:20 +0000581 def PreConnectFetch(self, ssh_proxy):
582 """Run any setup for this remote before we connect to it.
Mike Frysinger19e409c2021-05-05 19:44:35 -0400583
Gavin Makea2e3302023-03-11 06:46:20 +0000584 In practice, if the remote is using SSH, we'll attempt to create a new
585 SSH master session to it for reuse across projects.
Mike Frysinger19e409c2021-05-05 19:44:35 -0400586
Gavin Makea2e3302023-03-11 06:46:20 +0000587 Args:
588 ssh_proxy: The SSH settings for managing master sessions.
Mike Frysinger339f2df2021-05-06 00:44:42 -0400589
Gavin Makea2e3302023-03-11 06:46:20 +0000590 Returns:
591 Whether the preconnect phase for this remote was successful.
592 """
593 if not ssh_proxy:
594 return True
Mike Frysinger339f2df2021-05-06 00:44:42 -0400595
Gavin Makea2e3302023-03-11 06:46:20 +0000596 connectionUrl = self._InsteadOf()
597 return ssh_proxy.preconnect(connectionUrl)
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700598
Gavin Makea2e3302023-03-11 06:46:20 +0000599 def ReviewUrl(self, userEmail, validate_certs):
600 if self._review_url is None:
601 if self.review is None:
602 return None
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800603
Gavin Makea2e3302023-03-11 06:46:20 +0000604 u = self.review
605 if u.startswith("persistent-"):
606 u = u[len("persistent-") :]
607 if u.split(":")[0] not in ("http", "https", "sso", "ssh"):
608 u = "http://%s" % u
609 if u.endswith("/Gerrit"):
610 u = u[: len(u) - len("/Gerrit")]
611 if u.endswith("/ssh_info"):
612 u = u[: len(u) - len("/ssh_info")]
613 if not u.endswith("/"):
614 u += "/"
615 http_url = u
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800616
Gavin Makea2e3302023-03-11 06:46:20 +0000617 if u in REVIEW_CACHE:
618 self._review_url = REVIEW_CACHE[u]
619 elif "REPO_HOST_PORT_INFO" in os.environ:
620 host, port = os.environ["REPO_HOST_PORT_INFO"].split()
621 self._review_url = self._SshReviewUrl(userEmail, host, port)
622 REVIEW_CACHE[u] = self._review_url
623 elif u.startswith("sso:") or u.startswith("ssh:"):
624 self._review_url = u # Assume it's right
625 REVIEW_CACHE[u] = self._review_url
626 elif "REPO_IGNORE_SSH_INFO" in os.environ:
627 self._review_url = http_url
628 REVIEW_CACHE[u] = self._review_url
629 else:
630 try:
631 info_url = u + "ssh_info"
632 if not validate_certs:
633 context = ssl._create_unverified_context()
634 info = urllib.request.urlopen(
635 info_url, context=context
636 ).read()
637 else:
638 info = urllib.request.urlopen(info_url).read()
639 if info == b"NOT_AVAILABLE" or b"<" in info:
640 # If `info` contains '<', we assume the server gave us
641 # some sort of HTML response back, like maybe a login
642 # page.
643 #
644 # Assume HTTP if SSH is not enabled or ssh_info doesn't
645 # look right.
646 self._review_url = http_url
647 else:
648 info = info.decode("utf-8")
649 host, port = info.split()
650 self._review_url = self._SshReviewUrl(
651 userEmail, host, port
652 )
653 except urllib.error.HTTPError as e:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400654 raise UploadError(f"{self.review}: {str(e)}")
Gavin Makea2e3302023-03-11 06:46:20 +0000655 except urllib.error.URLError as e:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400656 raise UploadError(f"{self.review}: {str(e)}")
Mike Frysinger06ddc8c2023-08-21 21:26:51 -0400657 except http.client.HTTPException as e:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400658 raise UploadError(f"{self.review}: {e.__class__.__name__}")
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800659
Gavin Makea2e3302023-03-11 06:46:20 +0000660 REVIEW_CACHE[u] = self._review_url
661 return self._review_url + self.projectname
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800662
Gavin Makea2e3302023-03-11 06:46:20 +0000663 def _SshReviewUrl(self, userEmail, host, port):
664 username = self._config.GetString("review.%s.username" % self.review)
665 if username is None:
666 username = userEmail.split("@")[0]
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400667 return f"ssh://{username}@{host}:{port}/"
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700668
Gavin Makea2e3302023-03-11 06:46:20 +0000669 def ToLocal(self, rev):
670 """Convert a remote revision string to something we have locally."""
671 if self.name == "." or IsId(rev):
672 return rev
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700673
Gavin Makea2e3302023-03-11 06:46:20 +0000674 if not rev.startswith("refs/"):
675 rev = R_HEADS + rev
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700676
Gavin Makea2e3302023-03-11 06:46:20 +0000677 for spec in self.fetch:
678 if spec.SourceMatches(rev):
679 return spec.MapSource(rev)
Nasser Grainawi909d58b2014-09-19 12:13:04 -0600680
Gavin Makea2e3302023-03-11 06:46:20 +0000681 if not rev.startswith(R_HEADS):
682 return rev
Nasser Grainawi909d58b2014-09-19 12:13:04 -0600683
Gavin Makea2e3302023-03-11 06:46:20 +0000684 raise GitError(
685 "%s: remote %s does not have %s"
686 % (self.projectname, self.name, rev)
687 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700688
Gavin Makea2e3302023-03-11 06:46:20 +0000689 def WritesTo(self, ref):
690 """True if the remote stores to the tracking ref."""
691 for spec in self.fetch:
692 if spec.DestMatches(ref):
693 return True
694 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700695
Gavin Makea2e3302023-03-11 06:46:20 +0000696 def ResetFetch(self, mirror=False):
697 """Set the fetch refspec to its default value."""
698 if mirror:
699 dst = "refs/heads/*"
700 else:
701 dst = "refs/remotes/%s/*" % self.name
702 self.fetch = [RefSpec(True, "refs/heads/*", dst)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700703
Gavin Makea2e3302023-03-11 06:46:20 +0000704 def Save(self):
705 """Save this remote to the configuration."""
706 self._Set("url", self.url)
707 if self.pushUrl is not None:
708 self._Set("pushurl", self.pushUrl + "/" + self.projectname)
709 else:
710 self._Set("pushurl", self.pushUrl)
711 self._Set("review", self.review)
712 self._Set("projectname", self.projectname)
713 self._Set("fetch", list(map(str, self.fetch)))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700714
Gavin Makea2e3302023-03-11 06:46:20 +0000715 def _Set(self, key, value):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400716 key = f"remote.{self.name}.{key}"
Gavin Makea2e3302023-03-11 06:46:20 +0000717 return self._config.SetString(key, value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700718
Gavin Makea2e3302023-03-11 06:46:20 +0000719 def _Get(self, key, all_keys=False):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400720 key = f"remote.{self.name}.{key}"
Gavin Makea2e3302023-03-11 06:46:20 +0000721 return self._config.GetString(key, all_keys=all_keys)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700722
723
Mike Frysingerd4aee652023-10-19 05:13:32 -0400724class Branch:
Gavin Makea2e3302023-03-11 06:46:20 +0000725 """Configuration options related to a single branch."""
David Pursehouse819827a2020-02-12 15:20:19 +0900726
Gavin Makea2e3302023-03-11 06:46:20 +0000727 def __init__(self, config, name):
728 self._config = config
729 self.name = name
730 self.merge = self._Get("merge")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700731
Gavin Makea2e3302023-03-11 06:46:20 +0000732 r = self._Get("remote")
733 if r:
734 self.remote = self._config.GetRemote(r)
735 else:
736 self.remote = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700737
Gavin Makea2e3302023-03-11 06:46:20 +0000738 @property
739 def LocalMerge(self):
740 """Convert the merge spec to a local name."""
741 if self.remote and self.merge:
742 return self.remote.ToLocal(self.merge)
743 return None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700744
Gavin Makea2e3302023-03-11 06:46:20 +0000745 def Save(self):
746 """Save this branch back into the configuration."""
747 if self._config.HasSection("branch", self.name):
748 if self.remote:
749 self._Set("remote", self.remote.name)
750 else:
751 self._Set("remote", None)
752 self._Set("merge", self.merge)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700753
Gavin Makea2e3302023-03-11 06:46:20 +0000754 else:
755 with open(self._config.file, "a") as fd:
756 fd.write('[branch "%s"]\n' % self.name)
757 if self.remote:
758 fd.write("\tremote = %s\n" % self.remote.name)
759 if self.merge:
760 fd.write("\tmerge = %s\n" % self.merge)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700761
Gavin Makea2e3302023-03-11 06:46:20 +0000762 def _Set(self, key, value):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400763 key = f"branch.{self.name}.{key}"
Gavin Makea2e3302023-03-11 06:46:20 +0000764 return self._config.SetString(key, value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700765
Gavin Makea2e3302023-03-11 06:46:20 +0000766 def _Get(self, key, all_keys=False):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400767 key = f"branch.{self.name}.{key}"
Gavin Makea2e3302023-03-11 06:46:20 +0000768 return self._config.GetString(key, all_keys=all_keys)
Raman Tenneti7954de12021-07-28 14:36:49 -0700769
770
771class SyncAnalysisState:
Gavin Makea2e3302023-03-11 06:46:20 +0000772 """Configuration options related to logging of sync state for analysis.
Raman Tenneti7954de12021-07-28 14:36:49 -0700773
Gavin Makea2e3302023-03-11 06:46:20 +0000774 This object is versioned.
Raman Tenneti7954de12021-07-28 14:36:49 -0700775 """
Raman Tenneti7954de12021-07-28 14:36:49 -0700776
Gavin Makea2e3302023-03-11 06:46:20 +0000777 def __init__(self, config, options, superproject_logging_data):
778 """Initializes SyncAnalysisState.
Raman Tenneti7954de12021-07-28 14:36:49 -0700779
Gavin Makea2e3302023-03-11 06:46:20 +0000780 Saves the following data into the |config| object.
781 - sys.argv, options, superproject's logging data.
782 - repo.*, branch.* and remote.* parameters from config object.
783 - Current time as synctime.
784 - Version number of the object.
Raman Tenneti7954de12021-07-28 14:36:49 -0700785
Gavin Makea2e3302023-03-11 06:46:20 +0000786 All the keys saved by this object are prepended with SYNC_STATE_PREFIX.
Raman Tenneti7954de12021-07-28 14:36:49 -0700787
Gavin Makea2e3302023-03-11 06:46:20 +0000788 Args:
789 config: GitConfig object to store all options.
790 options: Options passed to sync returned from optparse. See
791 _Options().
792 superproject_logging_data: A dictionary of superproject data that is
793 to be logged.
794 """
795 self._config = config
LuK1337aadd12c2023-09-16 09:36:49 +0200796 now = datetime.datetime.now(datetime.timezone.utc)
797 self._Set("main.synctime", now.isoformat(timespec="microseconds"))
Gavin Makea2e3302023-03-11 06:46:20 +0000798 self._Set("main.version", "1")
799 self._Set("sys.argv", sys.argv)
800 for key, value in superproject_logging_data.items():
801 self._Set(f"superproject.{key}", value)
802 for key, value in options.__dict__.items():
803 self._Set(f"options.{key}", value)
804 config_items = config.DumpConfigDict().items()
805 EXTRACT_NAMESPACES = {"repo", "branch", "remote"}
806 self._SetDictionary(
807 {
808 k: v
809 for k, v in config_items
810 if not k.startswith(SYNC_STATE_PREFIX)
811 and k.split(".", 1)[0] in EXTRACT_NAMESPACES
812 }
813 )
Raman Tenneti7954de12021-07-28 14:36:49 -0700814
Gavin Makea2e3302023-03-11 06:46:20 +0000815 def _SetDictionary(self, data):
816 """Save all key/value pairs of |data| dictionary.
817
818 Args:
819 data: A dictionary whose key/value are to be saved.
820 """
821 for key, value in data.items():
822 self._Set(key, value)
823
824 def _Set(self, key, value):
825 """Set the |value| for a |key| in the |_config| member.
826
827 |key| is prepended with the value of SYNC_STATE_PREFIX constant.
828
829 Args:
830 key: Name of the key.
831 value: |value| could be of any type. If it is 'bool', it will be
832 saved as a Boolean and for all other types, it will be saved as
833 a String.
834 """
835 if value is None:
836 return
837 sync_key = f"{SYNC_STATE_PREFIX}{key}"
838 sync_key = sync_key.replace("_", "")
839 if isinstance(value, str):
840 self._config.SetString(sync_key, value)
841 elif isinstance(value, bool):
842 self._config.SetBoolean(sync_key, value)
843 else:
844 self._config.SetString(sync_key, str(value))