context-loader 1.3.4__tar.gz → 1.3.5__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.
- {context_loader-1.3.4 → context_loader-1.3.5}/PKG-INFO +23 -13
- {context_loader-1.3.4 → context_loader-1.3.5}/README.md +22 -12
- {context_loader-1.3.4 → context_loader-1.3.5}/context_loader/__init__.py +1 -1
- {context_loader-1.3.4 → context_loader-1.3.5}/context_loader/git.py +72 -25
- {context_loader-1.3.4 → context_loader-1.3.5}/pyproject.toml +1 -1
- {context_loader-1.3.4 → context_loader-1.3.5}/pyproject.toml.orig +1 -1
- {context_loader-1.3.4 → context_loader-1.3.5}/tool_cli_contract.json +1 -1
- {context_loader-1.3.4 → context_loader-1.3.5}/LICENSE +0 -0
- {context_loader-1.3.4 → context_loader-1.3.5}/context_loader/application.py +0 -0
- {context_loader-1.3.4 → context_loader-1.3.5}/context_loader/cli.py +0 -0
- {context_loader-1.3.4 → context_loader-1.3.5}/context_loader/collect.py +0 -0
- {context_loader-1.3.4 → context_loader-1.3.5}/context_loader/filesystem.py +0 -0
- {context_loader-1.3.4 → context_loader-1.3.5}/context_loader/model.py +0 -0
- {context_loader-1.3.4 → context_loader-1.3.5}/context_loader/render.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: context-loader
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5
|
|
4
4
|
Summary: Render deterministic local Git context for any executor
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -219,14 +219,14 @@ Python standard library.
|
|
|
219
219
|
|
|
220
220
|
## Open-source quick start
|
|
221
221
|
|
|
222
|
-
Current stable release: **1.3.
|
|
222
|
+
Current stable release: **1.3.5**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
|
|
223
223
|
|
|
224
224
|
```bash
|
|
225
|
-
uv tool install 'context-loader==1.3.
|
|
225
|
+
uv tool install 'context-loader==1.3.5'
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
```bash
|
|
229
|
-
pip install 'context-loader==1.3.
|
|
229
|
+
pip install 'context-loader==1.3.5'
|
|
230
230
|
```
|
|
231
231
|
|
|
232
232
|
```bash
|
|
@@ -239,21 +239,21 @@ project-context --repo /path/to/repo --format json
|
|
|
239
239
|
|
|
240
240
|
## Install
|
|
241
241
|
|
|
242
|
-
Source version: `1.3.
|
|
242
|
+
Source version: `1.3.5`. Install its matching published release or an exact source commit.
|
|
243
243
|
|
|
244
244
|
Install via `uv`:
|
|
245
245
|
|
|
246
246
|
```bash
|
|
247
|
-
uv tool install 'context-loader==1.3.
|
|
247
|
+
uv tool install 'context-loader==1.3.5'
|
|
248
248
|
```
|
|
249
249
|
|
|
250
250
|
Install via `pip`:
|
|
251
251
|
|
|
252
252
|
```bash
|
|
253
|
-
pip install 'context-loader==1.3.
|
|
253
|
+
pip install 'context-loader==1.3.5'
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
For development or source-based installs tracking current repository (`1.3.
|
|
256
|
+
For development or source-based installs tracking current repository (`1.3.5`):
|
|
257
257
|
|
|
258
258
|
```bash
|
|
259
259
|
uv tool install git+https://github.com/xuanheng-tech/context-loader.git
|
|
@@ -350,7 +350,7 @@ set differs, as described under “Compact model consumption”. The declared co
|
|
|
350
350
|
"schema_version": 3,
|
|
351
351
|
"tool": {
|
|
352
352
|
"name": "context-loader",
|
|
353
|
-
"version": "1.3.
|
|
353
|
+
"version": "1.3.5"
|
|
354
354
|
},
|
|
355
355
|
"repository": {
|
|
356
356
|
"requested_path": "/canonical/requested/path",
|
|
@@ -470,7 +470,7 @@ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward eac
|
|
|
470
470
|
names exactly one key set, and a new number is issued whenever a key set would otherwise be reused.
|
|
471
471
|
|
|
472
472
|
The JSON schema version and package version are independent: this build emits `schema_version` `3`
|
|
473
|
-
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.
|
|
473
|
+
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.5`. Version 1
|
|
474
474
|
and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
|
|
475
475
|
`nested_context` changes the default document's key set, version 3 names the current full-document
|
|
476
476
|
key set and version 4 names the compact projection.
|
|
@@ -596,6 +596,8 @@ bullet is a per-component, read or capture bound, as each one states:
|
|
|
596
596
|
repository root the filesystem could not decode makes the machine formats exit 1 where Markdown
|
|
597
597
|
still renders
|
|
598
598
|
- Git subprocess output: 16 MiB (bounded while reading)
|
|
599
|
+
- Git collection: one shared 30-second deadline for repository discovery, configuration/submodule
|
|
600
|
+
preflight, refs, status and recent commits
|
|
599
601
|
- Git status preflight: at most 128 repositories, including initialized nested submodules
|
|
600
602
|
- Candidate file validation: 16 MiB hard read bound
|
|
601
603
|
|
|
@@ -611,6 +613,12 @@ Symlinked candidate files are skipped, and directory symlinks are not traversed.
|
|
|
611
613
|
Git state is derived only from the working tree and local refs. The command does not fetch or query
|
|
612
614
|
the remote.
|
|
613
615
|
|
|
616
|
+
All Git commands in one collection share the same elapsed-time budget; each command receives only
|
|
617
|
+
the remaining time. Once that budget expires, the loader stops without starting subsequent commands,
|
|
618
|
+
terminates and reaps a running Git child, and exits with empty stdout. This deadline does not impose
|
|
619
|
+
a hard wall-clock limit on blocked filesystem/process-creation calls or operating-system child
|
|
620
|
+
reaping, and does not cover the later context-file collection.
|
|
621
|
+
|
|
614
622
|
- An unborn working tree reports its symbolic branch name, such as `Branch: main`, and
|
|
615
623
|
`HEAD: unborn`.
|
|
616
624
|
- A detached HEAD reports `Branch: detached` and the resolved commit object ID.
|
|
@@ -639,7 +647,7 @@ or candidate-file content.
|
|
|
639
647
|
|
|
640
648
|
## Not Included
|
|
641
649
|
|
|
642
|
-
Version 1.3.
|
|
650
|
+
Version 1.3.5 does not provide AI summaries, project-type detection, loading or transport of
|
|
643
651
|
nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
|
|
644
652
|
Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
|
|
645
653
|
network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
|
|
@@ -654,10 +662,12 @@ just check
|
|
|
654
662
|
|
|
655
663
|
The shared check also builds a wheel and installs it into a fresh virtual environment. Package
|
|
656
664
|
acceptance runs the installed CLI outside the source checkout, checking deterministic agreement
|
|
657
|
-
between all three formats, linked worktrees,
|
|
665
|
+
between all three formats, linked worktrees, long multi-module instructions with focus/path selection,
|
|
666
|
+
explicit truncation and nested-instruction presence, and safe failures. Build and installation run offline
|
|
658
667
|
using the backend cache populated by `uv run --frozen`; the tests use only disposable repositories.
|
|
659
668
|
Git deadline regressions verify that silent, streaming, and stdout-closed child processes are
|
|
660
|
-
terminated and reaped,
|
|
669
|
+
terminated and reaped, that successful commands cannot reset the shared budget, and that process
|
|
670
|
+
creation consumes it. A timeout leaves CLI stdout empty in every output format.
|
|
661
671
|
|
|
662
672
|
The lockfile uses public PyPI artifact URLs; no organization mirror or local Renovate preset
|
|
663
673
|
is needed. Renovate keeps scheduled, rate-limited updates and manual merges using a public
|
|
@@ -8,14 +8,14 @@ Python standard library.
|
|
|
8
8
|
|
|
9
9
|
## Open-source quick start
|
|
10
10
|
|
|
11
|
-
Current stable release: **1.3.
|
|
11
|
+
Current stable release: **1.3.5**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
uv tool install 'context-loader==1.3.
|
|
14
|
+
uv tool install 'context-loader==1.3.5'
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
pip install 'context-loader==1.3.
|
|
18
|
+
pip install 'context-loader==1.3.5'
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
```bash
|
|
@@ -28,21 +28,21 @@ project-context --repo /path/to/repo --format json
|
|
|
28
28
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
31
|
-
Source version: `1.3.
|
|
31
|
+
Source version: `1.3.5`. Install its matching published release or an exact source commit.
|
|
32
32
|
|
|
33
33
|
Install via `uv`:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
uv tool install 'context-loader==1.3.
|
|
36
|
+
uv tool install 'context-loader==1.3.5'
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Install via `pip`:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
pip install 'context-loader==1.3.
|
|
42
|
+
pip install 'context-loader==1.3.5'
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
For development or source-based installs tracking current repository (`1.3.
|
|
45
|
+
For development or source-based installs tracking current repository (`1.3.5`):
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
uv tool install git+https://github.com/xuanheng-tech/context-loader.git
|
|
@@ -139,7 +139,7 @@ set differs, as described under “Compact model consumption”. The declared co
|
|
|
139
139
|
"schema_version": 3,
|
|
140
140
|
"tool": {
|
|
141
141
|
"name": "context-loader",
|
|
142
|
-
"version": "1.3.
|
|
142
|
+
"version": "1.3.5"
|
|
143
143
|
},
|
|
144
144
|
"repository": {
|
|
145
145
|
"requested_path": "/canonical/requested/path",
|
|
@@ -259,7 +259,7 @@ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward eac
|
|
|
259
259
|
names exactly one key set, and a new number is issued whenever a key set would otherwise be reused.
|
|
260
260
|
|
|
261
261
|
The JSON schema version and package version are independent: this build emits `schema_version` `3`
|
|
262
|
-
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.
|
|
262
|
+
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.5`. Version 1
|
|
263
263
|
and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
|
|
264
264
|
`nested_context` changes the default document's key set, version 3 names the current full-document
|
|
265
265
|
key set and version 4 names the compact projection.
|
|
@@ -385,6 +385,8 @@ bullet is a per-component, read or capture bound, as each one states:
|
|
|
385
385
|
repository root the filesystem could not decode makes the machine formats exit 1 where Markdown
|
|
386
386
|
still renders
|
|
387
387
|
- Git subprocess output: 16 MiB (bounded while reading)
|
|
388
|
+
- Git collection: one shared 30-second deadline for repository discovery, configuration/submodule
|
|
389
|
+
preflight, refs, status and recent commits
|
|
388
390
|
- Git status preflight: at most 128 repositories, including initialized nested submodules
|
|
389
391
|
- Candidate file validation: 16 MiB hard read bound
|
|
390
392
|
|
|
@@ -400,6 +402,12 @@ Symlinked candidate files are skipped, and directory symlinks are not traversed.
|
|
|
400
402
|
Git state is derived only from the working tree and local refs. The command does not fetch or query
|
|
401
403
|
the remote.
|
|
402
404
|
|
|
405
|
+
All Git commands in one collection share the same elapsed-time budget; each command receives only
|
|
406
|
+
the remaining time. Once that budget expires, the loader stops without starting subsequent commands,
|
|
407
|
+
terminates and reaps a running Git child, and exits with empty stdout. This deadline does not impose
|
|
408
|
+
a hard wall-clock limit on blocked filesystem/process-creation calls or operating-system child
|
|
409
|
+
reaping, and does not cover the later context-file collection.
|
|
410
|
+
|
|
403
411
|
- An unborn working tree reports its symbolic branch name, such as `Branch: main`, and
|
|
404
412
|
`HEAD: unborn`.
|
|
405
413
|
- A detached HEAD reports `Branch: detached` and the resolved commit object ID.
|
|
@@ -428,7 +436,7 @@ or candidate-file content.
|
|
|
428
436
|
|
|
429
437
|
## Not Included
|
|
430
438
|
|
|
431
|
-
Version 1.3.
|
|
439
|
+
Version 1.3.5 does not provide AI summaries, project-type detection, loading or transport of
|
|
432
440
|
nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
|
|
433
441
|
Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
|
|
434
442
|
network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
|
|
@@ -443,10 +451,12 @@ just check
|
|
|
443
451
|
|
|
444
452
|
The shared check also builds a wheel and installs it into a fresh virtual environment. Package
|
|
445
453
|
acceptance runs the installed CLI outside the source checkout, checking deterministic agreement
|
|
446
|
-
between all three formats, linked worktrees,
|
|
454
|
+
between all three formats, linked worktrees, long multi-module instructions with focus/path selection,
|
|
455
|
+
explicit truncation and nested-instruction presence, and safe failures. Build and installation run offline
|
|
447
456
|
using the backend cache populated by `uv run --frozen`; the tests use only disposable repositories.
|
|
448
457
|
Git deadline regressions verify that silent, streaming, and stdout-closed child processes are
|
|
449
|
-
terminated and reaped,
|
|
458
|
+
terminated and reaped, that successful commands cannot reset the shared budget, and that process
|
|
459
|
+
creation consumes it. A timeout leaves CLI stdout empty in every output format.
|
|
450
460
|
|
|
451
461
|
The lockfile uses public PyPI artifact URLs; no organization mirror or local Renovate preset
|
|
452
462
|
is needed. Renovate keeps scheduled, rate-limited updates and manual merges using a public
|
|
@@ -118,8 +118,11 @@ def _read_bounded_process_stdout(
|
|
|
118
118
|
proc: subprocess.Popen[bytes],
|
|
119
119
|
max_bytes: int,
|
|
120
120
|
timeout_seconds: float,
|
|
121
|
+
*,
|
|
122
|
+
deadline: float | None = None,
|
|
121
123
|
) -> bytes:
|
|
122
|
-
deadline
|
|
124
|
+
if deadline is None:
|
|
125
|
+
deadline = time.monotonic() + timeout_seconds
|
|
123
126
|
buffer = bytearray()
|
|
124
127
|
assert proc.stdout is not None
|
|
125
128
|
os.set_blocking(proc.stdout.fileno(), False)
|
|
@@ -168,9 +171,23 @@ def _read_bounded_process_stdout(
|
|
|
168
171
|
return bytes(buffer)
|
|
169
172
|
|
|
170
173
|
|
|
171
|
-
def
|
|
174
|
+
def _check_git_deadline(deadline: float) -> None:
|
|
175
|
+
if time.monotonic() >= deadline:
|
|
176
|
+
raise ContextLoaderError("unable to read repository Git state")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _run_git(
|
|
180
|
+
repo: Path,
|
|
181
|
+
arguments: tuple[str, ...],
|
|
182
|
+
*,
|
|
183
|
+
check: bool = True,
|
|
184
|
+
deadline: float | None = None,
|
|
185
|
+
) -> GitResult:
|
|
186
|
+
command_deadline = time.monotonic() + GIT_TIMEOUT_SECONDS
|
|
187
|
+
deadline = command_deadline if deadline is None else min(deadline, command_deadline)
|
|
172
188
|
argv = [GIT_EXECUTABLE, *_GIT_PREFIX, "-C", os.fspath(repo), *arguments]
|
|
173
189
|
try:
|
|
190
|
+
_check_git_deadline(deadline)
|
|
174
191
|
with subprocess.Popen( # noqa: S603 - executable and every command are fixed here.
|
|
175
192
|
argv,
|
|
176
193
|
cwd="/",
|
|
@@ -179,8 +196,11 @@ def _run_git(repo: Path, arguments: tuple[str, ...], *, check: bool = True) -> G
|
|
|
179
196
|
stdout=subprocess.PIPE,
|
|
180
197
|
stderr=subprocess.DEVNULL,
|
|
181
198
|
) as proc:
|
|
182
|
-
stdout = _read_bounded_process_stdout(
|
|
199
|
+
stdout = _read_bounded_process_stdout(
|
|
200
|
+
proc, MAX_GIT_OUTPUT_BYTES, GIT_TIMEOUT_SECONDS, deadline=deadline
|
|
201
|
+
)
|
|
183
202
|
returncode = proc.returncode
|
|
203
|
+
_check_git_deadline(deadline)
|
|
184
204
|
except ContextLoaderError:
|
|
185
205
|
raise
|
|
186
206
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
@@ -209,6 +229,16 @@ def discover_repository(
|
|
|
209
229
|
raw_path: str | os.PathLike[str], *, require_canonical_root: bool = False
|
|
210
230
|
) -> RepositoryLocation:
|
|
211
231
|
"""Resolve an absolute directory and discover its non-bare Git worktree root."""
|
|
232
|
+
return _discover_repository(
|
|
233
|
+
raw_path,
|
|
234
|
+
require_canonical_root=require_canonical_root,
|
|
235
|
+
deadline=time.monotonic() + GIT_TIMEOUT_SECONDS,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _discover_repository(
|
|
240
|
+
raw_path: str | os.PathLike[str], *, require_canonical_root: bool, deadline: float
|
|
241
|
+
) -> RepositoryLocation:
|
|
212
242
|
candidate = Path(raw_path)
|
|
213
243
|
if not candidate.is_absolute():
|
|
214
244
|
raise ContextLoaderError("--repo must be an absolute path", exit_code=2)
|
|
@@ -219,11 +249,14 @@ def discover_repository(
|
|
|
219
249
|
if not canonical.is_dir():
|
|
220
250
|
raise ContextLoaderError("--repo must name an existing directory", exit_code=2)
|
|
221
251
|
|
|
222
|
-
bare = _run_git(
|
|
252
|
+
bare = _run_git(
|
|
253
|
+
canonical, ("rev-parse", "--is-bare-repository"), check=False, deadline=deadline
|
|
254
|
+
)
|
|
223
255
|
root = _run_git(
|
|
224
256
|
canonical,
|
|
225
257
|
("rev-parse", "--path-format=absolute", "--show-toplevel"),
|
|
226
258
|
check=False,
|
|
259
|
+
deadline=deadline,
|
|
227
260
|
)
|
|
228
261
|
if bare.returncode != 0 or root.returncode != 0:
|
|
229
262
|
raise ContextLoaderError(
|
|
@@ -243,6 +276,7 @@ def discover_repository(
|
|
|
243
276
|
raise ContextLoaderError(
|
|
244
277
|
"--repo must be the canonical root of a non-bare Git worktree", exit_code=2
|
|
245
278
|
)
|
|
279
|
+
_check_git_deadline(deadline)
|
|
246
280
|
return RepositoryLocation(requested_path=canonical, canonical_root=discovered)
|
|
247
281
|
|
|
248
282
|
|
|
@@ -251,14 +285,18 @@ def validate_repository(raw_path: str | os.PathLike[str]) -> Path:
|
|
|
251
285
|
return discover_repository(raw_path, require_canonical_root=True).canonical_root
|
|
252
286
|
|
|
253
287
|
|
|
254
|
-
def _head_and_branch(repo: Path) -> tuple[str, str, str | None]:
|
|
255
|
-
symbolic = _run_git(
|
|
288
|
+
def _head_and_branch(repo: Path, deadline: float) -> tuple[str, str, str | None]:
|
|
289
|
+
symbolic = _run_git(
|
|
290
|
+
repo, ("symbolic-ref", "--quiet", "--short", "HEAD"), check=False, deadline=deadline
|
|
291
|
+
)
|
|
256
292
|
branch_name = (
|
|
257
293
|
_decode_path(_without_one_line_ending(symbolic.stdout))
|
|
258
294
|
if symbolic.returncode == 0
|
|
259
295
|
else None
|
|
260
296
|
)
|
|
261
|
-
resolved_head = _run_git(
|
|
297
|
+
resolved_head = _run_git(
|
|
298
|
+
repo, ("rev-parse", "--verify", "HEAD^{commit}"), check=False, deadline=deadline
|
|
299
|
+
)
|
|
262
300
|
if resolved_head.returncode != 0:
|
|
263
301
|
if branch_name is None:
|
|
264
302
|
raise ContextLoaderError("unable to read repository Git state")
|
|
@@ -270,17 +308,17 @@ def _head_and_branch(repo: Path) -> tuple[str, str, str | None]:
|
|
|
270
308
|
return branch_name or "detached", head, branch_name
|
|
271
309
|
|
|
272
310
|
|
|
273
|
-
def _config_value(repo: Path, key: str) -> str | None:
|
|
274
|
-
result = _run_git(repo, ("config", "--get", key), check=False)
|
|
311
|
+
def _config_value(repo: Path, key: str, deadline: float) -> str | None:
|
|
312
|
+
result = _run_git(repo, ("config", "--get", key), check=False, deadline=deadline)
|
|
275
313
|
if result.returncode != 0:
|
|
276
314
|
return None
|
|
277
315
|
value = _decode_text(_without_one_line_ending(result.stdout))
|
|
278
316
|
return value or None
|
|
279
317
|
|
|
280
318
|
|
|
281
|
-
def _fallback_upstream(repo: Path, branch_name: str) -> str | None:
|
|
282
|
-
remote = _config_value(repo, f"branch.{branch_name}.remote")
|
|
283
|
-
merge = _config_value(repo, f"branch.{branch_name}.merge")
|
|
319
|
+
def _fallback_upstream(repo: Path, branch_name: str, deadline: float) -> str | None:
|
|
320
|
+
remote = _config_value(repo, f"branch.{branch_name}.remote", deadline)
|
|
321
|
+
merge = _config_value(repo, f"branch.{branch_name}.merge", deadline)
|
|
284
322
|
if remote is None or merge is None:
|
|
285
323
|
return None
|
|
286
324
|
merge_name = merge.removeprefix("refs/heads/")
|
|
@@ -289,7 +327,7 @@ def _fallback_upstream(repo: Path, branch_name: str) -> str | None:
|
|
|
289
327
|
return f"{remote}/{merge_name}"
|
|
290
328
|
|
|
291
329
|
|
|
292
|
-
def _upstream_state(repo: Path, branch_name: str | None) -> tuple[str, str]:
|
|
330
|
+
def _upstream_state(repo: Path, branch_name: str | None, deadline: float) -> tuple[str, str]:
|
|
293
331
|
if branch_name is None:
|
|
294
332
|
return "not configured", "not available"
|
|
295
333
|
|
|
@@ -297,9 +335,10 @@ def _upstream_state(repo: Path, branch_name: str | None) -> tuple[str, str]:
|
|
|
297
335
|
repo,
|
|
298
336
|
("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"),
|
|
299
337
|
check=False,
|
|
338
|
+
deadline=deadline,
|
|
300
339
|
)
|
|
301
340
|
if resolved.returncode != 0:
|
|
302
|
-
configured = _fallback_upstream(repo, branch_name)
|
|
341
|
+
configured = _fallback_upstream(repo, branch_name, deadline)
|
|
303
342
|
return configured or "not configured", "not available"
|
|
304
343
|
|
|
305
344
|
upstream = _decode_text(_without_one_line_ending(resolved.stdout))
|
|
@@ -309,6 +348,7 @@ def _upstream_state(repo: Path, branch_name: str | None) -> tuple[str, str]:
|
|
|
309
348
|
repo,
|
|
310
349
|
("rev-list", "--left-right", "--count", "HEAD...@{upstream}"),
|
|
311
350
|
check=False,
|
|
351
|
+
deadline=deadline,
|
|
312
352
|
)
|
|
313
353
|
if divergence.returncode != 0:
|
|
314
354
|
return upstream, "not available"
|
|
@@ -318,7 +358,7 @@ def _upstream_state(repo: Path, branch_name: str | None) -> tuple[str, str]:
|
|
|
318
358
|
return upstream, f"{counts[0].decode('ascii')} / {counts[1].decode('ascii')}"
|
|
319
359
|
|
|
320
360
|
|
|
321
|
-
def _ensure_safe_status(repo: Path) -> None:
|
|
361
|
+
def _ensure_safe_status(repo: Path, deadline: float) -> None:
|
|
322
362
|
"""Reject executable conversions before status can refresh any index entry.
|
|
323
363
|
|
|
324
364
|
Attributes can select clean/process drivers even during a read-only status.
|
|
@@ -340,6 +380,7 @@ def _ensure_safe_status(repo: Path) -> None:
|
|
|
340
380
|
current,
|
|
341
381
|
("config", "--null", "--get-regexp", r"^filter\..*\.(clean|process)$"),
|
|
342
382
|
check=False,
|
|
383
|
+
deadline=deadline,
|
|
343
384
|
)
|
|
344
385
|
if filters.returncode not in (0, 1):
|
|
345
386
|
raise ContextLoaderError("unable to safely inspect repository Git filters")
|
|
@@ -356,7 +397,7 @@ def _ensure_safe_status(repo: Path) -> None:
|
|
|
356
397
|
"repository or submodule config enables Git clean/process filters; "
|
|
357
398
|
"unable to safely inspect working-tree state"
|
|
358
399
|
)
|
|
359
|
-
index = _run_git(current, ("ls-files", "--stage", "-z"))
|
|
400
|
+
index = _run_git(current, ("ls-files", "--stage", "-z"), deadline=deadline)
|
|
360
401
|
children: set[Path] = set()
|
|
361
402
|
for record in index.stdout.split(b"\0"):
|
|
362
403
|
metadata, separator, raw_path = record.partition(b"\t")
|
|
@@ -375,8 +416,8 @@ def _ensure_safe_status(repo: Path) -> None:
|
|
|
375
416
|
raise ContextLoaderError("repository submodule inspection exceeded the safety limit")
|
|
376
417
|
|
|
377
418
|
|
|
378
|
-
def _working_tree_changes(repo: Path) -> tuple[WorkingTreeChange, ...]:
|
|
379
|
-
_ensure_safe_status(repo)
|
|
419
|
+
def _working_tree_changes(repo: Path, deadline: float) -> tuple[WorkingTreeChange, ...]:
|
|
420
|
+
_ensure_safe_status(repo, deadline)
|
|
380
421
|
result = _run_git(
|
|
381
422
|
repo,
|
|
382
423
|
(
|
|
@@ -387,6 +428,7 @@ def _working_tree_changes(repo: Path) -> tuple[WorkingTreeChange, ...]:
|
|
|
387
428
|
"--no-renames",
|
|
388
429
|
"--ignore-submodules=none",
|
|
389
430
|
),
|
|
431
|
+
deadline=deadline,
|
|
390
432
|
)
|
|
391
433
|
if not result.stdout:
|
|
392
434
|
return ()
|
|
@@ -408,7 +450,7 @@ def _working_tree_changes(repo: Path) -> tuple[WorkingTreeChange, ...]:
|
|
|
408
450
|
return tuple(sorted(changes, key=lambda change: change.sort_key))
|
|
409
451
|
|
|
410
452
|
|
|
411
|
-
def _recent_commits(repo: Path, head: str) -> tuple[RecentCommit, ...]:
|
|
453
|
+
def _recent_commits(repo: Path, head: str, deadline: float) -> tuple[RecentCommit, ...]:
|
|
412
454
|
if head == "unborn":
|
|
413
455
|
return ()
|
|
414
456
|
result = _run_git(
|
|
@@ -421,6 +463,7 @@ def _recent_commits(repo: Path, head: str) -> tuple[RecentCommit, ...]:
|
|
|
421
463
|
"HEAD",
|
|
422
464
|
"--",
|
|
423
465
|
),
|
|
466
|
+
deadline=deadline,
|
|
424
467
|
)
|
|
425
468
|
commits: list[RecentCommit] = []
|
|
426
469
|
for line in result.stdout.splitlines():
|
|
@@ -443,13 +486,16 @@ def _recent_commits(repo: Path, head: str) -> tuple[RecentCommit, ...]:
|
|
|
443
486
|
def collect_repository(
|
|
444
487
|
raw_path: str | os.PathLike[str], *, require_canonical_root: bool = True
|
|
445
488
|
) -> tuple[RepositoryLocation, RepositoryState]:
|
|
446
|
-
"""Discover
|
|
447
|
-
|
|
489
|
+
"""Discover and collect local Git state within one shared elapsed-time budget."""
|
|
490
|
+
deadline = time.monotonic() + GIT_TIMEOUT_SECONDS
|
|
491
|
+
location = _discover_repository(
|
|
492
|
+
raw_path, require_canonical_root=require_canonical_root, deadline=deadline
|
|
493
|
+
)
|
|
448
494
|
repository = location.canonical_root
|
|
449
|
-
branch, head, branch_name = _head_and_branch(repository)
|
|
450
|
-
upstream, ahead_behind = _upstream_state(repository, branch_name)
|
|
451
|
-
changes = _working_tree_changes(repository)
|
|
452
|
-
commits = _recent_commits(repository, head)
|
|
495
|
+
branch, head, branch_name = _head_and_branch(repository, deadline)
|
|
496
|
+
upstream, ahead_behind = _upstream_state(repository, branch_name, deadline)
|
|
497
|
+
changes = _working_tree_changes(repository, deadline)
|
|
498
|
+
commits = _recent_commits(repository, head, deadline)
|
|
453
499
|
state = RepositoryState(
|
|
454
500
|
repository=repository,
|
|
455
501
|
branch=branch,
|
|
@@ -459,6 +505,7 @@ def collect_repository(
|
|
|
459
505
|
changes=changes,
|
|
460
506
|
commits=commits,
|
|
461
507
|
)
|
|
508
|
+
_check_git_deadline(deadline)
|
|
462
509
|
return location, state
|
|
463
510
|
|
|
464
511
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|