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.
- ruyi/__init__.py +21 -0
- ruyi/__main__.py +98 -0
- ruyi/cli/__init__.py +5 -0
- ruyi/cli/builtin_commands.py +14 -0
- ruyi/cli/cmd.py +224 -0
- ruyi/cli/completer.py +50 -0
- ruyi/cli/completion.py +26 -0
- ruyi/cli/config_cli.py +153 -0
- ruyi/cli/main.py +111 -0
- ruyi/cli/self_cli.py +295 -0
- ruyi/cli/user_input.py +127 -0
- ruyi/cli/version_cli.py +45 -0
- ruyi/config/__init__.py +401 -0
- ruyi/config/editor.py +92 -0
- ruyi/config/errors.py +76 -0
- ruyi/config/news.py +39 -0
- ruyi/config/schema.py +197 -0
- ruyi/device/__init__.py +0 -0
- ruyi/device/provision.py +591 -0
- ruyi/device/provision_cli.py +40 -0
- ruyi/log/__init__.py +272 -0
- ruyi/mux/.gitignore +1 -0
- ruyi/mux/__init__.py +0 -0
- ruyi/mux/runtime.py +213 -0
- ruyi/mux/venv/__init__.py +12 -0
- ruyi/mux/venv/emulator_cfg.py +41 -0
- ruyi/mux/venv/maker.py +782 -0
- ruyi/mux/venv/venv_cli.py +92 -0
- ruyi/mux/venv_cfg.py +214 -0
- ruyi/pluginhost/__init__.py +0 -0
- ruyi/pluginhost/api.py +206 -0
- ruyi/pluginhost/ctx.py +222 -0
- ruyi/pluginhost/paths.py +135 -0
- ruyi/pluginhost/plugin_cli.py +37 -0
- ruyi/pluginhost/unsandboxed.py +246 -0
- ruyi/py.typed +0 -0
- ruyi/resource_bundle/__init__.py +20 -0
- ruyi/resource_bundle/__main__.py +55 -0
- ruyi/resource_bundle/data.py +26 -0
- ruyi/ruyipkg/__init__.py +0 -0
- ruyi/ruyipkg/admin_checksum.py +88 -0
- ruyi/ruyipkg/admin_cli.py +83 -0
- ruyi/ruyipkg/atom.py +184 -0
- ruyi/ruyipkg/augmented_pkg.py +212 -0
- ruyi/ruyipkg/canonical_dump.py +320 -0
- ruyi/ruyipkg/checksum.py +39 -0
- ruyi/ruyipkg/cli_completion.py +42 -0
- ruyi/ruyipkg/distfile.py +208 -0
- ruyi/ruyipkg/entity.py +387 -0
- ruyi/ruyipkg/entity_cli.py +123 -0
- ruyi/ruyipkg/entity_provider.py +273 -0
- ruyi/ruyipkg/fetch.py +271 -0
- ruyi/ruyipkg/host.py +55 -0
- ruyi/ruyipkg/install.py +554 -0
- ruyi/ruyipkg/install_cli.py +150 -0
- ruyi/ruyipkg/list.py +126 -0
- ruyi/ruyipkg/list_cli.py +79 -0
- ruyi/ruyipkg/list_filter.py +173 -0
- ruyi/ruyipkg/msg.py +99 -0
- ruyi/ruyipkg/news.py +123 -0
- ruyi/ruyipkg/news_cli.py +78 -0
- ruyi/ruyipkg/news_store.py +183 -0
- ruyi/ruyipkg/pkg_manifest.py +657 -0
- ruyi/ruyipkg/profile.py +208 -0
- ruyi/ruyipkg/profile_cli.py +33 -0
- ruyi/ruyipkg/protocols.py +55 -0
- ruyi/ruyipkg/repo.py +763 -0
- ruyi/ruyipkg/state.py +345 -0
- ruyi/ruyipkg/unpack.py +369 -0
- ruyi/ruyipkg/unpack_method.py +91 -0
- ruyi/ruyipkg/update_cli.py +54 -0
- ruyi/telemetry/__init__.py +0 -0
- ruyi/telemetry/aggregate.py +72 -0
- ruyi/telemetry/event.py +41 -0
- ruyi/telemetry/node_info.py +192 -0
- ruyi/telemetry/provider.py +411 -0
- ruyi/telemetry/scope.py +43 -0
- ruyi/telemetry/store.py +238 -0
- ruyi/telemetry/telemetry_cli.py +127 -0
- ruyi/utils/__init__.py +0 -0
- ruyi/utils/ar.py +74 -0
- ruyi/utils/ci.py +63 -0
- ruyi/utils/frontmatter.py +38 -0
- ruyi/utils/git.py +169 -0
- ruyi/utils/global_mode.py +204 -0
- ruyi/utils/l10n.py +83 -0
- ruyi/utils/markdown.py +73 -0
- ruyi/utils/nuitka.py +33 -0
- ruyi/utils/porcelain.py +51 -0
- ruyi/utils/prereqs.py +77 -0
- ruyi/utils/ssl_patch.py +170 -0
- ruyi/utils/templating.py +34 -0
- ruyi/utils/toml.py +115 -0
- ruyi/utils/url.py +7 -0
- ruyi/utils/xdg_basedir.py +80 -0
- ruyi/version.py +67 -0
- ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
- ruyi-0.39.0.dist-info/METADATA +403 -0
- ruyi-0.39.0.dist-info/RECORD +101 -0
- ruyi-0.39.0.dist-info/WHEEL +4 -0
- 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
|