codealmanac 0.1.0.dev0__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.
- codealmanac/__init__.py +13 -0
- codealmanac/app.py +175 -0
- codealmanac/cli/__init__.py +1 -0
- codealmanac/cli/dispatch/__init__.py +0 -0
- codealmanac/cli/dispatch/admin.py +124 -0
- codealmanac/cli/dispatch/config.py +50 -0
- codealmanac/cli/dispatch/root.py +328 -0
- codealmanac/cli/main.py +28 -0
- codealmanac/cli/parser/__init__.py +0 -0
- codealmanac/cli/parser/admin.py +81 -0
- codealmanac/cli/parser/lifecycle.py +57 -0
- codealmanac/cli/parser/root.py +19 -0
- codealmanac/cli/parser/wiki.py +87 -0
- codealmanac/cli/render/__init__.py +0 -0
- codealmanac/cli/render/admin.py +191 -0
- codealmanac/cli/render/root.py +290 -0
- codealmanac/core/__init__.py +1 -0
- codealmanac/core/errors.py +45 -0
- codealmanac/core/models.py +14 -0
- codealmanac/core/paths.py +25 -0
- codealmanac/core/slug.py +7 -0
- codealmanac/core/text.py +5 -0
- codealmanac/database/__init__.py +15 -0
- codealmanac/database/sqlite.py +54 -0
- codealmanac/integrations/__init__.py +1 -0
- codealmanac/integrations/automation/__init__.py +3 -0
- codealmanac/integrations/automation/scheduler/__init__.py +5 -0
- codealmanac/integrations/automation/scheduler/launchd.py +163 -0
- codealmanac/integrations/command.py +56 -0
- codealmanac/integrations/harnesses/__init__.py +7 -0
- codealmanac/integrations/harnesses/claude/__init__.py +1 -0
- codealmanac/integrations/harnesses/claude/adapter.py +217 -0
- codealmanac/integrations/harnesses/codex/__init__.py +3 -0
- codealmanac/integrations/harnesses/codex/adapter.py +221 -0
- codealmanac/integrations/harnesses/git_status.py +49 -0
- codealmanac/integrations/sources/__init__.py +29 -0
- codealmanac/integrations/sources/filesystem/__init__.py +5 -0
- codealmanac/integrations/sources/filesystem/adapter.py +685 -0
- codealmanac/integrations/sources/filesystem/selection.py +209 -0
- codealmanac/integrations/sources/git/__init__.py +3 -0
- codealmanac/integrations/sources/git/adapter.py +132 -0
- codealmanac/integrations/sources/github/__init__.py +3 -0
- codealmanac/integrations/sources/github/adapter.py +413 -0
- codealmanac/integrations/sources/runtime.py +22 -0
- codealmanac/integrations/sources/transcripts/__init__.py +33 -0
- codealmanac/integrations/sources/transcripts/claude.py +61 -0
- codealmanac/integrations/sources/transcripts/codex.py +69 -0
- codealmanac/integrations/sources/transcripts/jsonl.py +84 -0
- codealmanac/integrations/sources/transcripts/runtime.py +387 -0
- codealmanac/integrations/sources/web/__init__.py +3 -0
- codealmanac/integrations/sources/web/adapter.py +303 -0
- codealmanac/integrations/updates/__init__.py +7 -0
- codealmanac/integrations/updates/package.py +85 -0
- codealmanac/integrations/workspaces/__init__.py +1 -0
- codealmanac/integrations/workspaces/git/__init__.py +3 -0
- codealmanac/integrations/workspaces/git/probe.py +128 -0
- codealmanac/manual/README.md +24 -0
- codealmanac/manual/__init__.py +19 -0
- codealmanac/manual/build.md +20 -0
- codealmanac/manual/evidence.md +23 -0
- codealmanac/manual/garden.md +20 -0
- codealmanac/manual/ingest.md +17 -0
- codealmanac/manual/library.py +84 -0
- codealmanac/manual/models.py +83 -0
- codealmanac/manual/pages.md +28 -0
- codealmanac/manual/requests.py +6 -0
- codealmanac/manual/sources.md +18 -0
- codealmanac/manual/style.md +19 -0
- codealmanac/prompts/__init__.py +5 -0
- codealmanac/prompts/base/notability.md +14 -0
- codealmanac/prompts/base/purpose.md +23 -0
- codealmanac/prompts/base/syntax.md +19 -0
- codealmanac/prompts/models.py +9 -0
- codealmanac/prompts/operations/garden.md +26 -0
- codealmanac/prompts/operations/ingest.md +18 -0
- codealmanac/prompts/renderer.py +24 -0
- codealmanac/prompts/requests.py +22 -0
- codealmanac/server/__init__.py +1 -0
- codealmanac/server/app.py +202 -0
- codealmanac/server/assets/__init__.py +1 -0
- codealmanac/server/assets/app.css +865 -0
- codealmanac/server/assets/app.js +3 -0
- codealmanac/server/assets/index.html +80 -0
- codealmanac/server/assets/viewer/api.js +30 -0
- codealmanac/server/assets/viewer/components.js +197 -0
- codealmanac/server/assets/viewer/main.js +126 -0
- codealmanac/server/assets/viewer/renderers.js +122 -0
- codealmanac/server/assets/viewer/routes.js +36 -0
- codealmanac/services/__init__.py +1 -0
- codealmanac/services/automation/__init__.py +3 -0
- codealmanac/services/automation/models.py +83 -0
- codealmanac/services/automation/ports.py +14 -0
- codealmanac/services/automation/requests.py +40 -0
- codealmanac/services/automation/service.py +294 -0
- codealmanac/services/config/__init__.py +17 -0
- codealmanac/services/config/models.py +61 -0
- codealmanac/services/config/requests.py +21 -0
- codealmanac/services/config/service.py +55 -0
- codealmanac/services/config/store.py +26 -0
- codealmanac/services/diagnostics/__init__.py +1 -0
- codealmanac/services/diagnostics/models.py +22 -0
- codealmanac/services/diagnostics/requests.py +8 -0
- codealmanac/services/diagnostics/service.py +283 -0
- codealmanac/services/harnesses/__init__.py +1 -0
- codealmanac/services/harnesses/models.py +104 -0
- codealmanac/services/harnesses/ports.py +18 -0
- codealmanac/services/harnesses/requests.py +19 -0
- codealmanac/services/harnesses/service.py +38 -0
- codealmanac/services/health/__init__.py +1 -0
- codealmanac/services/health/requests.py +8 -0
- codealmanac/services/health/service.py +20 -0
- codealmanac/services/index/__init__.py +1 -0
- codealmanac/services/index/models.py +135 -0
- codealmanac/services/index/requests.py +26 -0
- codealmanac/services/index/service.py +86 -0
- codealmanac/services/index/store.py +411 -0
- codealmanac/services/index/views.py +524 -0
- codealmanac/services/pages/__init__.py +1 -0
- codealmanac/services/pages/requests.py +17 -0
- codealmanac/services/pages/service.py +26 -0
- codealmanac/services/runs/__init__.py +1 -0
- codealmanac/services/runs/models.py +91 -0
- codealmanac/services/runs/requests.py +76 -0
- codealmanac/services/runs/service.py +86 -0
- codealmanac/services/runs/store.py +256 -0
- codealmanac/services/search/__init__.py +1 -0
- codealmanac/services/search/requests.py +23 -0
- codealmanac/services/search/service.py +31 -0
- codealmanac/services/sources/__init__.py +1 -0
- codealmanac/services/sources/models.py +126 -0
- codealmanac/services/sources/ports.py +30 -0
- codealmanac/services/sources/requests.py +76 -0
- codealmanac/services/sources/service.py +351 -0
- codealmanac/services/tagging/__init__.py +1 -0
- codealmanac/services/tagging/models.py +9 -0
- codealmanac/services/tagging/requests.py +35 -0
- codealmanac/services/tagging/service.py +43 -0
- codealmanac/services/topics/__init__.py +1 -0
- codealmanac/services/topics/models.py +36 -0
- codealmanac/services/topics/requests.py +115 -0
- codealmanac/services/topics/service.py +297 -0
- codealmanac/services/updates/__init__.py +4 -0
- codealmanac/services/updates/models.py +83 -0
- codealmanac/services/updates/ports.py +17 -0
- codealmanac/services/updates/requests.py +10 -0
- codealmanac/services/updates/service.py +113 -0
- codealmanac/services/viewer/__init__.py +1 -0
- codealmanac/services/viewer/models.py +80 -0
- codealmanac/services/viewer/renderer.py +89 -0
- codealmanac/services/viewer/requests.py +86 -0
- codealmanac/services/viewer/service.py +211 -0
- codealmanac/services/wiki/__init__.py +1 -0
- codealmanac/services/wiki/documents.py +83 -0
- codealmanac/services/wiki/frontmatter.py +94 -0
- codealmanac/services/wiki/frontmatter_rewrite.py +142 -0
- codealmanac/services/wiki/models.py +69 -0
- codealmanac/services/wiki/paths.py +42 -0
- codealmanac/services/wiki/service.py +57 -0
- codealmanac/services/wiki/templates.py +73 -0
- codealmanac/services/wiki/topics.py +266 -0
- codealmanac/services/wiki/wikilinks.py +58 -0
- codealmanac/services/workspaces/__init__.py +1 -0
- codealmanac/services/workspaces/models.py +124 -0
- codealmanac/services/workspaces/ports.py +9 -0
- codealmanac/services/workspaces/requests.py +82 -0
- codealmanac/services/workspaces/roots.py +74 -0
- codealmanac/services/workspaces/service.py +303 -0
- codealmanac/services/workspaces/store.py +127 -0
- codealmanac/workflows/__init__.py +1 -0
- codealmanac/workflows/build/__init__.py +1 -0
- codealmanac/workflows/build/models.py +8 -0
- codealmanac/workflows/build/service.py +45 -0
- codealmanac/workflows/garden/__init__.py +3 -0
- codealmanac/workflows/garden/models.py +30 -0
- codealmanac/workflows/garden/requests.py +22 -0
- codealmanac/workflows/garden/service.py +239 -0
- codealmanac/workflows/ingest/__init__.py +1 -0
- codealmanac/workflows/ingest/models.py +26 -0
- codealmanac/workflows/ingest/requests.py +39 -0
- codealmanac/workflows/ingest/service.py +302 -0
- codealmanac/workflows/lifecycle.py +197 -0
- codealmanac/workflows/sync/__init__.py +3 -0
- codealmanac/workflows/sync/models.py +157 -0
- codealmanac/workflows/sync/requests.py +63 -0
- codealmanac/workflows/sync/service.py +651 -0
- codealmanac/workflows/sync/store.py +51 -0
- codealmanac-0.1.0.dev0.dist-info/METADATA +248 -0
- codealmanac-0.1.0.dev0.dist-info/RECORD +192 -0
- codealmanac-0.1.0.dev0.dist-info/WHEEL +5 -0
- codealmanac-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- codealmanac-0.1.0.dev0.dist-info/licenses/LICENSE.md +201 -0
- codealmanac-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic import field_validator
|
|
5
|
+
|
|
6
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
7
|
+
from codealmanac.core.text import required_text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FilesystemDirectoryListingSource(StrEnum):
|
|
11
|
+
GIT = "git"
|
|
12
|
+
WALK = "walk"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FilesystemDirectoryFileState(StrEnum):
|
|
16
|
+
CHANGED = "changed"
|
|
17
|
+
UNCHANGED = "unchanged"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FilesystemDirectorySelectionPolicy(StrEnum):
|
|
21
|
+
CHANGED_THEN_DIVERSE = "changed_then_diverse"
|
|
22
|
+
DIVERSE = "diverse"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FilesystemDirectoryCandidate(CodeAlmanacModel):
|
|
26
|
+
path: Path
|
|
27
|
+
display_path: str
|
|
28
|
+
selection_group: str
|
|
29
|
+
state: FilesystemDirectoryFileState = FilesystemDirectoryFileState.UNCHANGED
|
|
30
|
+
git_status: str | None = None
|
|
31
|
+
|
|
32
|
+
@field_validator("display_path", "selection_group")
|
|
33
|
+
@classmethod
|
|
34
|
+
def require_display_path(cls, value: str) -> str:
|
|
35
|
+
return required_text(value, "filesystem directory candidate")
|
|
36
|
+
|
|
37
|
+
@field_validator("git_status")
|
|
38
|
+
@classmethod
|
|
39
|
+
def validate_git_status(cls, value: str | None) -> str | None:
|
|
40
|
+
if value is None:
|
|
41
|
+
return value
|
|
42
|
+
if len(value) != 2:
|
|
43
|
+
raise ValueError("filesystem directory git status must be two characters")
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
SOURCE_SUFFIXES = frozenset(
|
|
48
|
+
{
|
|
49
|
+
".c",
|
|
50
|
+
".cc",
|
|
51
|
+
".cpp",
|
|
52
|
+
".cs",
|
|
53
|
+
".css",
|
|
54
|
+
".go",
|
|
55
|
+
".h",
|
|
56
|
+
".hpp",
|
|
57
|
+
".html",
|
|
58
|
+
".java",
|
|
59
|
+
".js",
|
|
60
|
+
".jsx",
|
|
61
|
+
".kt",
|
|
62
|
+
".mjs",
|
|
63
|
+
".php",
|
|
64
|
+
".py",
|
|
65
|
+
".rb",
|
|
66
|
+
".rs",
|
|
67
|
+
".scss",
|
|
68
|
+
".sh",
|
|
69
|
+
".sql",
|
|
70
|
+
".swift",
|
|
71
|
+
".ts",
|
|
72
|
+
".tsx",
|
|
73
|
+
".vue",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
STRUCTURED_SUFFIXES = frozenset(
|
|
77
|
+
{
|
|
78
|
+
".cfg",
|
|
79
|
+
".ini",
|
|
80
|
+
".json",
|
|
81
|
+
".md",
|
|
82
|
+
".toml",
|
|
83
|
+
".yaml",
|
|
84
|
+
".yml",
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
LOW_VALUE_FILENAMES = frozenset(
|
|
88
|
+
{
|
|
89
|
+
".gitkeep",
|
|
90
|
+
"__init__.py",
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
PRIMARY_ROLE_FILENAMES = frozenset(
|
|
94
|
+
{
|
|
95
|
+
"adapter.py",
|
|
96
|
+
"app.py",
|
|
97
|
+
"main.py",
|
|
98
|
+
"service.py",
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
SECONDARY_ROLE_FILENAMES = frozenset(
|
|
102
|
+
{
|
|
103
|
+
"models.py",
|
|
104
|
+
"ports.py",
|
|
105
|
+
"renderer.py",
|
|
106
|
+
"requests.py",
|
|
107
|
+
"root.py",
|
|
108
|
+
"store.py",
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def ranked_directory_candidates(
|
|
114
|
+
candidates: tuple[FilesystemDirectoryCandidate, ...],
|
|
115
|
+
) -> tuple[FilesystemDirectoryCandidate, ...]:
|
|
116
|
+
changed = tuple(
|
|
117
|
+
candidate
|
|
118
|
+
for candidate in candidates
|
|
119
|
+
if candidate.state == FilesystemDirectoryFileState.CHANGED
|
|
120
|
+
)
|
|
121
|
+
unchanged = tuple(
|
|
122
|
+
candidate
|
|
123
|
+
for candidate in candidates
|
|
124
|
+
if candidate.state == FilesystemDirectoryFileState.UNCHANGED
|
|
125
|
+
)
|
|
126
|
+
return (
|
|
127
|
+
*diverse_directory_candidates(changed),
|
|
128
|
+
*diverse_directory_candidates(unchanged),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def directory_candidate_key(
|
|
133
|
+
candidate: FilesystemDirectoryCandidate,
|
|
134
|
+
) -> tuple[int, int, int, str]:
|
|
135
|
+
return (
|
|
136
|
+
filename_role_rank(candidate.path),
|
|
137
|
+
unchanged_content_rank(candidate.path),
|
|
138
|
+
path_depth(candidate.display_path),
|
|
139
|
+
candidate.display_path.casefold(),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def diverse_directory_candidates(
|
|
144
|
+
candidates: tuple[FilesystemDirectoryCandidate, ...],
|
|
145
|
+
) -> tuple[FilesystemDirectoryCandidate, ...]:
|
|
146
|
+
grouped: dict[str, list[FilesystemDirectoryCandidate]] = {}
|
|
147
|
+
for candidate in candidates:
|
|
148
|
+
grouped.setdefault(candidate.selection_group, []).append(candidate)
|
|
149
|
+
for group_candidates in grouped.values():
|
|
150
|
+
group_candidates.sort(key=directory_candidate_key)
|
|
151
|
+
groups = tuple(
|
|
152
|
+
sorted(
|
|
153
|
+
grouped.items(),
|
|
154
|
+
key=lambda item: (directory_candidate_key(item[1][0]), item[0].casefold()),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
ordered: list[FilesystemDirectoryCandidate] = []
|
|
158
|
+
index = 0
|
|
159
|
+
while True:
|
|
160
|
+
added = False
|
|
161
|
+
for _group, group_candidates in groups:
|
|
162
|
+
if index >= len(group_candidates):
|
|
163
|
+
continue
|
|
164
|
+
ordered.append(group_candidates[index])
|
|
165
|
+
added = True
|
|
166
|
+
if not added:
|
|
167
|
+
return tuple(ordered)
|
|
168
|
+
index += 1
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def filename_role_rank(path: Path) -> int:
|
|
172
|
+
name = path.name
|
|
173
|
+
if name in PRIMARY_ROLE_FILENAMES:
|
|
174
|
+
return 0
|
|
175
|
+
if name in SECONDARY_ROLE_FILENAMES:
|
|
176
|
+
return 1
|
|
177
|
+
if name in LOW_VALUE_FILENAMES:
|
|
178
|
+
return 4
|
|
179
|
+
return 2
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def unchanged_content_rank(path: Path) -> int:
|
|
183
|
+
if path.name in LOW_VALUE_FILENAMES:
|
|
184
|
+
return 3
|
|
185
|
+
suffix = path.suffix.casefold()
|
|
186
|
+
if suffix in SOURCE_SUFFIXES:
|
|
187
|
+
return 0
|
|
188
|
+
if suffix in STRUCTURED_SUFFIXES:
|
|
189
|
+
return 1
|
|
190
|
+
return 2
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def path_depth(display_path: str) -> int:
|
|
194
|
+
return display_path.count("/")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def directory_selection_group(path: Path, root: Path) -> str:
|
|
198
|
+
try:
|
|
199
|
+
relative = path.relative_to(root)
|
|
200
|
+
except ValueError:
|
|
201
|
+
return path.name
|
|
202
|
+
parts = relative.parts
|
|
203
|
+
if len(parts) == 0:
|
|
204
|
+
return "."
|
|
205
|
+
if len(parts) == 1:
|
|
206
|
+
return parts[0]
|
|
207
|
+
if Path(parts[1]).suffix:
|
|
208
|
+
return parts[0]
|
|
209
|
+
return "/".join(parts[:2])
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from codealmanac.core.errors import ExecutionFailed
|
|
5
|
+
from codealmanac.integrations.command import CommandRunner, SubprocessCommandRunner
|
|
6
|
+
from codealmanac.integrations.sources.runtime import (
|
|
7
|
+
bounded_text,
|
|
8
|
+
source_runtime_section,
|
|
9
|
+
surface_process_error,
|
|
10
|
+
)
|
|
11
|
+
from codealmanac.services.sources.models import (
|
|
12
|
+
SourceKind,
|
|
13
|
+
SourceRef,
|
|
14
|
+
SourceRuntime,
|
|
15
|
+
SourceRuntimeStatus,
|
|
16
|
+
)
|
|
17
|
+
from codealmanac.services.sources.requests import InspectSourceRuntimeRequest
|
|
18
|
+
|
|
19
|
+
GIT_RUNTIME_TIMEOUT_SECONDS = 30
|
|
20
|
+
DEFAULT_MAX_CHARS = 60_000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GitSourceRuntimeAdapter:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
runner: CommandRunner | None = None,
|
|
27
|
+
max_chars: int = DEFAULT_MAX_CHARS,
|
|
28
|
+
timeout_seconds: int = GIT_RUNTIME_TIMEOUT_SECONDS,
|
|
29
|
+
):
|
|
30
|
+
self.runner = runner or SubprocessCommandRunner()
|
|
31
|
+
self.max_chars = max_chars
|
|
32
|
+
self.timeout_seconds = timeout_seconds
|
|
33
|
+
|
|
34
|
+
def supports(self, ref: SourceRef) -> bool:
|
|
35
|
+
return ref.kind in {SourceKind.GIT_DIFF, SourceKind.GIT_RANGE}
|
|
36
|
+
|
|
37
|
+
def inspect(self, request: InspectSourceRuntimeRequest) -> SourceRuntime:
|
|
38
|
+
if request.ref.kind == SourceKind.GIT_RANGE:
|
|
39
|
+
return self._inspect_range(request.cwd, request.ref)
|
|
40
|
+
if request.ref.kind == SourceKind.GIT_DIFF:
|
|
41
|
+
return self._inspect_diff(request.cwd, request.ref)
|
|
42
|
+
return SourceRuntime(
|
|
43
|
+
ref=request.ref,
|
|
44
|
+
status=SourceRuntimeStatus.SKIPPED,
|
|
45
|
+
title=f"Unsupported Git source {request.ref.identity}",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def _inspect_range(self, cwd: Path, ref: SourceRef) -> SourceRuntime:
|
|
49
|
+
revision_range = require_revision_range(ref)
|
|
50
|
+
sections = (
|
|
51
|
+
source_runtime_section(
|
|
52
|
+
"commits",
|
|
53
|
+
self._git(cwd, ("log", "--oneline", "--decorate", revision_range)),
|
|
54
|
+
),
|
|
55
|
+
source_runtime_section(
|
|
56
|
+
"stat",
|
|
57
|
+
self._git(cwd, ("diff", "--stat", revision_range)),
|
|
58
|
+
),
|
|
59
|
+
source_runtime_section(
|
|
60
|
+
"diff",
|
|
61
|
+
self._git(cwd, ("diff", "--no-ext-diff", revision_range)),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
content, truncated = bounded_text("\n\n".join(sections), self.max_chars)
|
|
65
|
+
return SourceRuntime(
|
|
66
|
+
ref=ref,
|
|
67
|
+
status=SourceRuntimeStatus.AVAILABLE,
|
|
68
|
+
title=f"Git range {revision_range}",
|
|
69
|
+
content=content,
|
|
70
|
+
truncated=truncated,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _inspect_diff(self, cwd: Path, ref: SourceRef) -> SourceRuntime:
|
|
74
|
+
target = require_revision_range(ref)
|
|
75
|
+
if target == "working-tree":
|
|
76
|
+
sections = (
|
|
77
|
+
source_runtime_section("status", self._git(cwd, ("status", "--short"))),
|
|
78
|
+
source_runtime_section(
|
|
79
|
+
"unstaged stat",
|
|
80
|
+
self._git(cwd, ("diff", "--stat")),
|
|
81
|
+
),
|
|
82
|
+
source_runtime_section(
|
|
83
|
+
"unstaged diff",
|
|
84
|
+
self._git(cwd, ("diff", "--no-ext-diff")),
|
|
85
|
+
),
|
|
86
|
+
source_runtime_section(
|
|
87
|
+
"staged stat",
|
|
88
|
+
self._git(cwd, ("diff", "--cached", "--stat")),
|
|
89
|
+
),
|
|
90
|
+
source_runtime_section(
|
|
91
|
+
"staged diff",
|
|
92
|
+
self._git(cwd, ("diff", "--cached", "--no-ext-diff")),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
sections = (
|
|
97
|
+
source_runtime_section(
|
|
98
|
+
"stat",
|
|
99
|
+
self._git(cwd, ("diff", "--stat", target)),
|
|
100
|
+
),
|
|
101
|
+
source_runtime_section(
|
|
102
|
+
"diff",
|
|
103
|
+
self._git(cwd, ("diff", "--no-ext-diff", target)),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
content, truncated = bounded_text("\n\n".join(sections), self.max_chars)
|
|
107
|
+
return SourceRuntime(
|
|
108
|
+
ref=ref,
|
|
109
|
+
status=SourceRuntimeStatus.AVAILABLE,
|
|
110
|
+
title=f"Git diff {target}",
|
|
111
|
+
content=content,
|
|
112
|
+
truncated=truncated,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _git(self, cwd: Path, args: tuple[str, ...]) -> str:
|
|
116
|
+
try:
|
|
117
|
+
result = self.runner.run("git", args, cwd, self.timeout_seconds)
|
|
118
|
+
except FileNotFoundError as error:
|
|
119
|
+
raise ExecutionFailed("git not found on PATH") from error
|
|
120
|
+
except subprocess.TimeoutExpired as error:
|
|
121
|
+
raise ExecutionFailed(f"git {' '.join(args)} timed out") from error
|
|
122
|
+
if result.returncode != 0:
|
|
123
|
+
raise ExecutionFailed(
|
|
124
|
+
f"git {' '.join(args)} failed: {surface_process_error(result)}"
|
|
125
|
+
)
|
|
126
|
+
return result.stdout.strip()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def require_revision_range(ref: SourceRef) -> str:
|
|
130
|
+
if ref.revision_range is None or ref.revision_range.strip() == "":
|
|
131
|
+
raise ExecutionFailed(f"Git source missing revision range: {ref.identity}")
|
|
132
|
+
return ref.revision_range
|