ruyi 0.39.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. ruyi/__init__.py +21 -0
  2. ruyi/__main__.py +98 -0
  3. ruyi/cli/__init__.py +5 -0
  4. ruyi/cli/builtin_commands.py +14 -0
  5. ruyi/cli/cmd.py +224 -0
  6. ruyi/cli/completer.py +50 -0
  7. ruyi/cli/completion.py +26 -0
  8. ruyi/cli/config_cli.py +153 -0
  9. ruyi/cli/main.py +111 -0
  10. ruyi/cli/self_cli.py +295 -0
  11. ruyi/cli/user_input.py +127 -0
  12. ruyi/cli/version_cli.py +45 -0
  13. ruyi/config/__init__.py +401 -0
  14. ruyi/config/editor.py +92 -0
  15. ruyi/config/errors.py +76 -0
  16. ruyi/config/news.py +39 -0
  17. ruyi/config/schema.py +197 -0
  18. ruyi/device/__init__.py +0 -0
  19. ruyi/device/provision.py +591 -0
  20. ruyi/device/provision_cli.py +40 -0
  21. ruyi/log/__init__.py +272 -0
  22. ruyi/mux/.gitignore +1 -0
  23. ruyi/mux/__init__.py +0 -0
  24. ruyi/mux/runtime.py +213 -0
  25. ruyi/mux/venv/__init__.py +12 -0
  26. ruyi/mux/venv/emulator_cfg.py +41 -0
  27. ruyi/mux/venv/maker.py +782 -0
  28. ruyi/mux/venv/venv_cli.py +92 -0
  29. ruyi/mux/venv_cfg.py +214 -0
  30. ruyi/pluginhost/__init__.py +0 -0
  31. ruyi/pluginhost/api.py +206 -0
  32. ruyi/pluginhost/ctx.py +222 -0
  33. ruyi/pluginhost/paths.py +135 -0
  34. ruyi/pluginhost/plugin_cli.py +37 -0
  35. ruyi/pluginhost/unsandboxed.py +246 -0
  36. ruyi/py.typed +0 -0
  37. ruyi/resource_bundle/__init__.py +20 -0
  38. ruyi/resource_bundle/__main__.py +55 -0
  39. ruyi/resource_bundle/data.py +26 -0
  40. ruyi/ruyipkg/__init__.py +0 -0
  41. ruyi/ruyipkg/admin_checksum.py +88 -0
  42. ruyi/ruyipkg/admin_cli.py +83 -0
  43. ruyi/ruyipkg/atom.py +184 -0
  44. ruyi/ruyipkg/augmented_pkg.py +212 -0
  45. ruyi/ruyipkg/canonical_dump.py +320 -0
  46. ruyi/ruyipkg/checksum.py +39 -0
  47. ruyi/ruyipkg/cli_completion.py +42 -0
  48. ruyi/ruyipkg/distfile.py +208 -0
  49. ruyi/ruyipkg/entity.py +387 -0
  50. ruyi/ruyipkg/entity_cli.py +123 -0
  51. ruyi/ruyipkg/entity_provider.py +273 -0
  52. ruyi/ruyipkg/fetch.py +271 -0
  53. ruyi/ruyipkg/host.py +55 -0
  54. ruyi/ruyipkg/install.py +554 -0
  55. ruyi/ruyipkg/install_cli.py +150 -0
  56. ruyi/ruyipkg/list.py +126 -0
  57. ruyi/ruyipkg/list_cli.py +79 -0
  58. ruyi/ruyipkg/list_filter.py +173 -0
  59. ruyi/ruyipkg/msg.py +99 -0
  60. ruyi/ruyipkg/news.py +123 -0
  61. ruyi/ruyipkg/news_cli.py +78 -0
  62. ruyi/ruyipkg/news_store.py +183 -0
  63. ruyi/ruyipkg/pkg_manifest.py +657 -0
  64. ruyi/ruyipkg/profile.py +208 -0
  65. ruyi/ruyipkg/profile_cli.py +33 -0
  66. ruyi/ruyipkg/protocols.py +55 -0
  67. ruyi/ruyipkg/repo.py +763 -0
  68. ruyi/ruyipkg/state.py +345 -0
  69. ruyi/ruyipkg/unpack.py +369 -0
  70. ruyi/ruyipkg/unpack_method.py +91 -0
  71. ruyi/ruyipkg/update_cli.py +54 -0
  72. ruyi/telemetry/__init__.py +0 -0
  73. ruyi/telemetry/aggregate.py +72 -0
  74. ruyi/telemetry/event.py +41 -0
  75. ruyi/telemetry/node_info.py +192 -0
  76. ruyi/telemetry/provider.py +411 -0
  77. ruyi/telemetry/scope.py +43 -0
  78. ruyi/telemetry/store.py +238 -0
  79. ruyi/telemetry/telemetry_cli.py +127 -0
  80. ruyi/utils/__init__.py +0 -0
  81. ruyi/utils/ar.py +74 -0
  82. ruyi/utils/ci.py +63 -0
  83. ruyi/utils/frontmatter.py +38 -0
  84. ruyi/utils/git.py +169 -0
  85. ruyi/utils/global_mode.py +204 -0
  86. ruyi/utils/l10n.py +83 -0
  87. ruyi/utils/markdown.py +73 -0
  88. ruyi/utils/nuitka.py +33 -0
  89. ruyi/utils/porcelain.py +51 -0
  90. ruyi/utils/prereqs.py +77 -0
  91. ruyi/utils/ssl_patch.py +170 -0
  92. ruyi/utils/templating.py +34 -0
  93. ruyi/utils/toml.py +115 -0
  94. ruyi/utils/url.py +7 -0
  95. ruyi/utils/xdg_basedir.py +80 -0
  96. ruyi/version.py +67 -0
  97. ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
  98. ruyi-0.39.0.dist-info/METADATA +403 -0
  99. ruyi-0.39.0.dist-info/RECORD +101 -0
  100. ruyi-0.39.0.dist-info/WHEEL +4 -0
  101. ruyi-0.39.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,127 @@
1
+ import argparse
2
+ import datetime
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ..cli.cmd import RootCommand
6
+
7
+ if TYPE_CHECKING:
8
+ from ..cli.completion import ArgumentParser
9
+ from ..config import GlobalConfig
10
+
11
+
12
+ # Telemetry preference commands
13
+ class TelemetryCommand(
14
+ RootCommand,
15
+ cmd="telemetry",
16
+ has_subcommands=True,
17
+ help="Manage your telemetry preferences",
18
+ ):
19
+ @classmethod
20
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
21
+ pass
22
+
23
+
24
+ class TelemetryConsentCommand(
25
+ TelemetryCommand,
26
+ cmd="consent",
27
+ aliases=["on"],
28
+ help="Give consent to telemetry data uploads",
29
+ ):
30
+ @classmethod
31
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
32
+ pass
33
+
34
+ @classmethod
35
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
36
+ from .provider import set_telemetry_mode
37
+
38
+ now = datetime.datetime.now().astimezone()
39
+ set_telemetry_mode(cfg, "on", now)
40
+ return 0
41
+
42
+
43
+ class TelemetryLocalCommand(
44
+ TelemetryCommand,
45
+ cmd="local",
46
+ help="Set telemetry mode to local collection only",
47
+ ):
48
+ @classmethod
49
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
50
+ pass
51
+
52
+ @classmethod
53
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
54
+ from .provider import set_telemetry_mode
55
+
56
+ set_telemetry_mode(cfg, "local")
57
+ return 0
58
+
59
+
60
+ class TelemetryOptoutCommand(
61
+ TelemetryCommand,
62
+ cmd="optout",
63
+ aliases=["off"],
64
+ help="Opt out of telemetry data collection",
65
+ ):
66
+ @classmethod
67
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
68
+ pass
69
+
70
+ @classmethod
71
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
72
+ from .provider import set_telemetry_mode
73
+
74
+ set_telemetry_mode(cfg, "off")
75
+ return 0
76
+
77
+
78
+ class TelemetryStatusCommand(
79
+ TelemetryCommand,
80
+ cmd="status",
81
+ help="Print the current telemetry mode",
82
+ ):
83
+ @classmethod
84
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
85
+ p.add_argument(
86
+ "--verbose",
87
+ "-v",
88
+ action="store_true",
89
+ help="Enable verbose output",
90
+ )
91
+
92
+ @classmethod
93
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
94
+ verbose: bool = args.verbose
95
+ if not verbose:
96
+ cfg.logger.stdout(cfg.telemetry_mode)
97
+ return 0
98
+
99
+ if cfg.telemetry is None:
100
+ cfg.logger.I(
101
+ "telemetry mode is [green]off[/]: no further data will be collected"
102
+ )
103
+ return 0
104
+
105
+ cfg.telemetry.print_telemetry_notice(for_cli_verbose_output=True)
106
+ return 0
107
+
108
+
109
+ class TelemetryUploadCommand(
110
+ TelemetryCommand,
111
+ cmd="upload",
112
+ help="Upload collected telemetry data now",
113
+ ):
114
+ @classmethod
115
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
116
+ pass
117
+
118
+ @classmethod
119
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
120
+ if cfg.telemetry is None:
121
+ cfg.logger.W("telemetry is disabled, nothing to upload")
122
+ return 0
123
+
124
+ cfg.telemetry.flush(upload_now=True)
125
+ # disable the flush at program exit because we have just done that
126
+ cfg.telemetry.discard_events()
127
+ return 0
ruyi/utils/__init__.py ADDED
File without changes
ruyi/utils/ar.py ADDED
@@ -0,0 +1,74 @@
1
+ from contextlib import AbstractContextManager
2
+ from typing import BinaryIO, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from types import TracebackType
6
+
7
+ import arpy
8
+
9
+
10
+ class ArpyArchiveWrapper(arpy.Archive, AbstractContextManager["arpy.Archive"]):
11
+ """Compatibility shim for arpy.Archive, for easy interop with both arpy 1.x
12
+ and 2.x."""
13
+
14
+ def __init__(
15
+ self,
16
+ filename: str | None = None,
17
+ fileobj: BinaryIO | None = None,
18
+ ) -> None:
19
+ super().__init__(filename=filename, fileobj=fileobj)
20
+
21
+ def __enter__(self) -> arpy.Archive:
22
+ if hasattr(super(), "__enter__"):
23
+ # in case we're working with a newer arpy version that has a
24
+ # non-trivial __enter__ implementation
25
+ return super().__enter__()
26
+
27
+ # backport of arpy 2.x __enter__ implementation
28
+ return self
29
+
30
+ def __exit__(
31
+ self,
32
+ exc_type: type[BaseException] | None,
33
+ exc_value: BaseException | None,
34
+ traceback: "TracebackType | None",
35
+ ) -> None:
36
+ if hasattr(super(), "__exit__"):
37
+ return super().__exit__(exc_type, exc_value, traceback)
38
+
39
+ # backport of arpy 2.x __exit__ implementation
40
+ self.close()
41
+
42
+ def infolist(self) -> list[arpy.ArchiveFileHeader]:
43
+ if hasattr(super(), "infolist"):
44
+ return super().infolist()
45
+
46
+ # backport of arpy 2.x infolist()
47
+ self.read_all_headers()
48
+ return [
49
+ header
50
+ for header in self.headers
51
+ if header.type
52
+ in (
53
+ arpy.HEADER_BSD,
54
+ arpy.HEADER_NORMAL,
55
+ arpy.HEADER_GNU,
56
+ )
57
+ ]
58
+
59
+ def open(self, name: bytes | arpy.ArchiveFileHeader) -> arpy.ArchiveFileData:
60
+ if hasattr(super(), "open"):
61
+ return super().open(name)
62
+
63
+ # backport of arpy 2.x open()
64
+ if isinstance(name, bytes):
65
+ ar_file = self.archived_files.get(name)
66
+ if ar_file is None:
67
+ raise KeyError("There is no item named %r in the archive" % (name,))
68
+
69
+ return ar_file
70
+
71
+ if name not in self.headers:
72
+ raise KeyError("Provided header does not match this archive")
73
+
74
+ return arpy.ArchiveFileData(ar_obj=self, header=name)
ruyi/utils/ci.py ADDED
@@ -0,0 +1,63 @@
1
+ from typing import Mapping
2
+
3
+
4
+ def is_running_in_ci(os_environ: Mapping[str, str]) -> bool:
5
+ """Simplified and quick CI check meant for basic judgement."""
6
+ if os_environ.get("CI", "") == "true":
7
+ return True
8
+ elif os_environ.get("TF_BUILD", "") == "True":
9
+ return True
10
+ return False
11
+
12
+
13
+ def probe_for_ci(os_environ: Mapping[str, str]) -> str | None:
14
+ # https://www.appveyor.com/docs/environment-variables/
15
+ if os_environ.get("APPVEYOR", "").lower() == "true":
16
+ return "appveyor"
17
+ # https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#system-variables-devops-services
18
+ elif os_environ.get("TF_BUILD", "") == "True":
19
+ return "azure"
20
+ # https://circleci.com/docs/variables/#built-in-environment-variables
21
+ elif os_environ.get("CIRCLECI", "") == "true":
22
+ return "circleci"
23
+ # https://cirrus-ci.org/guide/writing-tasks/#environment-variables
24
+ elif os_environ.get("CIRRUS_CI", "") == "true":
25
+ return "cirrus"
26
+ # https://gitea.com/gitea/act_runner/pulls/113
27
+ # this should be checked before GHA because upstream maintains compatibility
28
+ # with GHA by also providing GHA-style preset variables
29
+ # TODO: also detect Forgejo
30
+ elif os_environ.get("GITEA_ACTIONS", "") == "true":
31
+ return "gitea"
32
+ # https://gitee.com/help/articles/4358#article-header8
33
+ elif "GITEE_PIPELINE_NAME" in os_environ:
34
+ return "gitee"
35
+ # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
36
+ elif os_environ.get("GITHUB_ACTIONS", "") == "true":
37
+ return "github"
38
+ # https://docs.gitlab.com/ee/ci/variables/predefined_variables.html#predefined-variables
39
+ elif os_environ.get("GITLAB_CI", "") == "true":
40
+ return "gitlab"
41
+ # https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
42
+ # may have false-negatives but likely no false-positives
43
+ elif "JENKINS_URL" in os_environ:
44
+ return "jenkins"
45
+ # https://gitee.com/openeuler/mugen
46
+ # seems nothing except $OET_PATH is guaranteed
47
+ elif "OET_PATH" in os_environ:
48
+ return "mugen"
49
+ # there seems to be no designated marker for openQA, test a couple of
50
+ # hopefully ubiquitous variables to avoid going through the entire key set
51
+ elif "OPENQA_CONFIG" in os_environ or "OPENQA_URL" in os_environ:
52
+ return "openqa"
53
+ # https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
54
+ elif os_environ.get("TRAVIS", "") == "true":
55
+ return "travis"
56
+ # https://docs.koderover.com/zadig/Zadig%20v3.1/project/build/
57
+ # https://github.com/koderover/zadig/blob/v3.1.0/pkg/microservice/jobexecutor/core/service/job.go#L117
58
+ elif os_environ.get("ZADIG", "") == "true":
59
+ return "zadig"
60
+ elif os_environ.get("CI", "") == "true":
61
+ return "unidentified"
62
+
63
+ return None
@@ -0,0 +1,38 @@
1
+ # Minimal frontmatter support for Markdown, because [python-frontmatter] is
2
+ # not packaged in major Linux distributions, complicating packaging work.
3
+ #
4
+ # Only the YAML frontmatter is supported here, unlike python-frontmatter
5
+ # which supports additionally JSON and TOML frontmatter formats.
6
+ #
7
+ # [python-frontmatter]: https://github.com/eyeseast/python-frontmatter
8
+
9
+ import re
10
+ from typing import Final
11
+ import yaml
12
+
13
+
14
+ FRONTMATTER_BOUNDARY_RE: Final = re.compile(r"(?m)^-{3,}\s*$")
15
+
16
+
17
+ class Post:
18
+ def __init__(self, metadata: dict[str, object] | None, content: str) -> None:
19
+ self._md = metadata
20
+ self.content = content
21
+
22
+ def get(self, key: str) -> object | None:
23
+ return None if self._md is None else self._md.get(key)
24
+
25
+
26
+ def loads(s: str) -> Post:
27
+ m = FRONTMATTER_BOUNDARY_RE.match(s)
28
+ if m is None:
29
+ return Post(None, s)
30
+
31
+ x = FRONTMATTER_BOUNDARY_RE.split(s, 2)
32
+ if len(x) != 3:
33
+ return Post(None, s)
34
+
35
+ fm, content = x[1], x[2]
36
+
37
+ metadata = yaml.safe_load(fm)
38
+ return Post(metadata, content)
ruyi/utils/git.py ADDED
@@ -0,0 +1,169 @@
1
+ from contextlib import AbstractContextManager
2
+ import pathlib
3
+ from typing import Any, TYPE_CHECKING
4
+
5
+ from pygit2 import GitError, Oid
6
+ from pygit2.callbacks import RemoteCallbacks
7
+ from pygit2.repository import Repository
8
+
9
+ try:
10
+ from pygit2.remotes import TransferProgress
11
+ except ModuleNotFoundError:
12
+ # pygit2 < 1.14.0
13
+ # see https://github.com/libgit2/pygit2/commit/a8b2421bea550292
14
+ #
15
+ # import-untyped: the current pygit2 type stubs were written after the
16
+ # `remote` -> `remotes` rename, so no stubs for it
17
+ from pygit2.remote import TransferProgress # type: ignore[import-not-found,import-untyped,no-redef,unused-ignore]
18
+
19
+ # for compatibility with <1.14.0, cannot `from pygit2.enums import MergeAnalysis`
20
+ # see https://github.com/libgit2/pygit2/pull/1251
21
+ from pygit2 import (
22
+ GIT_MERGE_ANALYSIS_UNBORN,
23
+ GIT_MERGE_ANALYSIS_FASTFORWARD,
24
+ GIT_MERGE_ANALYSIS_UP_TO_DATE,
25
+ )
26
+
27
+ from rich.progress import Progress, TaskID
28
+ from rich.text import Text
29
+
30
+ if TYPE_CHECKING:
31
+ from typing_extensions import Self
32
+
33
+ from ..log import RuyiLogger
34
+
35
+
36
+ def human_readable_path_of_repo(repo: Repository) -> pathlib.Path:
37
+ """
38
+ Returns a human-readable path of the repository.
39
+ If the repository is a submodule, returns the path to the parent module.
40
+ """
41
+ repo_path = pathlib.Path(repo.path)
42
+ return repo_path.parent if repo_path.name == ".git" else repo_path
43
+
44
+
45
+ class RemoteGitProgressIndicator(
46
+ RemoteCallbacks,
47
+ AbstractContextManager["RemoteGitProgressIndicator"],
48
+ ):
49
+ def __init__(self) -> None:
50
+ super().__init__()
51
+ self.p = Progress()
52
+ self.task: TaskID | None = None
53
+ self._last_stats: TransferProgress | None = None
54
+ self._task_name: str = ""
55
+
56
+ def __enter__(self) -> "Self":
57
+ self.p.__enter__()
58
+ return self
59
+
60
+ def __exit__(self, exc_type: Any, exc_value: Any, tb: Any) -> None:
61
+ return self.p.__exit__(exc_type, exc_value, tb)
62
+
63
+ # Compatibility with pygit2 < 1.8.0.
64
+ def progress(self, string: str) -> None:
65
+ return self.sideband_progress(string)
66
+
67
+ def sideband_progress(self, string: str) -> None:
68
+ self.p.console.print("\r", Text(string), sep="", end="")
69
+
70
+ def transfer_progress(self, stats: TransferProgress) -> None:
71
+ new_phase = False
72
+ task_name: str = self._task_name
73
+ total: int = 0
74
+ completed: int = 0
75
+
76
+ if (
77
+ self._last_stats is None
78
+ or self._last_stats.received_objects != stats.received_objects
79
+ ):
80
+ task_name = "transferring objects"
81
+ total = stats.total_objects
82
+ completed = stats.received_objects
83
+ elif self._last_stats.indexed_deltas != stats.indexed_deltas:
84
+ task_name = "processing deltas"
85
+ total = stats.total_deltas
86
+ completed = stats.indexed_deltas
87
+ elif self._last_stats.received_bytes != stats.received_bytes:
88
+ # we don't render the received size at the moment
89
+ pass
90
+
91
+ new_phase = self._task_name != task_name
92
+ if new_phase:
93
+ self.task = self.p.add_task(task_name, total=total, completed=completed)
94
+ self._task_name = task_name
95
+ else:
96
+ if self.task is not None:
97
+ self.p.update(self.task, total=total, completed=completed)
98
+
99
+ self._last_stats = stats
100
+
101
+
102
+ # based on https://stackoverflow.com/questions/27749418/implementing-pull-with-pygit2
103
+ def pull_ff_or_die(
104
+ logger: RuyiLogger,
105
+ repo: Repository,
106
+ remote_name: str,
107
+ remote_url: str,
108
+ branch_name: str,
109
+ *,
110
+ allow_auto_management: bool,
111
+ ) -> None:
112
+ remote = repo.remotes[remote_name]
113
+ if remote.url != remote_url:
114
+ if not allow_auto_management:
115
+ logger.F(
116
+ f"URL of remote '[yellow]{remote_name}[/]' does not match expected URL"
117
+ )
118
+ repo_path = human_readable_path_of_repo(repo)
119
+ logger.I(f"repository: [yellow]{repo_path}[/]")
120
+ logger.I(f"expected remote URL: [yellow]{remote_url}[/]")
121
+ logger.I(f"actual remote URL: [yellow]{remote.url}[/]")
122
+ logger.I("please fix the repo settings manually")
123
+ raise SystemExit(1)
124
+
125
+ logger.D(
126
+ f"updating url of remote {remote_name} from {remote.url} to {remote_url}"
127
+ )
128
+ repo.remotes.set_url("origin", remote_url)
129
+
130
+ logger.D("fetching")
131
+ try:
132
+ with RemoteGitProgressIndicator() as pr:
133
+ remote.fetch(callbacks=pr)
134
+ except GitError as e:
135
+ logger.F(f"failed to fetch from remote URL {remote_url}: {e}")
136
+ raise SystemExit(1) from e
137
+
138
+ remote_head_ref = repo.lookup_reference(f"refs/remotes/{remote_name}/{branch_name}")
139
+ remote_head: Oid
140
+ if isinstance(remote_head_ref.target, Oid):
141
+ remote_head = remote_head_ref.target
142
+ else:
143
+ assert isinstance(remote_head_ref.target, str)
144
+ remote_head = Oid(hex=remote_head_ref.target)
145
+
146
+ merge_analysis, _ = repo.merge_analysis(remote_head)
147
+
148
+ if merge_analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE:
149
+ # nothing to do
150
+ logger.D("repo state already up-to-date")
151
+ return
152
+
153
+ if merge_analysis & (GIT_MERGE_ANALYSIS_UNBORN | GIT_MERGE_ANALYSIS_FASTFORWARD):
154
+ # simple fast-forwarding is enough in both cases
155
+ logger.D(f"fast-forwarding repo to {remote_head}")
156
+ tgt = repo.get(remote_head)
157
+ assert tgt is not None
158
+ repo.checkout_tree(tgt)
159
+
160
+ logger.D(f"updating branch {branch_name} HEAD")
161
+ local_branch_ref = repo.lookup_reference(f"refs/heads/{branch_name}")
162
+ local_branch_ref.set_target(remote_head)
163
+ repo.head.set_target(remote_head)
164
+ return
165
+
166
+ # cannot handle these cases
167
+ logger.F("cannot fast-forward repo to newly fetched state")
168
+ logger.I("manual intervention is required to avoid data loss")
169
+ raise SystemExit(1)
@@ -0,0 +1,204 @@
1
+ import abc
2
+ import os
3
+ from typing import Final, Mapping, Protocol, runtime_checkable
4
+
5
+ import ruyi
6
+
7
+ ENV_DEBUG: Final = "RUYI_DEBUG"
8
+ ENV_EXPERIMENTAL: Final = "RUYI_EXPERIMENTAL"
9
+ ENV_FORCE_ALLOW_ROOT: Final = "RUYI_FORCE_ALLOW_ROOT"
10
+ ENV_TELEMETRY_OPTOUT_KEY: Final = "RUYI_TELEMETRY_OPTOUT"
11
+ ENV_VENV_ROOT_KEY: Final = "RUYI_VENV"
12
+
13
+ TRUTHY_ENV_VAR_VALUES: Final = {"1", "true", "x", "y", "yes"}
14
+
15
+
16
+ def is_env_var_truthy(env: Mapping[str, str], var: str) -> bool:
17
+ if v := env.get(var):
18
+ return v.lower() in TRUTHY_ENV_VAR_VALUES
19
+ return False
20
+
21
+
22
+ @runtime_checkable
23
+ class ProvidesGlobalMode(Protocol):
24
+ @property
25
+ def argv0(self) -> str: ...
26
+
27
+ @property
28
+ def main_file(self) -> str: ...
29
+
30
+ @property
31
+ def self_exe(self) -> str: ...
32
+
33
+ @property
34
+ def is_debug(self) -> bool: ...
35
+
36
+ @property
37
+ def is_experimental(self) -> bool: ...
38
+
39
+ @property
40
+ def is_packaged(self) -> bool: ...
41
+
42
+ @property
43
+ def is_porcelain(self) -> bool: ...
44
+
45
+ @property
46
+ def is_telemetry_optout(self) -> bool: ...
47
+
48
+ @property
49
+ def is_cli_autocomplete(self) -> bool: ...
50
+
51
+ @property
52
+ def venv_root(self) -> str | None: ...
53
+
54
+
55
+ class GlobalModeProvider(metaclass=abc.ABCMeta):
56
+ """
57
+ Abstract base class for global mode providers.
58
+ """
59
+
60
+ @property
61
+ @abc.abstractmethod
62
+ def argv0(self) -> str:
63
+ return ""
64
+
65
+ @property
66
+ @abc.abstractmethod
67
+ def main_file(self) -> str:
68
+ return ""
69
+
70
+ @property
71
+ @abc.abstractmethod
72
+ def self_exe(self) -> str:
73
+ return ""
74
+
75
+ def record_self_exe(self, argv0: str, main_file: str, self_exe: str) -> None:
76
+ pass
77
+
78
+ @property
79
+ @abc.abstractmethod
80
+ def is_debug(self) -> bool:
81
+ return False
82
+
83
+ @property
84
+ @abc.abstractmethod
85
+ def is_experimental(self) -> bool:
86
+ return False
87
+
88
+ @property
89
+ @abc.abstractmethod
90
+ def is_packaged(self) -> bool:
91
+ return False
92
+
93
+ @property
94
+ @abc.abstractmethod
95
+ def is_porcelain(self) -> bool:
96
+ return False
97
+
98
+ @is_porcelain.setter
99
+ @abc.abstractmethod
100
+ def is_porcelain(self, v: bool) -> None:
101
+ pass
102
+
103
+ @property
104
+ @abc.abstractmethod
105
+ def is_telemetry_optout(self) -> bool:
106
+ return False
107
+
108
+ @property
109
+ @abc.abstractmethod
110
+ def is_cli_autocomplete(self) -> bool:
111
+ return False
112
+
113
+ @property
114
+ @abc.abstractmethod
115
+ def venv_root(self) -> str | None:
116
+ return None
117
+
118
+
119
+ def _guess_porcelain_from_argv(argv: list[str]) -> bool:
120
+ """
121
+ Guess if the current invocation is a "porcelain" command based on the
122
+ arguments passed, without requiring the ``argparse`` machinery to be
123
+ completely initialized.
124
+ """
125
+ # If the first argument is `--porcelain`, we assume it's a porcelain command.
126
+ # This is currently accurate as the porcelain flag is only possible at this
127
+ # position right now.
128
+ return len(argv) > 1 and argv[1] == "--porcelain"
129
+
130
+
131
+ class EnvGlobalModeProvider(GlobalModeProvider):
132
+ def __init__(
133
+ self,
134
+ env: Mapping[str, str] | None = None,
135
+ argv: list[str] | None = None,
136
+ ) -> None:
137
+ if env is None:
138
+ env = os.environ
139
+ if argv is None:
140
+ argv = []
141
+
142
+ self._argv0 = ""
143
+ self._main_file = ""
144
+ self._self_exe = ""
145
+
146
+ self._is_debug = is_env_var_truthy(env, ENV_DEBUG)
147
+ self._is_experimental = is_env_var_truthy(env, ENV_EXPERIMENTAL)
148
+ self._is_porcelain = _guess_porcelain_from_argv(argv)
149
+ self._is_telemetry_optout = is_env_var_truthy(env, ENV_TELEMETRY_OPTOUT_KEY)
150
+
151
+ # We have to lift this piece of implementation detail out of argcomplete,
152
+ # as the argcomplete import is very costly in terms of startup time.
153
+ self._is_cli_autocomplete = "_ARGCOMPLETE" in os.environ
154
+
155
+ self._venv_root = env.get(ENV_VENV_ROOT_KEY)
156
+
157
+ @property
158
+ def argv0(self) -> str:
159
+ return self._argv0
160
+
161
+ @property
162
+ def main_file(self) -> str:
163
+ return self._main_file
164
+
165
+ @property
166
+ def self_exe(self) -> str:
167
+ return self._self_exe
168
+
169
+ def record_self_exe(self, argv0: str, main_file: str, self_exe: str) -> None:
170
+ self._argv0 = argv0
171
+ self._main_file = main_file
172
+ self._self_exe = self_exe
173
+
174
+ @property
175
+ def is_debug(self) -> bool:
176
+ return self._is_debug
177
+
178
+ @property
179
+ def is_experimental(self) -> bool:
180
+ return self._is_experimental
181
+
182
+ @property
183
+ def is_packaged(self) -> bool:
184
+ return hasattr(ruyi, "__compiled__")
185
+
186
+ @property
187
+ def is_porcelain(self) -> bool:
188
+ return self._is_porcelain
189
+
190
+ @is_porcelain.setter
191
+ def is_porcelain(self, v: bool) -> None:
192
+ self._is_porcelain = v
193
+
194
+ @property
195
+ def is_telemetry_optout(self) -> bool:
196
+ return self._is_telemetry_optout
197
+
198
+ @property
199
+ def is_cli_autocomplete(self) -> bool:
200
+ return self._is_cli_autocomplete
201
+
202
+ @property
203
+ def venv_root(self) -> str | None:
204
+ return self._venv_root