pyquipu-cli 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/PKG-INFO +7 -7
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/pyproject.toml +7 -8
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/axon.py +6 -5
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/cache.py +11 -10
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/export.py +11 -10
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/helpers.py +6 -5
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/navigation.py +37 -26
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/query.py +9 -8
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/remote.py +24 -23
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/run.py +11 -10
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/show.py +16 -10
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/ui.py +6 -5
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/workspace.py +13 -11
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/main.py +1 -1
- pyquipu_cli-0.3.0/src/quipu/cli/rendering.py +28 -0
- pyquipu_cli-0.3.0/src/quipu/cli/rendering.stitcher.yaml +6 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/tui.py +14 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/ui_utils.py +7 -4
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/stitcher.lock +14 -4
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_cache_commands.py +13 -12
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_cli_interaction.py +5 -5
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_export_command.py +7 -6
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_navigation_commands.py +24 -3
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_query_commands.py +5 -4
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_unfriendly_paths.py +5 -4
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_workspace_commands.py +5 -4
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_tui_reachability.py +7 -7
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_view_model.py +3 -3
- pyquipu_cli-0.2.0/src/quipu/cli/rendering.py +0 -18
- pyquipu_cli-0.2.0/src/quipu/cli/rendering.stitcher.yaml +0 -2
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/.gitignore +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/__init__.py +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/__init__.py +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/cache.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/export.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/helpers.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/query.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/commands/show.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/config.py +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/config.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/logger_config.py +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/logger_config.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/tui.css +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/tui.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/ui_utils.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/view_model.py +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/src/quipu/cli/view_model.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/conftest.py +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_cache_commands.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_cli_interaction.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_export_command.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_query_commands.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/integration/test_unfriendly_paths.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_tui_logic.py +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_tui_logic.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_tui_reachability.stitcher.yaml +0 -0
- {pyquipu_cli-0.2.0 → pyquipu_cli-0.3.0}/tests/unit/test_view_model.stitcher.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyquipu-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The command-line interface for Quipu.
|
|
5
5
|
Project-URL: Homepage, https://github.com/quipu-dev/Quipu
|
|
6
6
|
Project-URL: Repository, https://github.com/quipu-dev/Quipu
|
|
@@ -20,11 +20,11 @@ Classifier: Topic :: Software Development :: Version Control
|
|
|
20
20
|
Classifier: Topic :: Text Processing
|
|
21
21
|
Classifier: Typing :: Typed
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist: pyquipu-
|
|
25
|
-
Requires-Dist: pyquipu-common~=0.
|
|
26
|
-
Requires-Dist: pyquipu-engine~=0.
|
|
27
|
-
Requires-Dist: pyquipu-runtime~=0.
|
|
28
|
-
Requires-Dist: pyquipu-spec~=0.1.
|
|
23
|
+
Requires-Dist: click>=8.1.0
|
|
24
|
+
Requires-Dist: pyquipu-application~=0.3.0
|
|
25
|
+
Requires-Dist: pyquipu-common~=0.2.0
|
|
26
|
+
Requires-Dist: pyquipu-engine~=0.3.0
|
|
27
|
+
Requires-Dist: pyquipu-runtime~=0.3.0
|
|
28
|
+
Requires-Dist: pyquipu-spec~=0.1.1
|
|
29
29
|
Requires-Dist: textual>=0.58.0
|
|
30
30
|
Requires-Dist: typer>=0.9.0
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyquipu-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
authors = [{ name="doucx", email="doucxldh@gmail.com" }]
|
|
9
9
|
description = "The command-line interface for Quipu."
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -25,14 +25,14 @@ classifiers = [
|
|
|
25
25
|
keywords = ["quipu", "devops", "cli", "tui", "git", "history"]
|
|
26
26
|
|
|
27
27
|
dependencies = [
|
|
28
|
-
"pyquipu-spec ~= 0.1.
|
|
29
|
-
"pyquipu-engine ~= 0.
|
|
30
|
-
"pyquipu-runtime ~= 0.
|
|
31
|
-
"pyquipu-common ~= 0.
|
|
32
|
-
"pyquipu-application ~= 0.
|
|
33
|
-
"pyquipu-bus ~= 0.2.0",
|
|
28
|
+
"pyquipu-spec ~= 0.1.1",
|
|
29
|
+
"pyquipu-engine ~= 0.3.0",
|
|
30
|
+
"pyquipu-runtime ~= 0.3.0",
|
|
31
|
+
"pyquipu-common ~= 0.2.0",
|
|
32
|
+
"pyquipu-application ~= 0.3.0",
|
|
34
33
|
"typer >= 0.9.0",
|
|
35
34
|
"textual >= 0.58.0",
|
|
35
|
+
"click >= 8.1.0",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[project.urls]
|
|
@@ -46,7 +46,6 @@ pyquipu-engine = { workspace = true }
|
|
|
46
46
|
pyquipu-runtime = { workspace = true }
|
|
47
47
|
pyquipu-common = { workspace = true }
|
|
48
48
|
pyquipu-application = { workspace = true }
|
|
49
|
-
pyquipu-bus = { workspace = true }
|
|
50
49
|
|
|
51
50
|
[project.scripts]
|
|
52
51
|
quipu = "quipu.cli.main:app"
|
|
@@ -5,7 +5,8 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Annotated, Optional
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
|
-
from
|
|
8
|
+
from needle.pointer import L
|
|
9
|
+
from quipu.common.bus import bus
|
|
9
10
|
|
|
10
11
|
from ..config import DEFAULT_ENTRY_FILE, DEFAULT_WORK_DIR
|
|
11
12
|
from ..logger_config import setup_logging
|
|
@@ -39,13 +40,13 @@ def register(app: typer.Typer):
|
|
|
39
40
|
if list_acts:
|
|
40
41
|
from quipu.application.controller import get_available_acts
|
|
41
42
|
|
|
42
|
-
bus.info(
|
|
43
|
+
bus.info(L.axon.listActs.ui.header)
|
|
43
44
|
acts = get_available_acts(work_dir)
|
|
44
45
|
for name in sorted(acts.keys()):
|
|
45
46
|
doc = acts[name]
|
|
46
47
|
clean_doc = inspect.cleandoc(doc) if doc else "暂无说明"
|
|
47
48
|
indented_doc = "\n".join(f" {line}" for line in clean_doc.splitlines())
|
|
48
|
-
item_header = bus.
|
|
49
|
+
item_header = bus.render_to_string(L.axon.listActs.ui.actItem, name=name)
|
|
49
50
|
bus.data(f"{item_header}\n{indented_doc}\n")
|
|
50
51
|
ctx.exit(0)
|
|
51
52
|
|
|
@@ -56,7 +57,7 @@ def register(app: typer.Typer):
|
|
|
56
57
|
source_desc = ""
|
|
57
58
|
if file:
|
|
58
59
|
if not file.exists():
|
|
59
|
-
bus.error(
|
|
60
|
+
bus.error(L.common.error.fileNotFound, path=file)
|
|
60
61
|
ctx.exit(1)
|
|
61
62
|
content = file.read_text(encoding="utf-8")
|
|
62
63
|
source_desc = f"文件 ({file.name})"
|
|
@@ -74,7 +75,7 @@ def register(app: typer.Typer):
|
|
|
74
75
|
source_desc = f"默认文件 ({DEFAULT_ENTRY_FILE.name})"
|
|
75
76
|
|
|
76
77
|
if not content.strip():
|
|
77
|
-
bus.warning(
|
|
78
|
+
bus.warning(L.axon.warning.noInput)
|
|
78
79
|
ctx.exit(0)
|
|
79
80
|
|
|
80
81
|
# --- 2. 委托给应用层服务 (核心重构) ---
|
|
@@ -3,7 +3,8 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Annotated
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
|
-
from
|
|
6
|
+
from needle.pointer import L
|
|
7
|
+
from quipu.common.bus import bus
|
|
7
8
|
|
|
8
9
|
from ..config import DEFAULT_WORK_DIR
|
|
9
10
|
from ..logger_config import setup_logging
|
|
@@ -24,14 +25,14 @@ def cache_sync(
|
|
|
24
25
|
),
|
|
25
26
|
] = DEFAULT_WORK_DIR,
|
|
26
27
|
):
|
|
27
|
-
bus.info(
|
|
28
|
+
bus.info(L.cache.sync.info.hydrating)
|
|
28
29
|
try:
|
|
29
30
|
with engine_context(work_dir):
|
|
30
31
|
pass
|
|
31
|
-
bus.success(
|
|
32
|
+
bus.success(L.cache.sync.success)
|
|
32
33
|
except Exception as e:
|
|
33
34
|
logger.error("数据同步失败", exc_info=True)
|
|
34
|
-
bus.error(
|
|
35
|
+
bus.error(L.cache.sync.error, error=str(e))
|
|
35
36
|
ctx.exit(1)
|
|
36
37
|
|
|
37
38
|
|
|
@@ -49,7 +50,7 @@ def cache_rebuild(
|
|
|
49
50
|
setup_logging()
|
|
50
51
|
db_path = work_dir.resolve() / ".quipu" / "history.sqlite"
|
|
51
52
|
if not db_path.exists():
|
|
52
|
-
bus.warning(
|
|
53
|
+
bus.warning(L.cache.rebuild.info.dbNotFound)
|
|
53
54
|
cache_sync(ctx, work_dir)
|
|
54
55
|
|
|
55
56
|
|
|
@@ -66,12 +67,12 @@ def cache_prune_refs(
|
|
|
66
67
|
setup_logging()
|
|
67
68
|
|
|
68
69
|
with engine_context(work_dir) as engine:
|
|
69
|
-
bus.info(
|
|
70
|
+
bus.info(L.cache.prune.info.scanning)
|
|
70
71
|
|
|
71
72
|
# 1. 获取所有本地 heads
|
|
72
73
|
local_heads = engine.git_db.get_all_ref_heads("refs/quipu/local/heads/")
|
|
73
74
|
if not local_heads:
|
|
74
|
-
bus.success(
|
|
75
|
+
bus.success(L.cache.prune.info.noRedundant)
|
|
75
76
|
return
|
|
76
77
|
|
|
77
78
|
head_commits = {h[0] for h in local_heads}
|
|
@@ -93,7 +94,7 @@ def cache_prune_refs(
|
|
|
93
94
|
redundant_commits = head_commits.intersection(parents_of_heads)
|
|
94
95
|
|
|
95
96
|
if not redundant_commits:
|
|
96
|
-
bus.success(
|
|
97
|
+
bus.success(L.cache.prune.info.noRedundant)
|
|
97
98
|
return
|
|
98
99
|
|
|
99
100
|
# 4. 找出对应的 ref names 并删除
|
|
@@ -102,12 +103,12 @@ def cache_prune_refs(
|
|
|
102
103
|
if c_hash in redundant_commits:
|
|
103
104
|
refs_to_delete.append(ref_name)
|
|
104
105
|
|
|
105
|
-
bus.info(
|
|
106
|
+
bus.info(L.cache.prune.info.found, count=len(refs_to_delete), total=len(local_heads))
|
|
106
107
|
|
|
107
108
|
deleted_count = 0
|
|
108
109
|
for ref in refs_to_delete:
|
|
109
110
|
engine.git_db.delete_ref(ref)
|
|
110
111
|
deleted_count += 1
|
|
111
112
|
|
|
112
|
-
bus.success(
|
|
113
|
+
bus.success(L.cache.prune.success, count=deleted_count)
|
|
113
114
|
return
|
|
@@ -6,7 +6,8 @@ from typing import Annotated, Dict, List, Optional, Set
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
import yaml
|
|
9
|
-
from
|
|
9
|
+
from needle.pointer import L
|
|
10
|
+
from quipu.common.bus import bus
|
|
10
11
|
from quipu.engine.state_machine import Engine
|
|
11
12
|
from quipu.spec.models.graph import QuipuNode
|
|
12
13
|
|
|
@@ -157,7 +158,7 @@ def register(app: typer.Typer):
|
|
|
157
158
|
|
|
158
159
|
with engine_context(work_dir) as engine:
|
|
159
160
|
if not engine.history_graph:
|
|
160
|
-
bus.info(
|
|
161
|
+
bus.info(L.export.info.emptyHistory)
|
|
161
162
|
ctx.exit(0)
|
|
162
163
|
|
|
163
164
|
nodes_to_process = sorted(engine.history_graph.values(), key=lambda n: n.timestamp, reverse=True)
|
|
@@ -171,22 +172,22 @@ def register(app: typer.Typer):
|
|
|
171
172
|
filtered = filter_nodes(nodes_to_process, limit, since, until)
|
|
172
173
|
nodes_to_export = list(reversed(filtered))
|
|
173
174
|
except typer.BadParameter as e:
|
|
174
|
-
bus.error(
|
|
175
|
+
bus.error(L.export.error.badParam, error=str(e))
|
|
175
176
|
ctx.exit(1)
|
|
176
177
|
|
|
177
178
|
if not nodes_to_export:
|
|
178
|
-
bus.info(
|
|
179
|
+
bus.info(L.export.info.noMatchingNodes)
|
|
179
180
|
ctx.exit(0)
|
|
180
181
|
|
|
181
182
|
if output_dir.exists() and any(output_dir.iterdir()):
|
|
182
|
-
prompt = bus.
|
|
183
|
+
prompt = bus.render_to_string(L.export.prompt.overwrite, path=output_dir)
|
|
183
184
|
if not prompt_for_confirmation(prompt, default=False):
|
|
184
|
-
bus.warning(
|
|
185
|
+
bus.warning(L.common.prompt.cancel)
|
|
185
186
|
raise typer.Abort()
|
|
186
187
|
shutil.rmtree(output_dir)
|
|
187
188
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
188
189
|
|
|
189
|
-
bus.info(
|
|
190
|
+
bus.info(L.export.info.starting, count=len(nodes_to_export), path=output_dir)
|
|
190
191
|
|
|
191
192
|
# 预计算文件名和节点集合以供导航栏使用
|
|
192
193
|
filename_map = {node.commit_hash: _generate_filename(node) for node in nodes_to_export}
|
|
@@ -201,9 +202,9 @@ def register(app: typer.Typer):
|
|
|
201
202
|
(output_dir / filename).write_text(content, encoding="utf-8")
|
|
202
203
|
|
|
203
204
|
if zip_output:
|
|
204
|
-
bus.info(
|
|
205
|
+
bus.info(L.export.info.zipping)
|
|
205
206
|
zip_path = shutil.make_archive(str(output_dir), "zip", output_dir)
|
|
206
207
|
shutil.rmtree(output_dir)
|
|
207
|
-
bus.success(
|
|
208
|
+
bus.success(L.export.success.zip, path=zip_path)
|
|
208
209
|
else:
|
|
209
|
-
bus.success(
|
|
210
|
+
bus.success(L.export.success.dir)
|
|
@@ -6,7 +6,8 @@ from typing import Dict, Generator, List, Optional
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
from quipu.application.factory import create_engine
|
|
9
|
-
from
|
|
9
|
+
from needle.pointer import L
|
|
10
|
+
from quipu.common.bus import bus
|
|
10
11
|
from quipu.engine.state_machine import Engine
|
|
11
12
|
from quipu.spec.models.graph import QuipuNode
|
|
12
13
|
|
|
@@ -34,8 +35,8 @@ def _find_current_node(engine: Engine, graph: Dict[str, QuipuNode]) -> Optional[
|
|
|
34
35
|
if node.output_tree == current_hash:
|
|
35
36
|
return node
|
|
36
37
|
|
|
37
|
-
bus.warning(
|
|
38
|
-
bus.info(
|
|
38
|
+
bus.warning(L.navigation.warning.workspaceDirty)
|
|
39
|
+
bus.info(L.navigation.info.saveHint)
|
|
39
40
|
return None
|
|
40
41
|
|
|
41
42
|
|
|
@@ -43,10 +44,10 @@ def _execute_visit(ctx: typer.Context, engine: Engine, target_hash: str, msg_id:
|
|
|
43
44
|
bus.info(msg_id, **kwargs)
|
|
44
45
|
try:
|
|
45
46
|
engine.visit(target_hash)
|
|
46
|
-
bus.success(
|
|
47
|
+
bus.success(L.navigation.success.visit, short_hash=target_hash[:7])
|
|
47
48
|
except Exception as e:
|
|
48
49
|
logger.error(f"导航操作失败 (目标哈希: {target_hash[:12]})", exc_info=True)
|
|
49
|
-
bus.error(
|
|
50
|
+
bus.error(L.navigation.error.generic, error=str(e))
|
|
50
51
|
ctx.exit(1)
|
|
51
52
|
|
|
52
53
|
|
|
@@ -3,7 +3,8 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Annotated
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
|
-
from
|
|
6
|
+
from needle.pointer import L
|
|
7
|
+
from quipu.common.bus import bus
|
|
7
8
|
|
|
8
9
|
from ..config import DEFAULT_WORK_DIR
|
|
9
10
|
from ..ui_utils import prompt_for_confirmation
|
|
@@ -28,38 +29,48 @@ def register(app: typer.Typer):
|
|
|
28
29
|
with engine_context(work_dir) as engine:
|
|
29
30
|
graph = engine.history_graph
|
|
30
31
|
|
|
31
|
-
matches = [
|
|
32
|
+
matches = [
|
|
33
|
+
node
|
|
34
|
+
for node in graph.values()
|
|
35
|
+
if node.commit_hash.startswith(hash_prefix) or node.output_tree.startswith(hash_prefix)
|
|
36
|
+
]
|
|
32
37
|
if not matches:
|
|
33
|
-
bus.error(
|
|
38
|
+
bus.error(L.navigation.checkout.error.notFound, hash_prefix=hash_prefix)
|
|
34
39
|
ctx.exit(1)
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
|
|
41
|
+
unique_output_trees = {node.output_tree for node in matches}
|
|
42
|
+
if len(unique_output_trees) > 1:
|
|
43
|
+
bus.error(L.navigation.checkout.error.notUnique, hash_prefix=hash_prefix, count=len(matches))
|
|
37
44
|
ctx.exit(1)
|
|
45
|
+
|
|
46
|
+
if len(matches) > 1:
|
|
47
|
+
matches.sort(key=lambda n: (1 if n.parent else 0, n.timestamp), reverse=True)
|
|
48
|
+
|
|
38
49
|
target_node = matches[0]
|
|
39
50
|
target_output_tree_hash = target_node.output_tree
|
|
40
51
|
|
|
41
52
|
current_hash = engine.git_db.get_tree_hash()
|
|
42
53
|
if current_hash == target_output_tree_hash:
|
|
43
|
-
bus.success(
|
|
54
|
+
bus.success(L.navigation.checkout.info.noAction, short_hash=target_node.short_hash)
|
|
44
55
|
ctx.exit(0)
|
|
45
56
|
|
|
46
57
|
is_dirty = engine.current_node is None or engine.current_node.output_tree != current_hash
|
|
47
58
|
if is_dirty:
|
|
48
|
-
bus.warning(
|
|
59
|
+
bus.warning(L.navigation.checkout.info.capturingDrift)
|
|
49
60
|
engine.capture_drift(current_hash)
|
|
50
|
-
bus.success(
|
|
61
|
+
bus.success(L.navigation.checkout.success.driftCaptured)
|
|
51
62
|
current_hash = engine.git_db.get_tree_hash()
|
|
52
63
|
|
|
53
64
|
diff_stat_str = engine.git_db.get_diff_stat(current_hash, target_output_tree_hash)
|
|
54
65
|
|
|
55
66
|
if not force:
|
|
56
|
-
prompt = bus.
|
|
57
|
-
|
|
67
|
+
prompt = bus.render_to_string(
|
|
68
|
+
L.navigation.checkout.prompt.confirm,
|
|
58
69
|
short_hash=target_node.short_hash,
|
|
59
70
|
timestamp=target_node.timestamp,
|
|
60
71
|
)
|
|
61
72
|
if not prompt_for_confirmation(prompt, diff_lines=diff_stat_str.splitlines(), default=False):
|
|
62
|
-
bus.warning(
|
|
73
|
+
bus.warning(L.common.prompt.cancel)
|
|
63
74
|
raise typer.Abort()
|
|
64
75
|
|
|
65
76
|
_execute_visit(
|
|
@@ -85,9 +96,9 @@ def register(app: typer.Typer):
|
|
|
85
96
|
for i in range(count):
|
|
86
97
|
if not target_node.parent:
|
|
87
98
|
if i > 0:
|
|
88
|
-
bus.success(
|
|
99
|
+
bus.success(L.navigation.undo.reachedRoot, steps=i)
|
|
89
100
|
else:
|
|
90
|
-
bus.success(
|
|
101
|
+
bus.success(L.navigation.undo.atRoot)
|
|
91
102
|
if target_node == current_node:
|
|
92
103
|
ctx.exit(0)
|
|
93
104
|
break
|
|
@@ -116,15 +127,15 @@ def register(app: typer.Typer):
|
|
|
116
127
|
for i in range(count):
|
|
117
128
|
if not target_node.children:
|
|
118
129
|
if i > 0:
|
|
119
|
-
bus.success(
|
|
130
|
+
bus.success(L.navigation.redo.reachedEnd, steps=i)
|
|
120
131
|
else:
|
|
121
|
-
bus.success(
|
|
132
|
+
bus.success(L.navigation.redo.atEnd)
|
|
122
133
|
if target_node == current_node:
|
|
123
134
|
ctx.exit(0)
|
|
124
135
|
break
|
|
125
136
|
target_node = target_node.children[-1]
|
|
126
137
|
if len(current_node.children) > 1:
|
|
127
|
-
bus.info(
|
|
138
|
+
bus.info(L.navigation.redo.info.multiBranch, short_hash=target_node.short_hash)
|
|
128
139
|
|
|
129
140
|
_execute_visit(
|
|
130
141
|
ctx,
|
|
@@ -146,12 +157,12 @@ def register(app: typer.Typer):
|
|
|
146
157
|
ctx.exit(1)
|
|
147
158
|
siblings = current_node.siblings
|
|
148
159
|
if len(siblings) <= 1:
|
|
149
|
-
bus.success(
|
|
160
|
+
bus.success(L.navigation.prev.noSiblings)
|
|
150
161
|
ctx.exit(0)
|
|
151
162
|
try:
|
|
152
163
|
idx = siblings.index(current_node)
|
|
153
164
|
if idx == 0:
|
|
154
|
-
bus.success(
|
|
165
|
+
bus.success(L.navigation.prev.atOldest)
|
|
155
166
|
ctx.exit(0)
|
|
156
167
|
target_node = siblings[idx - 1]
|
|
157
168
|
_execute_visit(
|
|
@@ -176,12 +187,12 @@ def register(app: typer.Typer):
|
|
|
176
187
|
ctx.exit(1)
|
|
177
188
|
siblings = current_node.siblings
|
|
178
189
|
if len(siblings) <= 1:
|
|
179
|
-
bus.success(
|
|
190
|
+
bus.success(L.navigation.next.noSiblings)
|
|
180
191
|
ctx.exit(0)
|
|
181
192
|
try:
|
|
182
193
|
idx = siblings.index(current_node)
|
|
183
194
|
if idx == len(siblings) - 1:
|
|
184
|
-
bus.success(
|
|
195
|
+
bus.success(L.navigation.next.atNewest)
|
|
185
196
|
ctx.exit(0)
|
|
186
197
|
target_node = siblings[idx + 1]
|
|
187
198
|
_execute_visit(
|
|
@@ -203,12 +214,12 @@ def register(app: typer.Typer):
|
|
|
203
214
|
try:
|
|
204
215
|
result_hash = engine.back()
|
|
205
216
|
if result_hash:
|
|
206
|
-
bus.success(
|
|
217
|
+
bus.success(L.navigation.back.success, short_hash=result_hash[:7])
|
|
207
218
|
else:
|
|
208
|
-
bus.warning(
|
|
219
|
+
bus.warning(L.navigation.back.atStart)
|
|
209
220
|
except Exception as e:
|
|
210
221
|
logger.error("后退操作失败", exc_info=True)
|
|
211
|
-
bus.error(
|
|
222
|
+
bus.error(L.navigation.back.error, error=str(e))
|
|
212
223
|
ctx.exit(1)
|
|
213
224
|
|
|
214
225
|
@app.command(help="在访问历史中前进一步。")
|
|
@@ -220,10 +231,10 @@ def register(app: typer.Typer):
|
|
|
220
231
|
try:
|
|
221
232
|
result_hash = engine.forward()
|
|
222
233
|
if result_hash:
|
|
223
|
-
bus.success(
|
|
234
|
+
bus.success(L.navigation.forward.success, short_hash=result_hash[:7])
|
|
224
235
|
else:
|
|
225
|
-
bus.warning(
|
|
236
|
+
bus.warning(L.navigation.forward.atEnd)
|
|
226
237
|
except Exception as e:
|
|
227
238
|
logger.error("前进操作失败", exc_info=True)
|
|
228
|
-
bus.error(
|
|
239
|
+
bus.error(L.navigation.forward.error, error=str(e))
|
|
229
240
|
ctx.exit(1)
|
|
@@ -5,7 +5,8 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Annotated, List, Optional
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
|
-
from
|
|
8
|
+
from needle.pointer import L
|
|
9
|
+
from quipu.common.bus import bus
|
|
9
10
|
from quipu.spec.models.graph import QuipuNode
|
|
10
11
|
|
|
11
12
|
from ..config import DEFAULT_WORK_DIR
|
|
@@ -58,7 +59,7 @@ def register(app: typer.Typer):
|
|
|
58
59
|
if json_output:
|
|
59
60
|
bus.data("[]")
|
|
60
61
|
else:
|
|
61
|
-
bus.info(
|
|
62
|
+
bus.info(L.query.info.emptyHistory)
|
|
62
63
|
raise typer.Exit(0)
|
|
63
64
|
|
|
64
65
|
nodes_to_process = sorted(graph.values(), key=lambda n: n.timestamp, reverse=True)
|
|
@@ -69,21 +70,21 @@ def register(app: typer.Typer):
|
|
|
69
70
|
try:
|
|
70
71
|
nodes = filter_nodes(nodes_to_process, limit, since, until)
|
|
71
72
|
except typer.BadParameter as e:
|
|
72
|
-
bus.error(
|
|
73
|
+
bus.error(L.common.error.invalidConfig, error=str(e))
|
|
73
74
|
ctx.exit(1)
|
|
74
75
|
|
|
75
76
|
if not nodes:
|
|
76
77
|
if json_output:
|
|
77
78
|
bus.data("[]")
|
|
78
79
|
else:
|
|
79
|
-
bus.info(
|
|
80
|
+
bus.info(L.query.info.noResults)
|
|
80
81
|
raise typer.Exit(0)
|
|
81
82
|
|
|
82
83
|
if json_output:
|
|
83
84
|
bus.data(_nodes_to_json_str(nodes))
|
|
84
85
|
raise typer.Exit(0)
|
|
85
86
|
|
|
86
|
-
bus.info(
|
|
87
|
+
bus.info(L.query.log.ui.header)
|
|
87
88
|
for node in nodes:
|
|
88
89
|
ts = node.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
89
90
|
tag = f"[{node.node_type.upper()}]"
|
|
@@ -111,7 +112,7 @@ def register(app: typer.Typer):
|
|
|
111
112
|
if json_output:
|
|
112
113
|
bus.data("[]")
|
|
113
114
|
else:
|
|
114
|
-
bus.info(
|
|
115
|
+
bus.info(L.query.info.emptyHistory)
|
|
115
116
|
ctx.exit(0)
|
|
116
117
|
|
|
117
118
|
nodes = engine.find_nodes(summary_regex=summary_regex, node_type=node_type, limit=limit)
|
|
@@ -120,14 +121,14 @@ def register(app: typer.Typer):
|
|
|
120
121
|
if json_output:
|
|
121
122
|
bus.data("[]")
|
|
122
123
|
else:
|
|
123
|
-
bus.info(
|
|
124
|
+
bus.info(L.query.info.noResults)
|
|
124
125
|
ctx.exit(0)
|
|
125
126
|
|
|
126
127
|
if json_output:
|
|
127
128
|
bus.data(_nodes_to_json_str(nodes))
|
|
128
129
|
ctx.exit(0)
|
|
129
130
|
|
|
130
|
-
bus.info(
|
|
131
|
+
bus.info(L.query.find.ui.header)
|
|
131
132
|
for node in nodes:
|
|
132
133
|
ts = node.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
133
134
|
tag = f"[{node.node_type.upper()}]"
|
|
@@ -5,7 +5,8 @@ from typing import Annotated, Optional
|
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
from quipu.application.utils import find_git_repository_root
|
|
8
|
-
from
|
|
8
|
+
from needle.pointer import L
|
|
9
|
+
from quipu.common.bus import bus
|
|
9
10
|
from quipu.common.identity import get_user_id_from_email
|
|
10
11
|
from quipu.engine.config import ConfigManager
|
|
11
12
|
from quipu.engine.git_db import GitDB
|
|
@@ -52,7 +53,7 @@ def register(app: typer.Typer):
|
|
|
52
53
|
|
|
53
54
|
final_user_id = config.get("sync.user_id")
|
|
54
55
|
if not final_user_id:
|
|
55
|
-
bus.info(
|
|
56
|
+
bus.info(L.sync.setup.firstUse)
|
|
56
57
|
try:
|
|
57
58
|
result = subprocess.run(
|
|
58
59
|
["git", "config", "user.email"], cwd=sync_dir, capture_output=True, text=True, check=True
|
|
@@ -64,11 +65,11 @@ def register(app: typer.Typer):
|
|
|
64
65
|
final_user_id = get_user_id_from_email(email)
|
|
65
66
|
config.set("sync.user_id", final_user_id)
|
|
66
67
|
config.save()
|
|
67
|
-
bus.success(
|
|
68
|
+
bus.success(L.sync.setup.success, email=email, user_id=final_user_id)
|
|
68
69
|
|
|
69
70
|
except (subprocess.CalledProcessError, ValueError, FileNotFoundError):
|
|
70
|
-
bus.error(
|
|
71
|
-
bus.warning(
|
|
71
|
+
bus.error(L.sync.setup.error.noEmail)
|
|
72
|
+
bus.warning(L.sync.setup.info.emailHint)
|
|
72
73
|
ctx.exit(1)
|
|
73
74
|
|
|
74
75
|
try:
|
|
@@ -77,50 +78,50 @@ def register(app: typer.Typer):
|
|
|
77
78
|
target_ids_to_fetch = set(subscriptions)
|
|
78
79
|
target_ids_to_fetch.add(final_user_id)
|
|
79
80
|
|
|
80
|
-
bus.info(
|
|
81
|
+
bus.info(L.sync.run.info.mode, mode=mode.value)
|
|
81
82
|
|
|
82
83
|
# --- Operation Dispatch based on Mode ---
|
|
83
84
|
match mode:
|
|
84
85
|
case SyncMode.BIDIRECTIONAL:
|
|
85
|
-
bus.info(
|
|
86
|
+
bus.info(L.sync.run.info.pulling)
|
|
86
87
|
for target_id in sorted(list(target_ids_to_fetch)):
|
|
87
88
|
git_db.fetch_quipu_refs(remote, target_id)
|
|
88
|
-
bus.info(
|
|
89
|
+
bus.info(L.sync.run.info.reconciling)
|
|
89
90
|
git_db.reconcile_local_with_remote(remote, final_user_id)
|
|
90
|
-
bus.info(
|
|
91
|
+
bus.info(L.sync.run.info.pushing)
|
|
91
92
|
git_db.push_quipu_refs(remote, final_user_id)
|
|
92
|
-
bus.success(
|
|
93
|
+
bus.success(L.sync.run.success.bidirectional)
|
|
93
94
|
|
|
94
95
|
case SyncMode.PULL_ONLY:
|
|
95
|
-
bus.info(
|
|
96
|
+
bus.info(L.sync.run.info.pulling)
|
|
96
97
|
for target_id in sorted(list(target_ids_to_fetch)):
|
|
97
98
|
git_db.fetch_quipu_refs(remote, target_id)
|
|
98
|
-
bus.info(
|
|
99
|
+
bus.info(L.sync.run.info.reconciling)
|
|
99
100
|
git_db.reconcile_local_with_remote(remote, final_user_id)
|
|
100
|
-
bus.success(
|
|
101
|
+
bus.success(L.sync.run.success.pullOnly)
|
|
101
102
|
|
|
102
103
|
case SyncMode.PULL_PRUNE:
|
|
103
|
-
bus.info(
|
|
104
|
+
bus.info(L.sync.run.info.pullingPrune)
|
|
104
105
|
for target_id in sorted(list(target_ids_to_fetch)):
|
|
105
106
|
git_db.fetch_quipu_refs(remote, target_id)
|
|
106
|
-
bus.info(
|
|
107
|
+
bus.info(L.sync.run.info.reconciling)
|
|
107
108
|
git_db.reconcile_local_with_remote(remote, final_user_id)
|
|
108
|
-
bus.info(
|
|
109
|
+
bus.info(L.sync.run.info.pruning)
|
|
109
110
|
git_db.prune_local_from_remote(remote, final_user_id)
|
|
110
|
-
bus.success(
|
|
111
|
+
bus.success(L.sync.run.success.pullPrune)
|
|
111
112
|
|
|
112
113
|
case SyncMode.PUSH_ONLY:
|
|
113
|
-
bus.info(
|
|
114
|
+
bus.info(L.sync.run.info.pushing)
|
|
114
115
|
git_db.push_quipu_refs(remote, final_user_id, force=False)
|
|
115
|
-
bus.success(
|
|
116
|
+
bus.success(L.sync.run.success.pushOnly)
|
|
116
117
|
|
|
117
118
|
case SyncMode.PUSH_FORCE:
|
|
118
|
-
bus.info(
|
|
119
|
+
bus.info(L.sync.run.info.pushingForce)
|
|
119
120
|
git_db.push_quipu_refs(remote, final_user_id, force=True)
|
|
120
|
-
bus.success(
|
|
121
|
+
bus.success(L.sync.run.success.pushForce)
|
|
121
122
|
|
|
122
|
-
bus.info(
|
|
123
|
+
bus.info(L.sync.run.info.cacheHint)
|
|
123
124
|
|
|
124
125
|
except RuntimeError as e:
|
|
125
|
-
bus.error(
|
|
126
|
+
bus.error(L.sync.run.error.generic, error=str(e))
|
|
126
127
|
ctx.exit(1)
|