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.
Files changed (40) hide show
  1. {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
  2. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/janitor.py +65 -1
  3. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/loop.py +28 -5
  4. alissa_tools_github_revloop-0.31.19/src/main/alissa/tools/github/revloop/version +1 -0
  5. {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
  6. alissa_tools_github_revloop-0.31.18/src/main/alissa/tools/github/revloop/version +0 -1
  7. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/LICENSE +0 -0
  8. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/MANIFEST.in +0 -0
  9. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/NOTICE +0 -0
  10. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/README.md +0 -0
  11. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/requirements.txt +0 -0
  12. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/setup.cfg +0 -0
  13. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/setup.py +0 -0
  14. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
  15. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
  16. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
  17. {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
  18. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/bows.py +0 -0
  19. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/config.py +0 -0
  20. {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
  21. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
  22. {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
  23. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/parking.py +0 -0
  24. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/proc.py +0 -0
  25. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
  26. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
  27. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/state.py +0 -0
  28. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/trust.py +0 -0
  29. {alissa_tools_github_revloop-0.31.18 → alissa_tools_github_revloop-0.31.19}/src/main/alissa/tools/github/revloop/version.py +0 -0
  30. {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
  31. {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
  32. {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
  33. {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
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.31.18
3
+ Version: 0.31.19
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -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 and the removal half is skipped: a full
7950
- volume is worth saying whatever else went wrong."""
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.31.18
3
+ Version: 0.31.19
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera