repoactive 0.2.3.dev2__tar.gz → 0.2.4.dev1__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 (22) hide show
  1. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/PKG-INFO +1 -1
  2. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/pyproject.toml +1 -1
  3. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/jj.py +15 -7
  4. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/runner.py +36 -11
  5. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/.gitignore +0 -0
  6. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/LICENSE +0 -0
  7. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/README.md +0 -0
  8. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/__init__.py +0 -0
  9. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/cli.py +0 -0
  10. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/config.py +0 -0
  11. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/constants.py +0 -0
  12. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/graph.py +0 -0
  13. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/jobtree.py +0 -0
  14. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/lock.py +0 -0
  15. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/platforms/__init__.py +0 -0
  16. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/platforms/base.py +0 -0
  17. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/platforms/github.py +0 -0
  18. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/platforms/gitlab.py +0 -0
  19. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/progress.py +0 -0
  20. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/settings.py +0 -0
  21. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/ui.py +0 -0
  22. {repoactive-0.2.3.dev2 → repoactive-0.2.4.dev1}/src/repoactive/updates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repoactive
3
- Version: 0.2.3.dev2
3
+ Version: 0.2.4.dev1
4
4
  Summary: Script-driven code changes with automated merge requests
5
5
  License-Expression: GPL-3.0-or-later
6
6
  License-File: LICENSE
@@ -10,7 +10,7 @@ only-include = ["src"]
10
10
 
11
11
  [project]
12
12
  name = "repoactive"
13
- version = "0.2.3.dev2"
13
+ version = "0.2.4.dev1"
14
14
  description = "Script-driven code changes with automated merge requests"
15
15
  readme = "README.md"
16
16
  requires-python = ">=3.11"
@@ -8,7 +8,7 @@ import tempfile
8
8
  import time
9
9
  from collections.abc import Generator
10
10
  from dataclasses import dataclass
11
- from datetime import datetime
11
+ from datetime import UTC, datetime
12
12
  from pathlib import Path
13
13
 
14
14
  from repoactive.constants import JOB_TRAILER_KEY
@@ -215,8 +215,8 @@ class JJ:
215
215
  def edit(self, revision: str) -> None:
216
216
  self._run("edit", revision)
217
217
 
218
- def restore(self, source: str) -> None:
219
- self._run("restore", "--changes-in", source)
218
+ def restore(self, *, source_rev: str, destination_rev: str) -> None:
219
+ self._run("restore", "--from", source_rev, "--into", destination_rev)
220
220
 
221
221
  def rebase(self, *onto: str) -> None:
222
222
  onto_args = [arg for parent in onto for arg in ("--onto", parent)]
@@ -400,8 +400,10 @@ class JJ:
400
400
  if name.strip()
401
401
  }
402
402
 
403
- def has_recent_job_commit(self, *, job_name: str, base: str, since: datetime) -> bool:
404
- """Whether ``base`` already carries a recent commit from the given job.
403
+ def last_job_commit_date(
404
+ self, *, job_name: str, base: str, since: datetime
405
+ ) -> datetime | None:
406
+ """Return the committer date of the most recent job commit on ``base``, or ``None``.
405
407
 
406
408
  Matches commits that have a ``Repoactive-Job: <job_name>`` trailer and a
407
409
  committer date at or after ``since``. Used to throttle jobs: a recent
@@ -410,16 +412,22 @@ class JJ:
410
412
  The trailer is matched via jj's trailer parsing, which only considers the
411
413
  final paragraph of the description, so a stray matching line in the body
412
414
  is correctly ignored.
415
+
416
+ Returns the newest matching committer timestamp, or ``None`` if no match.
413
417
  """
414
418
  revset = f'::{base} & committer_date(after:"{_jj_timestamp(since)}")'
415
419
  template = f"""
416
420
  if (trailers.any(|t| t.key() == "{JOB_TRAILER_KEY}" && t.value() == "{job_name}"),
417
- "x",
421
+ committer.timestamp().utc().format("%Y-%m-%dT%H:%M:%S") ++ "\\n",
418
422
  ""
419
423
  )
420
424
  """
421
425
  output = self._run("log", "--no-graph", "-r", revset, "-T", template)
422
- return bool(output.strip())
426
+ lines = [line.strip() for line in output.splitlines() if line.strip()]
427
+ if not lines:
428
+ return None
429
+ # jj log returns newest-first; take the first line.
430
+ return datetime.fromisoformat(lines[0]).replace(tzinfo=UTC)
423
431
 
424
432
  def git_push_bookmarks(self, *bookmarks: str) -> None:
425
433
  """Push bookmarks to the remote.
@@ -622,22 +622,43 @@ def _build_generated_jobs(
622
622
  return emitted
623
623
 
624
624
 
625
- def _on_cooldown(job: Job, repo_path: Path) -> bool:
626
- """Whether the job landed on its base branch within its cooldown_period window."""
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 False
650
+ return None
630
651
  base = job.base_branch or "trunk()"
631
652
  since = datetime.now(UTC) - delta
632
- on_cooldown = JJ(repo_path).has_recent_job_commit(job_name=job.name, base=base, since=since)
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 -> on_cooldown=%s",
655
+ "[%s] cooldown check: base=%s since=%s -> last_run=%s",
635
656
  job.name,
636
657
  base,
637
658
  since.isoformat(),
638
- on_cooldown,
659
+ last_run,
639
660
  )
640
- return on_cooldown
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
- print(f"==> [{job.name}] on cooldown ({resolved_job.cooldown_period}), skipped")
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(
@@ -1005,8 +1031,7 @@ def _absorb_results(
1005
1031
  if not content_unchanged:
1006
1032
  if abs_ws is None:
1007
1033
  abs_ws = stack.enter_context(repo.temp_workspace("repoactive-absorb"))
1008
- abs_ws.edit(old_cid)
1009
- abs_ws.restore(new_cid)
1034
+ abs_ws.restore(source_rev=new_cid, destination_rev=old_cid)
1010
1035
  repo.describe_revision(old_cid, message)
1011
1036
  elif _strip_boxquote_and_trailers(
1012
1037
  repo.get_description(old_cid)
File without changes