blob: 14c4c11afb602cb01cdc96dbc39a6539db61cc81 [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():
flexagoon1711bc22025-01-23 16:05:47 +030093 """Get the user-specific config file.
94
95 Prefers the XDG config location if available, with fallback to
96 ~/.gitconfig
97
98 This matches git behavior:
99 https://git-scm.com/docs/git-config#FILES
100 """
101 xdg_config_home = os.getenv(
102 "XDG_CONFIG_HOME", os.path.expanduser("~/.config")
103 )
104 xdg_config_file = os.path.join(xdg_config_home, "git", "config")
105 if os.path.exists(xdg_config_file):
106 return xdg_config_file
Gavin Makea2e3302023-03-11 06:46:20 +0000107 return os.path.expanduser("~/.gitconfig")
Gavin Mak7e3b65b2023-01-26 23:27:51 +0000108
Gavin Makea2e3302023-03-11 06:46:20 +0000109 @classmethod
110 def ForRepository(cls, gitdir, defaults=None):
111 return cls(configfile=os.path.join(gitdir, "config"), defaults=defaults)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700112
Gavin Makea2e3302023-03-11 06:46:20 +0000113 def __init__(self, configfile, defaults=None, jsonFile=None):
114 self.file = configfile
115 self.defaults = defaults
116 self._cache_dict = None
117 self._section_dict = None
118 self._remotes = {}
119 self._branches = {}
Shawn O. Pearce1b34c912009-05-21 18:52:49 -0700120
Gavin Makea2e3302023-03-11 06:46:20 +0000121 self._json = jsonFile
122 if self._json is None:
123 self._json = os.path.join(
124 os.path.dirname(self.file),
125 ".repo_" + os.path.basename(self.file) + ".json",
126 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700127
Gavin Makea2e3302023-03-11 06:46:20 +0000128 def ClearCache(self):
129 """Clear the in-memory cache of config."""
130 self._cache_dict = None
Jack Neusc474c9c2021-07-26 23:08:54 +0000131
Gavin Makea2e3302023-03-11 06:46:20 +0000132 def Has(self, name, include_defaults=True):
133 """Return true if this configuration file has the key."""
134 if _key(name) in self._cache:
135 return True
136 if include_defaults and self.defaults:
137 return self.defaults.Has(name, include_defaults=True)
138 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700139
Gavin Makea2e3302023-03-11 06:46:20 +0000140 def GetInt(self, name: str) -> Union[int, None]:
141 """Returns an integer from the configuration file.
Mike Frysinger77b43972020-02-19 17:55:22 -0500142
Gavin Makea2e3302023-03-11 06:46:20 +0000143 This follows the git config syntax.
Mike Frysinger77b43972020-02-19 17:55:22 -0500144
Gavin Makea2e3302023-03-11 06:46:20 +0000145 Args:
146 name: The key to lookup.
Mike Frysinger77b43972020-02-19 17:55:22 -0500147
Gavin Makea2e3302023-03-11 06:46:20 +0000148 Returns:
149 None if the value was not defined, or is not an int.
150 Otherwise, the number itself.
151 """
152 v = self.GetString(name)
153 if v is None:
154 return None
155 v = v.strip()
Mike Frysinger77b43972020-02-19 17:55:22 -0500156
Gavin Makea2e3302023-03-11 06:46:20 +0000157 mult = 1
158 if v.endswith("k"):
159 v = v[:-1]
160 mult = 1024
161 elif v.endswith("m"):
162 v = v[:-1]
163 mult = 1024 * 1024
164 elif v.endswith("g"):
165 v = v[:-1]
166 mult = 1024 * 1024 * 1024
Mike Frysinger77b43972020-02-19 17:55:22 -0500167
Gavin Makea2e3302023-03-11 06:46:20 +0000168 base = 10
169 if v.startswith("0x"):
170 base = 16
Mike Frysinger77b43972020-02-19 17:55:22 -0500171
Gavin Makea2e3302023-03-11 06:46:20 +0000172 try:
173 return int(v, base=base) * mult
174 except ValueError:
175 print(
176 f"warning: expected {name} to represent an integer, got {v} "
177 "instead",
178 file=sys.stderr,
179 )
180 return None
Mike Frysinger77b43972020-02-19 17:55:22 -0500181
Gavin Makea2e3302023-03-11 06:46:20 +0000182 def DumpConfigDict(self):
183 """Returns the current configuration dict.
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800184
Gavin Makea2e3302023-03-11 06:46:20 +0000185 Configuration data is information only (e.g. logging) and
186 should not be considered a stable data-source.
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800187
Gavin Makea2e3302023-03-11 06:46:20 +0000188 Returns:
189 dict of {, } for git configuration cache.
190 are strings converted by GetString.
191 """
192 config_dict = {}
193 for key in self._cache:
194 config_dict[key] = self.GetString(key)
195 return config_dict
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800196
Daniel Kutik49c9b062023-10-20 18:25:25 +0200197 def GetBoolean(self, name: str) -> Union[bool, None]:
Gavin Makea2e3302023-03-11 06:46:20 +0000198 """Returns a boolean from the configuration file.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700199
Gavin Makea2e3302023-03-11 06:46:20 +0000200 Returns:
201 None: The value was not defined, or is not a boolean.
202 True: The value was set to true or yes.
203 False: The value was set to false or no.
204 """
205 v = self.GetString(name)
206 if v is None:
207 return None
208 v = v.lower()
209 if v in ("true", "yes"):
210 return True
211 if v in ("false", "no"):
212 return False
213 print(
214 f"warning: expected {name} to represent a boolean, got {v} instead",
215 file=sys.stderr,
216 )
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700217 return None
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700218
Gavin Makea2e3302023-03-11 06:46:20 +0000219 def SetBoolean(self, name, value):
220 """Set the truthy value for a key."""
221 if value is not None:
222 value = "true" if value else "false"
223 self.SetString(name, value)
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700224
Gavin Makea2e3302023-03-11 06:46:20 +0000225 def GetString(self, name: str, all_keys: bool = False) -> Union[str, None]:
226 """Get the first value for a key, or None if it is not defined.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700227
Gavin Makea2e3302023-03-11 06:46:20 +0000228 This configuration file is used first, if the key is not
229 defined or all_keys = True then the defaults are also searched.
230 """
231 try:
232 v = self._cache[_key(name)]
233 except KeyError:
234 if self.defaults:
235 return self.defaults.GetString(name, all_keys=all_keys)
236 v = []
David Aguilar438c5472009-06-28 15:09:16 -0700237
Gavin Makea2e3302023-03-11 06:46:20 +0000238 if not all_keys:
239 if v:
240 return v[0]
241 return None
Mike Frysingerf88282c2021-09-28 15:59:40 -0400242
Gavin Makea2e3302023-03-11 06:46:20 +0000243 r = []
244 r.extend(v)
245 if self.defaults:
246 r.extend(self.defaults.GetString(name, all_keys=True))
247 return r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700248
Gavin Makea2e3302023-03-11 06:46:20 +0000249 def SetString(self, name, value):
250 """Set the value(s) for a key.
251 Only this configuration file is modified.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700252
Gavin Makea2e3302023-03-11 06:46:20 +0000253 The supplied value should be either a string, or a list of strings (to
254 store multiple values), or None (to delete the key).
255 """
256 key = _key(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700257
Gavin Makea2e3302023-03-11 06:46:20 +0000258 try:
259 old = self._cache[key]
260 except KeyError:
261 old = []
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700262
Gavin Makea2e3302023-03-11 06:46:20 +0000263 if value is None:
264 if old:
265 del self._cache[key]
266 self._do("--unset-all", name)
267
268 elif isinstance(value, list):
269 if len(value) == 0:
270 self.SetString(name, None)
271
272 elif len(value) == 1:
273 self.SetString(name, value[0])
274
275 elif old != value:
276 self._cache[key] = list(value)
277 self._do("--replace-all", name, value[0])
278 for i in range(1, len(value)):
279 self._do("--add", name, value[i])
280
281 elif len(old) != 1 or old[0] != value:
282 self._cache[key] = [value]
283 self._do("--replace-all", name, value)
284
285 def GetRemote(self, name):
286 """Get the remote.$name.* configuration values as an object."""
287 try:
288 r = self._remotes[name]
289 except KeyError:
290 r = Remote(self, name)
291 self._remotes[r.name] = r
292 return r
293
294 def GetBranch(self, name):
295 """Get the branch.$name.* configuration values as an object."""
296 try:
297 b = self._branches[name]
298 except KeyError:
299 b = Branch(self, name)
300 self._branches[b.name] = b
301 return b
302
303 def GetSyncAnalysisStateData(self):
304 """Returns data to be logged for the analysis of sync performance."""
305 return {
306 k: v
307 for k, v in self.DumpConfigDict().items()
308 if k.startswith(SYNC_STATE_PREFIX)
309 }
310
311 def UpdateSyncAnalysisState(self, options, superproject_logging_data):
312 """Update Config's SYNC_STATE_PREFIX* data with the latest sync data.
313
314 Args:
315 options: Options passed to sync returned from optparse. See
316 _Options().
317 superproject_logging_data: A dictionary of superproject data that is
318 to be logged.
319
320 Returns:
321 SyncAnalysisState object.
322 """
323 return SyncAnalysisState(self, options, superproject_logging_data)
324
325 def GetSubSections(self, section):
326 """List all subsection names matching $section.*.*"""
327 return self._sections.get(section, set())
328
329 def HasSection(self, section, subsection=""):
330 """Does at least one key in section.subsection exist?"""
331 try:
332 return subsection in self._sections[section]
333 except KeyError:
334 return False
335
336 def UrlInsteadOf(self, url):
337 """Resolve any url.*.insteadof references."""
338 for new_url in self.GetSubSections("url"):
339 for old_url in self.GetString("url.%s.insteadof" % new_url, True):
340 if old_url is not None and url.startswith(old_url):
341 return new_url + url[len(old_url) :]
342 return url
343
344 @property
345 def _sections(self):
346 d = self._section_dict
347 if d is None:
348 d = {}
349 for name in self._cache.keys():
350 p = name.split(".")
351 if 2 == len(p):
352 section = p[0]
353 subsect = ""
354 else:
355 section = p[0]
356 subsect = ".".join(p[1:-1])
357 if section not in d:
358 d[section] = set()
359 d[section].add(subsect)
360 self._section_dict = d
361 return d
362
363 @property
364 def _cache(self):
365 if self._cache_dict is None:
366 self._cache_dict = self._Read()
367 return self._cache_dict
368
369 def _Read(self):
370 d = self._ReadJson()
371 if d is None:
372 d = self._ReadGit()
373 self._SaveJson(d)
374 return d
375
376 def _ReadJson(self):
377 try:
378 if os.path.getmtime(self._json) <= os.path.getmtime(self.file):
379 platform_utils.remove(self._json)
380 return None
381 except OSError:
382 return None
383 try:
384 with Trace(": parsing %s", self.file):
385 with open(self._json) as fd:
386 return json.load(fd)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545387 except (OSError, ValueError):
Gavin Makea2e3302023-03-11 06:46:20 +0000388 platform_utils.remove(self._json, missing_ok=True)
389 return None
390
391 def _SaveJson(self, cache):
392 try:
393 with open(self._json, "w") as fd:
394 json.dump(cache, fd, indent=2)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545395 except (OSError, TypeError):
Gavin Makea2e3302023-03-11 06:46:20 +0000396 platform_utils.remove(self._json, missing_ok=True)
397
398 def _ReadGit(self):
399 """
400 Read configuration data from git.
401
402 This internal method populates the GitConfig cache.
403
404 """
405 c = {}
406 if not os.path.exists(self.file):
407 return c
408
409 d = self._do("--null", "--list")
410 for line in d.rstrip("\0").split("\0"):
411 if "\n" in line:
412 key, val = line.split("\n", 1)
413 else:
414 key = line
415 val = None
416
417 if key in c:
418 c[key].append(val)
419 else:
420 c[key] = [val]
421
422 return c
423
424 def _do(self, *args):
425 if self.file == self._SYSTEM_CONFIG:
426 command = ["config", "--system", "--includes"]
427 else:
428 command = ["config", "--file", self.file, "--includes"]
429 command.extend(args)
430
431 p = GitCommand(None, command, capture_stdout=True, capture_stderr=True)
432 if p.Wait() == 0:
433 return p.stdout
434 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400435 raise GitError(f"git config {str(args)}: {p.stderr}")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700436
437
Mike Frysingerf841ca42020-02-18 21:31:51 -0500438class RepoConfig(GitConfig):
Gavin Makea2e3302023-03-11 06:46:20 +0000439 """User settings for repo itself."""
Mike Frysingerf841ca42020-02-18 21:31:51 -0500440
Gavin Makea2e3302023-03-11 06:46:20 +0000441 @staticmethod
442 def _getUserConfig():
443 repo_config_dir = os.getenv("REPO_CONFIG_DIR", os.path.expanduser("~"))
444 return os.path.join(repo_config_dir, ".repoconfig/config")
Mike Frysingerf841ca42020-02-18 21:31:51 -0500445
446
Mike Frysingerd4aee652023-10-19 05:13:32 -0400447class RefSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000448 """A Git refspec line, split into its components:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700449
Gavin Makea2e3302023-03-11 06:46:20 +0000450 forced: True if the line starts with '+'
451 src: Left side of the line
452 dst: Right side of the line
453 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700454
Gavin Makea2e3302023-03-11 06:46:20 +0000455 @classmethod
456 def FromString(cls, rs):
457 lhs, rhs = rs.split(":", 2)
458 if lhs.startswith("+"):
459 lhs = lhs[1:]
460 forced = True
461 else:
462 forced = False
463 return cls(forced, lhs, rhs)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700464
Gavin Makea2e3302023-03-11 06:46:20 +0000465 def __init__(self, forced, lhs, rhs):
466 self.forced = forced
467 self.src = lhs
468 self.dst = rhs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700469
Gavin Makea2e3302023-03-11 06:46:20 +0000470 def SourceMatches(self, rev):
471 if self.src:
472 if rev == self.src:
473 return True
474 if self.src.endswith("/*") and rev.startswith(self.src[:-1]):
475 return True
476 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700477
Gavin Makea2e3302023-03-11 06:46:20 +0000478 def DestMatches(self, ref):
479 if self.dst:
480 if ref == self.dst:
481 return True
482 if self.dst.endswith("/*") and ref.startswith(self.dst[:-1]):
483 return True
484 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700485
Gavin Makea2e3302023-03-11 06:46:20 +0000486 def MapSource(self, rev):
487 if self.src.endswith("/*"):
488 return self.dst[:-1] + rev[len(self.src) - 1 :]
489 return self.dst
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700490
Gavin Makea2e3302023-03-11 06:46:20 +0000491 def __str__(self):
492 s = ""
493 if self.forced:
494 s += "+"
495 if self.src:
496 s += self.src
497 if self.dst:
498 s += ":"
499 s += self.dst
500 return s
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700501
502
Gavin Makea2e3302023-03-11 06:46:20 +0000503URI_ALL = re.compile(r"^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/")
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700504
David Pursehouse819827a2020-02-12 15:20:19 +0900505
Shawn O. Pearcef322b9a2011-09-19 14:50:58 -0700506def GetSchemeFromUrl(url):
Gavin Makea2e3302023-03-11 06:46:20 +0000507 m = URI_ALL.match(url)
508 if m:
509 return m.group(1)
510 return None
Shawn O. Pearcef322b9a2011-09-19 14:50:58 -0700511
David Pursehouse819827a2020-02-12 15:20:19 +0900512
Dan Willemsen0745bb22015-08-17 13:41:45 -0700513@contextlib.contextmanager
514def GetUrlCookieFile(url, quiet):
Gavin Makea2e3302023-03-11 06:46:20 +0000515 if url.startswith("persistent-"):
516 try:
517 p = subprocess.Popen(
518 ["git-remote-persistent-https", "-print_config", url],
519 stdin=subprocess.PIPE,
520 stdout=subprocess.PIPE,
521 stderr=subprocess.PIPE,
522 )
523 try:
524 cookieprefix = "http.cookiefile="
525 proxyprefix = "http.proxy="
526 cookiefile = None
527 proxy = None
528 for line in p.stdout:
529 line = line.strip().decode("utf-8")
530 if line.startswith(cookieprefix):
531 cookiefile = os.path.expanduser(
532 line[len(cookieprefix) :]
533 )
534 if line.startswith(proxyprefix):
535 proxy = line[len(proxyprefix) :]
536 # Leave subprocess open, as cookie file may be transient.
537 if cookiefile or proxy:
538 yield cookiefile, proxy
539 return
540 finally:
541 p.stdin.close()
542 if p.wait():
543 err_msg = p.stderr.read().decode("utf-8")
544 if " -print_config" in err_msg:
545 pass # Persistent proxy doesn't support -print_config.
546 elif not quiet:
547 print(err_msg, file=sys.stderr)
548 except OSError as e:
549 if e.errno == errno.ENOENT:
550 pass # No persistent proxy.
551 raise
552 cookiefile = GitConfig.ForUser().GetString("http.cookiefile")
553 if cookiefile:
554 cookiefile = os.path.expanduser(cookiefile)
555 yield cookiefile, None
Dan Willemsen0745bb22015-08-17 13:41:45 -0700556
David Pursehouse819827a2020-02-12 15:20:19 +0900557
Mike Frysingerd4aee652023-10-19 05:13:32 -0400558class Remote:
Gavin Makea2e3302023-03-11 06:46:20 +0000559 """Configuration options related to a remote."""
David Pursehouse819827a2020-02-12 15:20:19 +0900560
Gavin Makea2e3302023-03-11 06:46:20 +0000561 def __init__(self, config, name):
562 self._config = config
563 self.name = name
564 self.url = self._Get("url")
565 self.pushUrl = self._Get("pushurl")
566 self.review = self._Get("review")
567 self.projectname = self._Get("projectname")
568 self.fetch = list(
569 map(RefSpec.FromString, self._Get("fetch", all_keys=True))
570 )
571 self._review_url = None
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800572
Gavin Makea2e3302023-03-11 06:46:20 +0000573 def _InsteadOf(self):
574 globCfg = GitConfig.ForUser()
575 urlList = globCfg.GetSubSections("url")
576 longest = ""
577 longestUrl = ""
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100578
Gavin Makea2e3302023-03-11 06:46:20 +0000579 for url in urlList:
580 key = "url." + url + ".insteadOf"
581 insteadOfList = globCfg.GetString(key, all_keys=True)
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100582
Gavin Makea2e3302023-03-11 06:46:20 +0000583 for insteadOf in insteadOfList:
584 if self.url.startswith(insteadOf) and len(insteadOf) > len(
585 longest
586 ):
587 longest = insteadOf
588 longestUrl = url
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100589
Gavin Makea2e3302023-03-11 06:46:20 +0000590 if len(longest) == 0:
591 return self.url
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100592
Gavin Makea2e3302023-03-11 06:46:20 +0000593 return self.url.replace(longest, longestUrl, 1)
Ulrik Sjolinb6ea3bf2010-01-03 18:20:17 +0100594
Gavin Makea2e3302023-03-11 06:46:20 +0000595 def PreConnectFetch(self, ssh_proxy):
596 """Run any setup for this remote before we connect to it.
Mike Frysinger19e409c2021-05-05 19:44:35 -0400597
Gavin Makea2e3302023-03-11 06:46:20 +0000598 In practice, if the remote is using SSH, we'll attempt to create a new
599 SSH master session to it for reuse across projects.
Mike Frysinger19e409c2021-05-05 19:44:35 -0400600
Gavin Makea2e3302023-03-11 06:46:20 +0000601 Args:
602 ssh_proxy: The SSH settings for managing master sessions.
Mike Frysinger339f2df2021-05-06 00:44:42 -0400603
Gavin Makea2e3302023-03-11 06:46:20 +0000604 Returns:
605 Whether the preconnect phase for this remote was successful.
606 """
607 if not ssh_proxy:
608 return True
Mike Frysinger339f2df2021-05-06 00:44:42 -0400609
Gavin Makea2e3302023-03-11 06:46:20 +0000610 connectionUrl = self._InsteadOf()
611 return ssh_proxy.preconnect(connectionUrl)
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700612
Gavin Makea2e3302023-03-11 06:46:20 +0000613 def ReviewUrl(self, userEmail, validate_certs):
614 if self._review_url is None:
615 if self.review is None:
616 return None
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800617
Gavin Makea2e3302023-03-11 06:46:20 +0000618 u = self.review
619 if u.startswith("persistent-"):
620 u = u[len("persistent-") :]
621 if u.split(":")[0] not in ("http", "https", "sso", "ssh"):
622 u = "http://%s" % u
623 if u.endswith("/Gerrit"):
624 u = u[: len(u) - len("/Gerrit")]
625 if u.endswith("/ssh_info"):
626 u = u[: len(u) - len("/ssh_info")]
627 if not u.endswith("/"):
628 u += "/"
629 http_url = u
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800630
Gavin Makea2e3302023-03-11 06:46:20 +0000631 if u in REVIEW_CACHE:
632 self._review_url = REVIEW_CACHE[u]
633 elif "REPO_HOST_PORT_INFO" in os.environ:
634 host, port = os.environ["REPO_HOST_PORT_INFO"].split()
635 self._review_url = self._SshReviewUrl(userEmail, host, port)
636 REVIEW_CACHE[u] = self._review_url
637 elif u.startswith("sso:") or u.startswith("ssh:"):
638 self._review_url = u # Assume it's right
639 REVIEW_CACHE[u] = self._review_url
640 elif "REPO_IGNORE_SSH_INFO" in os.environ:
641 self._review_url = http_url
642 REVIEW_CACHE[u] = self._review_url
643 else:
644 try:
645 info_url = u + "ssh_info"
646 if not validate_certs:
647 context = ssl._create_unverified_context()
648 info = urllib.request.urlopen(
649 info_url, context=context
650 ).read()
651 else:
652 info = urllib.request.urlopen(info_url).read()
653 if info == b"NOT_AVAILABLE" or b"<" in info:
654 # If `info` contains '<', we assume the server gave us
655 # some sort of HTML response back, like maybe a login
656 # page.
657 #
658 # Assume HTTP if SSH is not enabled or ssh_info doesn't
659 # look right.
660 self._review_url = http_url
661 else:
662 info = info.decode("utf-8")
663 host, port = info.split()
664 self._review_url = self._SshReviewUrl(
665 userEmail, host, port
666 )
667 except urllib.error.HTTPError as e:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400668 raise UploadError(f"{self.review}: {str(e)}")
Gavin Makea2e3302023-03-11 06:46:20 +0000669 except urllib.error.URLError as e:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400670 raise UploadError(f"{self.review}: {str(e)}")
Mike Frysinger06ddc8c2023-08-21 21:26:51 -0400671 except http.client.HTTPException as e:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400672 raise UploadError(f"{self.review}: {e.__class__.__name__}")
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800673
Gavin Makea2e3302023-03-11 06:46:20 +0000674 REVIEW_CACHE[u] = self._review_url
675 return self._review_url + self.projectname
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800676
Gavin Makea2e3302023-03-11 06:46:20 +0000677 def _SshReviewUrl(self, userEmail, host, port):
678 username = self._config.GetString("review.%s.username" % self.review)
679 if username is None:
680 username = userEmail.split("@")[0]
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400681 return f"ssh://{username}@{host}:{port}/"
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700682
Gavin Makea2e3302023-03-11 06:46:20 +0000683 def ToLocal(self, rev):
684 """Convert a remote revision string to something we have locally."""
685 if self.name == "." or IsId(rev):
686 return rev
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700687
Gavin Makea2e3302023-03-11 06:46:20 +0000688 if not rev.startswith("refs/"):
689 rev = R_HEADS + rev
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700690
Gavin Makea2e3302023-03-11 06:46:20 +0000691 for spec in self.fetch:
692 if spec.SourceMatches(rev):
693 return spec.MapSource(rev)
Nasser Grainawi909d58b2014-09-19 12:13:04 -0600694
Gavin Makea2e3302023-03-11 06:46:20 +0000695 if not rev.startswith(R_HEADS):
696 return rev
Nasser Grainawi909d58b2014-09-19 12:13:04 -0600697
Gavin Makea2e3302023-03-11 06:46:20 +0000698 raise GitError(
699 "%s: remote %s does not have %s"
700 % (self.projectname, self.name, rev)
701 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700702
Gavin Makea2e3302023-03-11 06:46:20 +0000703 def WritesTo(self, ref):
704 """True if the remote stores to the tracking ref."""
705 for spec in self.fetch:
706 if spec.DestMatches(ref):
707 return True
708 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700709
Gavin Makea2e3302023-03-11 06:46:20 +0000710 def ResetFetch(self, mirror=False):
711 """Set the fetch refspec to its default value."""
712 if mirror:
713 dst = "refs/heads/*"
714 else:
715 dst = "refs/remotes/%s/*" % self.name
716 self.fetch = [RefSpec(True, "refs/heads/*", dst)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700717
Gavin Makea2e3302023-03-11 06:46:20 +0000718 def Save(self):
719 """Save this remote to the configuration."""
720 self._Set("url", self.url)
721 if self.pushUrl is not None:
722 self._Set("pushurl", self.pushUrl + "/" + self.projectname)
723 else:
724 self._Set("pushurl", self.pushUrl)
725 self._Set("review", self.review)
726 self._Set("projectname", self.projectname)
727 self._Set("fetch", list(map(str, self.fetch)))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700728
Gavin Makea2e3302023-03-11 06:46:20 +0000729 def _Set(self, key, value):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400730 key = f"remote.{self.name}.{key}"
Gavin Makea2e3302023-03-11 06:46:20 +0000731 return self._config.SetString(key, value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700732
Gavin Makea2e3302023-03-11 06:46:20 +0000733 def _Get(self, key, all_keys=False):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400734 key = f"remote.{self.name}.{key}"
Gavin Makea2e3302023-03-11 06:46:20 +0000735 return self._config.GetString(key, all_keys=all_keys)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700736
737
Mike Frysingerd4aee652023-10-19 05:13:32 -0400738class Branch:
Gavin Makea2e3302023-03-11 06:46:20 +0000739 """Configuration options related to a single branch."""
David Pursehouse819827a2020-02-12 15:20:19 +0900740
Gavin Makea2e3302023-03-11 06:46:20 +0000741 def __init__(self, config, name):
742 self._config = config
743 self.name = name
744 self.merge = self._Get("merge")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700745
Gavin Makea2e3302023-03-11 06:46:20 +0000746 r = self._Get("remote")
747 if r:
748 self.remote = self._config.GetRemote(r)
749 else:
750 self.remote = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700751
Gavin Makea2e3302023-03-11 06:46:20 +0000752 @property
753 def LocalMerge(self):
754 """Convert the merge spec to a local name."""
755 if self.remote and self.merge:
756 return self.remote.ToLocal(self.merge)
757 return None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700758
Gavin Makea2e3302023-03-11 06:46:20 +0000759 def Save(self):
760 """Save this branch back into the configuration."""
761 if self._config.HasSection("branch", self.name):
762 if self.remote:
763 self._Set("remote", self.remote.name)
764 else:
765 self._Set("remote", None)
766 self._Set("merge", self.merge)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700767
Gavin Makea2e3302023-03-11 06:46:20 +0000768 else:
769 with open(self._config.file, "a") as fd:
770 fd.write('[branch "%s"]\n' % self.name)
771 if self.remote:
772 fd.write("\tremote = %s\n" % self.remote.name)
773 if self.merge:
774 fd.write("\tmerge = %s\n" % self.merge)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700775
Gavin Makea2e3302023-03-11 06:46:20 +0000776 def _Set(self, key, value):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400777 key = f"branch.{self.name}.{key}"
Gavin Makea2e3302023-03-11 06:46:20 +0000778 return self._config.SetString(key, value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700779
Gavin Makea2e3302023-03-11 06:46:20 +0000780 def _Get(self, key, all_keys=False):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400781 key = f"branch.{self.name}.{key}"
Gavin Makea2e3302023-03-11 06:46:20 +0000782 return self._config.GetString(key, all_keys=all_keys)
Raman Tenneti7954de12021-07-28 14:36:49 -0700783
784
785class SyncAnalysisState:
Gavin Makea2e3302023-03-11 06:46:20 +0000786 """Configuration options related to logging of sync state for analysis.
Raman Tenneti7954de12021-07-28 14:36:49 -0700787
Gavin Makea2e3302023-03-11 06:46:20 +0000788 This object is versioned.
Raman Tenneti7954de12021-07-28 14:36:49 -0700789 """
Raman Tenneti7954de12021-07-28 14:36:49 -0700790
Gavin Makea2e3302023-03-11 06:46:20 +0000791 def __init__(self, config, options, superproject_logging_data):
792 """Initializes SyncAnalysisState.
Raman Tenneti7954de12021-07-28 14:36:49 -0700793
Gavin Makea2e3302023-03-11 06:46:20 +0000794 Saves the following data into the |config| object.
795 - sys.argv, options, superproject's logging data.
796 - repo.*, branch.* and remote.* parameters from config object.
797 - Current time as synctime.
798 - Version number of the object.
Raman Tenneti7954de12021-07-28 14:36:49 -0700799
Gavin Makea2e3302023-03-11 06:46:20 +0000800 All the keys saved by this object are prepended with SYNC_STATE_PREFIX.
Raman Tenneti7954de12021-07-28 14:36:49 -0700801
Gavin Makea2e3302023-03-11 06:46:20 +0000802 Args:
803 config: GitConfig object to store all options.
804 options: Options passed to sync returned from optparse. See
805 _Options().
806 superproject_logging_data: A dictionary of superproject data that is
807 to be logged.
808 """
809 self._config = config
LuK1337aadd12c2023-09-16 09:36:49 +0200810 now = datetime.datetime.now(datetime.timezone.utc)
811 self._Set("main.synctime", now.isoformat(timespec="microseconds"))
Gavin Makea2e3302023-03-11 06:46:20 +0000812 self._Set("main.version", "1")
813 self._Set("sys.argv", sys.argv)
814 for key, value in superproject_logging_data.items():
815 self._Set(f"superproject.{key}", value)
816 for key, value in options.__dict__.items():
817 self._Set(f"options.{key}", value)
818 config_items = config.DumpConfigDict().items()
819 EXTRACT_NAMESPACES = {"repo", "branch", "remote"}
820 self._SetDictionary(
821 {
822 k: v
823 for k, v in config_items
824 if not k.startswith(SYNC_STATE_PREFIX)
825 and k.split(".", 1)[0] in EXTRACT_NAMESPACES
826 }
827 )
Raman Tenneti7954de12021-07-28 14:36:49 -0700828
Gavin Makea2e3302023-03-11 06:46:20 +0000829 def _SetDictionary(self, data):
830 """Save all key/value pairs of |data| dictionary.
831
832 Args:
833 data: A dictionary whose key/value are to be saved.
834 """
835 for key, value in data.items():
836 self._Set(key, value)
837
838 def _Set(self, key, value):
839 """Set the |value| for a |key| in the |_config| member.
840
841 |key| is prepended with the value of SYNC_STATE_PREFIX constant.
842
843 Args:
844 key: Name of the key.
845 value: |value| could be of any type. If it is 'bool', it will be
846 saved as a Boolean and for all other types, it will be saved as
847 a String.
848 """
849 if value is None:
850 return
851 sync_key = f"{SYNC_STATE_PREFIX}{key}"
852 sync_key = sync_key.replace("_", "")
853 if isinstance(value, str):
854 self._config.SetString(sync_key, value)
855 elif isinstance(value, bool):
856 self._config.SetBoolean(sync_key, value)
857 else:
858 self._config.SetString(sync_key, str(value))