repoactive 0.2.3.dev2__tar.gz → 0.2.4.dev2__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.
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/PKG-INFO +1 -1
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/pyproject.toml +1 -1
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/jj.py +36 -16
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/platforms/gitlab.py +5 -2
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/runner.py +40 -13
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/.gitignore +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/LICENSE +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/README.md +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/__init__.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/cli.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/config.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/constants.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/graph.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/jobtree.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/lock.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/platforms/__init__.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/platforms/base.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/platforms/github.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/progress.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/settings.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/ui.py +0 -0
- {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev2}/src/repoactive/updates.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Wrapper around the jj (Jujutsu) CLI for managing colocated jj+git repositories."""
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
|
+
import enum
|
|
4
5
|
import logging
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
@@ -8,13 +9,19 @@ import tempfile
|
|
|
8
9
|
import time
|
|
9
10
|
from collections.abc import Generator
|
|
10
11
|
from dataclasses import dataclass
|
|
11
|
-
from datetime import datetime
|
|
12
|
+
from datetime import UTC, datetime
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
from repoactive.constants import JOB_TRAILER_KEY
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
19
|
+
|
|
20
|
+
class Colocation(enum.Enum):
|
|
21
|
+
COLOCATED = "colocated"
|
|
22
|
+
PLAIN = "plain"
|
|
23
|
+
|
|
24
|
+
|
|
18
25
|
# Prefix for the temporary workspaces repoactive creates for each job. It is not
|
|
19
26
|
# configurable so that stale workspaces (left behind by a killed run) can always
|
|
20
27
|
# be recognised and reclaimed by name (see JJ.forget_stale_workspaces).
|
|
@@ -181,9 +188,9 @@ class JJ:
|
|
|
181
188
|
)
|
|
182
189
|
raise CommandFailedError(program, args, e.stderr) from e
|
|
183
190
|
logger.debug(
|
|
184
|
-
"%s %s -> %d bytes in %.3fs",
|
|
191
|
+
"%s %s ... -> %d bytes in %.3fs",
|
|
185
192
|
program,
|
|
186
|
-
" ".join(args),
|
|
193
|
+
" ".join(args[:1]),
|
|
187
194
|
len(result.stdout),
|
|
188
195
|
time.monotonic() - start,
|
|
189
196
|
)
|
|
@@ -215,8 +222,8 @@ class JJ:
|
|
|
215
222
|
def edit(self, revision: str) -> None:
|
|
216
223
|
self._run("edit", revision)
|
|
217
224
|
|
|
218
|
-
def restore(self,
|
|
219
|
-
self._run("restore", "--
|
|
225
|
+
def restore(self, *, source_rev: str, destination_rev: str) -> None:
|
|
226
|
+
self._run("restore", "--from", source_rev, "--into", destination_rev)
|
|
220
227
|
|
|
221
228
|
def rebase(self, *onto: str) -> None:
|
|
222
229
|
onto_args = [arg for parent in onto for arg in ("--onto", parent)]
|
|
@@ -400,8 +407,10 @@ class JJ:
|
|
|
400
407
|
if name.strip()
|
|
401
408
|
}
|
|
402
409
|
|
|
403
|
-
def
|
|
404
|
-
|
|
410
|
+
def last_job_commit_date(
|
|
411
|
+
self, *, job_name: str, base: str, since: datetime
|
|
412
|
+
) -> datetime | None:
|
|
413
|
+
"""Return the committer date of the most recent job commit on ``base``, or ``None``.
|
|
405
414
|
|
|
406
415
|
Matches commits that have a ``Repoactive-Job: <job_name>`` trailer and a
|
|
407
416
|
committer date at or after ``since``. Used to throttle jobs: a recent
|
|
@@ -410,16 +419,22 @@ class JJ:
|
|
|
410
419
|
The trailer is matched via jj's trailer parsing, which only considers the
|
|
411
420
|
final paragraph of the description, so a stray matching line in the body
|
|
412
421
|
is correctly ignored.
|
|
422
|
+
|
|
423
|
+
Returns the newest matching committer timestamp, or ``None`` if no match.
|
|
413
424
|
"""
|
|
414
425
|
revset = f'::{base} & committer_date(after:"{_jj_timestamp(since)}")'
|
|
415
426
|
template = f"""
|
|
416
427
|
if (trailers.any(|t| t.key() == "{JOB_TRAILER_KEY}" && t.value() == "{job_name}"),
|
|
417
|
-
"
|
|
428
|
+
committer.timestamp().utc().format("%Y-%m-%dT%H:%M:%S") ++ "\\n",
|
|
418
429
|
""
|
|
419
430
|
)
|
|
420
431
|
"""
|
|
421
432
|
output = self._run("log", "--no-graph", "-r", revset, "-T", template)
|
|
422
|
-
|
|
433
|
+
lines = [line.strip() for line in output.splitlines() if line.strip()]
|
|
434
|
+
if not lines:
|
|
435
|
+
return None
|
|
436
|
+
# jj log returns newest-first; take the first line.
|
|
437
|
+
return datetime.fromisoformat(lines[0]).replace(tzinfo=UTC)
|
|
423
438
|
|
|
424
439
|
def git_push_bookmarks(self, *bookmarks: str) -> None:
|
|
425
440
|
"""Push bookmarks to the remote.
|
|
@@ -464,7 +479,7 @@ class JJ:
|
|
|
464
479
|
"""Sync the colocated git checkout (HEAD and index) to the jj working copy.
|
|
465
480
|
|
|
466
481
|
jj only exports git HEAD in the default workspace; workspaces colocated
|
|
467
|
-
via
|
|
482
|
+
via _workspace_add() need this after the working copy moves (new, edit,
|
|
468
483
|
rebase). No-op if the workspace is not colocated.
|
|
469
484
|
"""
|
|
470
485
|
if not self.is_colocated():
|
|
@@ -500,9 +515,11 @@ class JJ:
|
|
|
500
515
|
if stale:
|
|
501
516
|
self.git_worktree_prune()
|
|
502
517
|
|
|
503
|
-
def
|
|
518
|
+
def _workspace_add(
|
|
519
|
+
self, name: str, path: Path, colocation: Colocation = Colocation.COLOCATED
|
|
520
|
+
) -> None:
|
|
504
521
|
self._run("workspace", "add", "--name", name, str(path))
|
|
505
|
-
if self.is_colocated():
|
|
522
|
+
if colocation is Colocation.COLOCATED and self.is_colocated():
|
|
506
523
|
self._colocate_workspace(name, path)
|
|
507
524
|
|
|
508
525
|
def _colocate_workspace(self, name: str, path: Path) -> None:
|
|
@@ -533,7 +550,9 @@ class JJ:
|
|
|
533
550
|
self._run("workspace", "forget", name)
|
|
534
551
|
|
|
535
552
|
@contextlib.contextmanager
|
|
536
|
-
def temp_workspace(
|
|
553
|
+
def temp_workspace(
|
|
554
|
+
self, name: str, colocation: Colocation = Colocation.COLOCATED
|
|
555
|
+
) -> Generator["JJ"]:
|
|
537
556
|
"""Create a workspace named ``name`` in a temp directory, cleaning up on exit.
|
|
538
557
|
|
|
539
558
|
Adds a jj workspace inside a fresh temp directory and yields a JJ bound
|
|
@@ -544,7 +563,7 @@ class JJ:
|
|
|
544
563
|
tmp_root = Path(tempfile.mkdtemp(prefix="repoactive-workspace-"))
|
|
545
564
|
workspace_path = tmp_root / "workspace"
|
|
546
565
|
logger.debug("adding workspace %s at %s", name, workspace_path)
|
|
547
|
-
self.
|
|
566
|
+
self._workspace_add(name, workspace_path, colocation)
|
|
548
567
|
try:
|
|
549
568
|
yield JJ(workspace_path)
|
|
550
569
|
finally:
|
|
@@ -552,5 +571,6 @@ class JJ:
|
|
|
552
571
|
with contextlib.suppress(JJError):
|
|
553
572
|
self.workspace_forget(name)
|
|
554
573
|
shutil.rmtree(tmp_root, ignore_errors=True)
|
|
555
|
-
|
|
556
|
-
|
|
574
|
+
if colocation is Colocation.COLOCATED:
|
|
575
|
+
with contextlib.suppress(JJError):
|
|
576
|
+
self.git_worktree_prune()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""GitLab platform implementation using python-gitlab."""
|
|
2
2
|
|
|
3
3
|
import gitlab
|
|
4
|
-
from gitlab.exceptions import GitlabError
|
|
4
|
+
from gitlab.exceptions import GitlabError, GitlabMRClosedError
|
|
5
5
|
|
|
6
6
|
from repoactive.platforms.base import MRParams, Platform, PlatformError
|
|
7
7
|
|
|
@@ -50,5 +50,8 @@ class GitLabPlatform(Platform):
|
|
|
50
50
|
}
|
|
51
51
|
)
|
|
52
52
|
if params.auto_merge:
|
|
53
|
-
|
|
53
|
+
try:
|
|
54
|
+
mr.merge(merge_when_pipeline_succeeds=True)
|
|
55
|
+
except GitlabMRClosedError as e:
|
|
56
|
+
print(f" warning: could not enable auto-merge ({e})")
|
|
54
57
|
return mr.web_url
|
|
@@ -28,7 +28,7 @@ from repoactive.config import (
|
|
|
28
28
|
)
|
|
29
29
|
from repoactive.constants import JOB_TRAILER_KEY
|
|
30
30
|
from repoactive.graph import CircularDependencyError, detect_dependency_cycle, topological_sort
|
|
31
|
-
from repoactive.jj import JJ, workspace_name
|
|
31
|
+
from repoactive.jj import JJ, Colocation, workspace_name
|
|
32
32
|
from repoactive.jobtree import format_job_forest, print_job_table
|
|
33
33
|
from repoactive.lock import run_lock
|
|
34
34
|
from repoactive.platforms.base import MRParams, Platform
|
|
@@ -622,22 +622,43 @@ def _build_generated_jobs(
|
|
|
622
622
|
return emitted
|
|
623
623
|
|
|
624
624
|
|
|
625
|
-
def
|
|
626
|
-
"""
|
|
625
|
+
def _format_duration(seconds: float) -> str:
|
|
626
|
+
"""Format a duration in seconds as a human-readable string like '3d 2h' or '45m'."""
|
|
627
|
+
seconds = int(seconds)
|
|
628
|
+
days, seconds = divmod(seconds, 86400)
|
|
629
|
+
hours, seconds = divmod(seconds, 3600)
|
|
630
|
+
minutes, seconds = divmod(seconds, 60)
|
|
631
|
+
if days:
|
|
632
|
+
parts = [f"{days}d"]
|
|
633
|
+
if hours:
|
|
634
|
+
parts.append(f"{hours}h")
|
|
635
|
+
return " ".join(parts)
|
|
636
|
+
if hours:
|
|
637
|
+
parts = [f"{hours}h"]
|
|
638
|
+
if minutes:
|
|
639
|
+
parts.append(f"{minutes}m")
|
|
640
|
+
return " ".join(parts)
|
|
641
|
+
if minutes:
|
|
642
|
+
return f"{minutes}m"
|
|
643
|
+
return f"{seconds}s"
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _on_cooldown(job: Job, repo_path: Path) -> datetime | None:
|
|
647
|
+
"""Return the last-run timestamp if the job is still on cooldown, else None."""
|
|
627
648
|
delta = job.cooldown_timedelta()
|
|
628
649
|
if delta is None:
|
|
629
|
-
return
|
|
650
|
+
return None
|
|
630
651
|
base = job.base_branch or "trunk()"
|
|
631
652
|
since = datetime.now(UTC) - delta
|
|
632
|
-
|
|
653
|
+
last_run = JJ(repo_path).last_job_commit_date(job_name=job.name, base=base, since=since)
|
|
633
654
|
logger.debug(
|
|
634
|
-
"[%s] cooldown check: base=%s since=%s ->
|
|
655
|
+
"[%s] cooldown check: base=%s since=%s -> last_run=%s",
|
|
635
656
|
job.name,
|
|
636
657
|
base,
|
|
637
658
|
since.isoformat(),
|
|
638
|
-
|
|
659
|
+
last_run,
|
|
639
660
|
)
|
|
640
|
-
return
|
|
661
|
+
return last_run
|
|
641
662
|
|
|
642
663
|
|
|
643
664
|
def _validate_selection(
|
|
@@ -828,8 +849,13 @@ def _dispatch_job( # noqa: PLR0913
|
|
|
828
849
|
# emits nothing, throttling its whole fan-out as a unit (the dual trailer
|
|
829
850
|
# records a recent child landing as the generator's). See
|
|
830
851
|
# docs/adr/0004-job-generators.md.
|
|
831
|
-
if _on_cooldown(resolved_job, repo_path):
|
|
832
|
-
|
|
852
|
+
if last_run := _on_cooldown(resolved_job, repo_path):
|
|
853
|
+
elapsed = datetime.now(UTC) - last_run
|
|
854
|
+
elapsed_str = _format_duration(elapsed.total_seconds())
|
|
855
|
+
print(
|
|
856
|
+
f"==> [{job.name}] on cooldown ({resolved_job.cooldown_period}),"
|
|
857
|
+
f" last run {elapsed_str} ago, skipped"
|
|
858
|
+
)
|
|
833
859
|
summary.on_cooldown.add(job.name)
|
|
834
860
|
# Treat like a no-op run so dependents proceed on the base branch.
|
|
835
861
|
summary.results[job.name] = JobResult(
|
|
@@ -1004,9 +1030,10 @@ def _absorb_results(
|
|
|
1004
1030
|
)
|
|
1005
1031
|
if not content_unchanged:
|
|
1006
1032
|
if abs_ws is None:
|
|
1007
|
-
abs_ws = stack.enter_context(
|
|
1008
|
-
|
|
1009
|
-
|
|
1033
|
+
abs_ws = stack.enter_context(
|
|
1034
|
+
repo.temp_workspace("repoactive-absorb", Colocation.PLAIN)
|
|
1035
|
+
)
|
|
1036
|
+
abs_ws.restore(source_rev=new_cid, destination_rev=old_cid)
|
|
1010
1037
|
repo.describe_revision(old_cid, message)
|
|
1011
1038
|
elif _strip_boxquote_and_trailers(
|
|
1012
1039
|
repo.get_description(old_cid)
|
|
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
|