fix: resolve forgejo date pins by walking history

This commit is contained in:
Abdessamad Derraz committed 2026-10-16 12:29:34 +02:00
1 parent 7744c21b65
commit 3c04a29d50
2 files changed
+92 -1

No files matched your search

+51 -1
View File
@@ -18,6 +18,7 @@ import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
USER_AGENT = "retrobios-profile-sync/1.0"
@@ -388,7 +389,56 @@ def resolve_commit_at(
branch: str | None = None,
) -> str | None:
"""Last commit on or before a date, on the default branch or a named one."""
return _first_sha(_api(_commits_url(repo, date, branch), cache_dir, offline))
if repo.family in ("github", "gitlab"):
return _first_sha(_api(_commits_url(repo, date, branch), cache_dir, offline))
return _gitea_commit_at(repo, date, cache_dir, offline, branch)
# Gitea and Forgejo serve 50 commits a page at most.
GITEA_PAGE = 50
def _gitea_commit_at(
repo: Repo, date: str, cache_dir: str, offline: bool,
branch: str | None,
) -> str | None:
"""Walk the history newest first to the first commit dated by the cutoff.
The Gitea and Forgejo commits API ignores ``until`` and answers with the
tip: gam4980's profiled_date of 2026-03-23 resolved to a commit of July,
a revision the profile never read.
"""
cutoff = datetime.fromisoformat(f"{date}T23:59:59+00:00")
base = (
f"{repo.api_base}/repos/{repo.slug}/commits?limit={GITEA_PAGE}"
"&stat=false&verification=false&files=false"
)
if branch:
base += f"&sha={urllib.parse.quote(branch, safe='')}"
page = 1
while True:
payload = _api(f"{base}&page={page}", cache_dir, offline, MOVING_TTL)
if not isinstance(payload, list) or not payload:
return None
for entry in payload:
stamp = _gitea_commit_date(entry)
if stamp is not None and stamp <= cutoff:
return entry.get("sha")
page += 1
def _gitea_commit_date(entry: object) -> datetime | None:
if not isinstance(entry, dict):
return None
commit = entry.get("commit")
committer = commit.get("committer") if isinstance(commit, dict) else None
raw = committer.get("date") if isinstance(committer, dict) else None
if not isinstance(raw, str):
return None
try:
return datetime.fromisoformat(raw.replace("Z", "+00:00"))
except ValueError:
return None
def _tags_url(repo: Repo) -> str:
+41
View File
@@ -648,5 +648,46 @@ class TestGitlabTree(unittest.TestCase):
self.assertTrue(truncated)
class TestGiteaCommitAt(unittest.TestCase):
"""Gitea and Forgejo ignore `until` and answer with the tip: the date
pin of a Codeberg view was the tip of the day of the first fetch."""
PAGES = {
1: [{"sha": f"t{i}", "commit": {"committer": {"date": "2026-07-04T10:00:00+02:00"}}}
for i in range(50)],
2: [
{"sha": "late", "commit": {"committer": {"date": "2026-03-24T05:00:00+02:00"}}},
{"sha": "pin", "commit": {"committer": {"date": "2026-03-17T19:49:15+08:00"}}},
],
}
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.urls: list[str] = []
original = upstream._http_json
def fake(url):
self.urls.append(url)
self.assertNotIn("until=", url)
page = int(url.rsplit("page=", 1)[1])
return self.PAGES.get(page, [])
upstream._http_json = fake
self.addCleanup(setattr, upstream, "_http_json", original)
def test_the_first_commit_by_the_cutoff(self):
repo = parse_repo("https://codeberg.org/iyzsong/gam4980")
sha = upstream.resolve_commit_at(repo, "2026-03-23", self.tmp.name)
self.assertEqual(sha, "pin")
self.assertEqual(len(self.urls), 2)
def test_a_date_before_the_history_resolves_nothing(self):
repo = parse_repo("https://codeberg.org/iyzsong/gam4980")
self.assertIsNone(
upstream.resolve_commit_at(repo, "2020-01-01", self.tmp.name)
)
if __name__ == "__main__":
unittest.main()