alissa-tools-github-revloop 0.31.18__tar.gz → 0.31.19__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.
- {alissa_tools_github_revloop-0.31.18/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.19}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/janitor.py +65 -1
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/loop.py +28 -5
- alissa_tools_github_revloop-0.31.19/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.31.18/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/README.md +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/parking.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/state.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -35,6 +35,12 @@ Three properties everything here is written around:
|
|
|
35
35
|
* **Nothing raises past its own concern.** A folder that vanishes mid-sweep,
|
|
36
36
|
an unreadable hub, a failed removal: logged and skipped, because this runs
|
|
37
37
|
at the end of a poll pass that must complete.
|
|
38
|
+
|
|
39
|
+
Two more bound it (issue #175). The layout is fixed: hubs are found at
|
|
40
|
+
`<root>/<repo>` with the session cwd at `<hub>/main`, so under any other
|
|
41
|
+
`hub_template` (`layout_supported` says no) the caller sweeps nothing rather
|
|
42
|
+
than mis-key the hold. And one sweep removes at most `MAX_REMOVALS` folders in
|
|
43
|
+
`MAX_SWEEP_SECONDS`; the rest wait for the next interval (`deferred`).
|
|
38
44
|
"""
|
|
39
45
|
|
|
40
46
|
from __future__ import annotations
|
|
@@ -43,6 +49,7 @@ import logging
|
|
|
43
49
|
import os
|
|
44
50
|
import re
|
|
45
51
|
import shutil
|
|
52
|
+
import time
|
|
46
53
|
from dataclasses import dataclass, field
|
|
47
54
|
from pathlib import Path
|
|
48
55
|
from typing import Callable, Iterable
|
|
@@ -70,6 +77,23 @@ SOURCE_DIR = ".source"
|
|
|
70
77
|
KIND_SCRATCH = "scratch"
|
|
71
78
|
KIND_REVIEW = "review-checkout"
|
|
72
79
|
|
|
80
|
+
# One sweep's bound (issue #175): at most this many removals, and no removal
|
|
81
|
+
# started past this many seconds into the sweep. It runs synchronously at the
|
|
82
|
+
# end of a poll pass, so a large backlog is drained over several intervals
|
|
83
|
+
# instead of stalling one pass for minutes.
|
|
84
|
+
MAX_REMOVALS = 25
|
|
85
|
+
MAX_SWEEP_SECONDS = 120.0
|
|
86
|
+
|
|
87
|
+
# The one hub layout the janitor knows: `discover_hubs` reads the root's
|
|
88
|
+
# direct children and `scan_hub` reads `<hub>/main`. Named in the caller's
|
|
89
|
+
# refusal WARNING.
|
|
90
|
+
SUPPORTED_TEMPLATE = "{root}/{repo}/main"
|
|
91
|
+
|
|
92
|
+
# What `layout_supported` probes a template with: two owners that differ, so a
|
|
93
|
+
# template that puts the owner in the path resolves to two different places.
|
|
94
|
+
_PROBE_OWNERS = ("janitor-probe-owner-a", "janitor-probe-owner-b")
|
|
95
|
+
_PROBE_REPO = "janitor-probe-repo"
|
|
96
|
+
|
|
73
97
|
Runner = Callable[..., str]
|
|
74
98
|
|
|
75
99
|
|
|
@@ -158,12 +182,34 @@ class SweepResult:
|
|
|
158
182
|
fresh: int = 0
|
|
159
183
|
failed: "list[tuple[Path, str]]" = field(default_factory=list)
|
|
160
184
|
pruned: int = 0
|
|
185
|
+
# Unheld stale candidates the per-sweep bound left for the next interval.
|
|
186
|
+
deferred: int = 0
|
|
161
187
|
|
|
162
188
|
@property
|
|
163
189
|
def freed(self) -> int:
|
|
164
190
|
return sum(size for _, size in self.removed)
|
|
165
191
|
|
|
166
192
|
|
|
193
|
+
def layout_supported(
|
|
194
|
+
hub_for: "Callable[[str, str], Path]", workspace_root: "Path | str"
|
|
195
|
+
) -> bool:
|
|
196
|
+
"""Whether `hub_for` (`Config.hub_for`) places every hub at
|
|
197
|
+
`<root>/<repo>` with the session cwd at `<hub>/main` -- the one layout
|
|
198
|
+
`discover_hubs` and `scan_hub` read, and the one in which a hub's
|
|
199
|
+
directory name is the repo slug the `Guard` keys on.
|
|
200
|
+
|
|
201
|
+
Probed with two distinct owners and one repo name: both must resolve to
|
|
202
|
+
`workspace_root / repo / "main"`. A template that cannot be formatted is
|
|
203
|
+
unsupported. Pure: nothing is read off the disk."""
|
|
204
|
+
expected = Path(os.fspath(workspace_root)).expanduser() / _PROBE_REPO / "main"
|
|
205
|
+
try:
|
|
206
|
+
return all(
|
|
207
|
+
Path(hub_for(owner, _PROBE_REPO)) == expected for owner in _PROBE_OWNERS
|
|
208
|
+
)
|
|
209
|
+
except (KeyError, IndexError, ValueError, AttributeError, TypeError):
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
167
213
|
def discover_hubs(workspace_root: "Path | str") -> "list[Path]":
|
|
168
214
|
"""Every worktree hub directly under the workspace root: a non-symlink
|
|
169
215
|
directory holding a `.source` directory. Read off the disk rather than the
|
|
@@ -323,13 +369,25 @@ def sweep(
|
|
|
323
369
|
now: float,
|
|
324
370
|
dry_run: bool = False,
|
|
325
371
|
runner: Runner = run,
|
|
372
|
+
max_removals: int = MAX_REMOVALS,
|
|
373
|
+
max_seconds: float = MAX_SWEEP_SECONDS,
|
|
374
|
+
clock: "Callable[[], float]" = time.monotonic,
|
|
326
375
|
) -> SweepResult:
|
|
327
376
|
"""Remove every candidate under `hubs` older than `grace_seconds` that
|
|
328
377
|
`guard` does not hold, then prune each hub's worktree records.
|
|
329
378
|
|
|
379
|
+
Bounded (issue #175): before each removal, a sweep that has already
|
|
380
|
+
removed `max_removals` folders, or has run `max_seconds` by `clock`,
|
|
381
|
+
stops removing. Every unheld stale candidate past that point is counted
|
|
382
|
+
in `deferred` and left for the next interval; every hub is still scanned
|
|
383
|
+
and pruned.
|
|
384
|
+
|
|
330
385
|
Dry-run removes nothing and prunes nothing: it logs each folder it WOULD
|
|
331
|
-
remove (with its size) and reports them in `would_remove
|
|
386
|
+
remove (with its size) and reports them in `would_remove`, under the
|
|
387
|
+
same bound -- so it reports what one live sweep would do."""
|
|
332
388
|
result = SweepResult()
|
|
389
|
+
started = clock()
|
|
390
|
+
bounded = False
|
|
333
391
|
for hub in hubs:
|
|
334
392
|
result.hubs += 1
|
|
335
393
|
for candidate in scan_hub(hub):
|
|
@@ -344,6 +402,12 @@ def sweep(
|
|
|
344
402
|
log.debug("janitor: keeping %s — %s", candidate.path, why)
|
|
345
403
|
result.held.append((candidate.path, why))
|
|
346
404
|
continue
|
|
405
|
+
if not bounded:
|
|
406
|
+
taken = len(result.would_remove if dry_run else result.removed)
|
|
407
|
+
bounded = taken >= max_removals or clock() - started >= max_seconds
|
|
408
|
+
if bounded:
|
|
409
|
+
result.deferred += 1
|
|
410
|
+
continue
|
|
347
411
|
if not _contained(candidate):
|
|
348
412
|
result.failed.append((candidate.path, "not contained in its hub"))
|
|
349
413
|
continue
|
|
@@ -2619,6 +2619,9 @@ class ReviewWatcher:
|
|
|
2619
2619
|
# When the workspace janitor last swept (monotonic), or None before the
|
|
2620
2620
|
# first sweep -- which therefore runs at the end of the first pass.
|
|
2621
2621
|
self._janitor_last: "float | None" = None
|
|
2622
|
+
# Whether the janitor has said, once for this process, that it will
|
|
2623
|
+
# not sweep under this `hub_template` (issue #175).
|
|
2624
|
+
self._janitor_layout_warned = False
|
|
2622
2625
|
# The (repo, number, round) whose alive session the stale-round probe
|
|
2623
2626
|
# just killed as `wedged:first-run-dialog` (issue #136), so the
|
|
2624
2627
|
# respawn site logs its re-enqueue at INFO instead of a second
|
|
@@ -7946,8 +7949,9 @@ class ReviewWatcher:
|
|
|
7946
7949
|
One INFO line per sweep (folders removed and bytes freed, or what a
|
|
7947
7950
|
dry run WOULD remove), and one WARNING when the workspace volume is
|
|
7948
7951
|
at or above `janitor_warn_pct`. The disk read runs even when the
|
|
7949
|
-
roster cannot be listed
|
|
7950
|
-
|
|
7952
|
+
roster cannot be listed, or the hub layout is not the one the janitor
|
|
7953
|
+
reads, and the removal half is skipped: a full volume is worth saying
|
|
7954
|
+
whatever else went wrong."""
|
|
7951
7955
|
if not self.config.janitor_enabled:
|
|
7952
7956
|
return None
|
|
7953
7957
|
mono = time.monotonic()
|
|
@@ -7969,6 +7973,22 @@ class ReviewWatcher:
|
|
|
7969
7973
|
return result
|
|
7970
7974
|
|
|
7971
7975
|
def _sweep_workspace(self, now: float) -> "janitor_mod.SweepResult | None":
|
|
7976
|
+
if not janitor_mod.layout_supported(
|
|
7977
|
+
self.config.hub_for, self.config.workspace_root
|
|
7978
|
+
):
|
|
7979
|
+
# Fail closed (issue #175): under another layout the hubs on disk
|
|
7980
|
+
# are not keyed by the slug the Guard holds (or are not found at
|
|
7981
|
+
# all), so a sweep could remove a live round's folders. Nothing is
|
|
7982
|
+
# removed or pruned; said once per process, not once per interval.
|
|
7983
|
+
if not self._janitor_layout_warned:
|
|
7984
|
+
self._janitor_layout_warned = True
|
|
7985
|
+
log.warning(
|
|
7986
|
+
"janitor: hub_template %r is not supported — the janitor "
|
|
7987
|
+
"supports only %s, so it removes and prunes nothing in "
|
|
7988
|
+
"this process",
|
|
7989
|
+
self.config.hub_template, janitor_mod.SUPPORTED_TEMPLATE,
|
|
7990
|
+
)
|
|
7991
|
+
return None
|
|
7972
7992
|
guard = self._janitor_guard(now)
|
|
7973
7993
|
if guard is None:
|
|
7974
7994
|
log.warning(
|
|
@@ -7986,17 +8006,20 @@ class ReviewWatcher:
|
|
|
7986
8006
|
if self.config.dry_run:
|
|
7987
8007
|
log.info(
|
|
7988
8008
|
"[dry-run] janitor: would remove %d folder(s) (%s) across %d "
|
|
7989
|
-
"hub(s); %d held by a live round, %d inside the grace"
|
|
8009
|
+
"hub(s); %d held by a live round, %d inside the grace, %d "
|
|
8010
|
+
"deferred to the next interval",
|
|
7990
8011
|
len(result.would_remove),
|
|
7991
8012
|
janitor_mod.human(sum(size for _, size in result.would_remove)),
|
|
7992
|
-
result.hubs, len(result.held), result.fresh,
|
|
8013
|
+
result.hubs, len(result.held), result.fresh, result.deferred,
|
|
7993
8014
|
)
|
|
7994
8015
|
else:
|
|
7995
8016
|
log.info(
|
|
7996
8017
|
"janitor: removed %d folder(s), freed %s across %d hub(s); "
|
|
7997
|
-
"%d held by a live round, %d inside the grace, %d failed"
|
|
8018
|
+
"%d held by a live round, %d inside the grace, %d failed, %d "
|
|
8019
|
+
"deferred to the next interval",
|
|
7998
8020
|
len(result.removed), janitor_mod.human(result.freed),
|
|
7999
8021
|
result.hubs, len(result.held), result.fresh, len(result.failed),
|
|
8022
|
+
result.deferred,
|
|
8000
8023
|
)
|
|
8001
8024
|
return result
|
|
8002
8025
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.31.19
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.18
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|