heatmap commits
This commit is contained in:
parent
086fc8fc49
commit
1fc1df5aeb
|
|
@ -30,11 +30,12 @@ if TYPE_CHECKING:
|
|||
# ---------------------------------------------------------------------------
|
||||
# Line-stats background cache
|
||||
# ---------------------------------------------------------------------------
|
||||
# Key: org_id string → (fetched_at, total_additions, total_deletions, has_data)
|
||||
# Key: org_id string → (fetched_at, total_additions, total_deletions, has_data, day_counts)
|
||||
# day_counts maps "YYYY-MM-DD" → commit count for the heatmap grid.
|
||||
# Populated by a fire-and-forget asyncio task so the heatmap endpoint never
|
||||
# blocks waiting for Forgejo's 202 "still computing" response.
|
||||
# ---------------------------------------------------------------------------
|
||||
_line_stats_cache: dict[str, tuple[float, int, int, bool]] = {}
|
||||
_line_stats_cache: dict[str, tuple[float, int, int, bool, dict[str, int]]] = {}
|
||||
_line_stats_fetching: set[str] = set()
|
||||
_LINE_STATS_TTL_HIT = 300 # 5 min — re-fetch cadence once real data is cached
|
||||
_LINE_STATS_TTL_MISS = 30 # 30 s — retry cadence while Forgejo is still computing
|
||||
|
|
@ -47,19 +48,23 @@ async def _bg_fetch_line_stats(
|
|||
) -> None:
|
||||
"""Background task: sum commit line stats across all tracked repos and cache."""
|
||||
|
||||
async def _one(owner: str, repo: str, base_url: str, token: str | None) -> tuple[int, int]:
|
||||
async def _one(owner: str, repo: str, base_url: str, token: str | None) -> tuple[int, int, dict[str, int]]:
|
||||
try:
|
||||
async with ForgejoAPIClient(base_url=base_url, token=token) as client:
|
||||
return await client.get_commit_line_stats_since(owner, repo, since_iso)
|
||||
except Exception:
|
||||
return 0, 0
|
||||
return 0, 0, {}
|
||||
|
||||
try:
|
||||
results = await asyncio.gather(*[_one(o, r, bu, tok) for o, r, bu, tok in repos])
|
||||
total_adds = sum(a for a, _ in results)
|
||||
total_dels = sum(d for _, d in results)
|
||||
total_adds = sum(a for a, _, _ in results)
|
||||
total_dels = sum(d for _, d, _ in results)
|
||||
merged_days: dict[str, int] = {}
|
||||
for _, _, day_counts in results:
|
||||
for day, cnt in day_counts.items():
|
||||
merged_days[day] = merged_days.get(day, 0) + cnt
|
||||
# Always mark has_data=True — the commits endpoint is reliable
|
||||
_line_stats_cache[cache_key] = (_time.monotonic(), total_adds, total_dels, True)
|
||||
_line_stats_cache[cache_key] = (_time.monotonic(), total_adds, total_dels, True, merged_days)
|
||||
finally:
|
||||
_line_stats_fetching.discard(cache_key)
|
||||
|
||||
|
|
@ -400,51 +405,8 @@ async def get_forgejo_heatmap(
|
|||
return HeatmapResponse(days=[], max_count=0)
|
||||
|
||||
repo_ids = [repo.id for repo, _ in repos_with_conns]
|
||||
counts: dict[str, int] = {}
|
||||
|
||||
# Issues created per day
|
||||
created_rows = (
|
||||
await session.exec(
|
||||
select(
|
||||
sa_cast(ForgejoIssue.forgejo_created_at, SADate).label("day"),
|
||||
func.count().label("cnt"),
|
||||
).where(
|
||||
ForgejoIssue.repository_id.in_(repo_ids),
|
||||
ForgejoIssue.is_pull_request.is_(False),
|
||||
ForgejoIssue.forgejo_created_at.is_not(None),
|
||||
ForgejoIssue.forgejo_created_at >= since,
|
||||
).group_by(sa_cast(ForgejoIssue.forgejo_created_at, SADate))
|
||||
)
|
||||
).all()
|
||||
for day, cnt in created_rows:
|
||||
if day:
|
||||
key = str(day)
|
||||
counts[key] = counts.get(key, 0) + int(cnt)
|
||||
|
||||
# Issues closed per day
|
||||
closed_rows = (
|
||||
await session.exec(
|
||||
select(
|
||||
sa_cast(ForgejoIssue.forgejo_closed_at, SADate).label("day"),
|
||||
func.count().label("cnt"),
|
||||
).where(
|
||||
ForgejoIssue.repository_id.in_(repo_ids),
|
||||
ForgejoIssue.is_pull_request.is_(False),
|
||||
ForgejoIssue.forgejo_closed_at.is_not(None),
|
||||
ForgejoIssue.forgejo_closed_at >= since,
|
||||
).group_by(sa_cast(ForgejoIssue.forgejo_closed_at, SADate))
|
||||
)
|
||||
).all()
|
||||
for day, cnt in closed_rows:
|
||||
if day:
|
||||
key = str(day)
|
||||
counts[key] = counts.get(key, 0) + int(cnt)
|
||||
|
||||
days_list = [HeatmapDay(date=k, count=v) for k, v in sorted(counts.items())]
|
||||
max_count = max((d.count for d in days_list), default=0)
|
||||
|
||||
# Line stats — served from cache; background task refreshes when stale.
|
||||
# Extract plain values NOW while the DB session is still open.
|
||||
# Line stats — served from background cache; fire refresh if stale.
|
||||
since_iso = line_stats_since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
cache_key = str(ctx.organization.id)
|
||||
cached = _line_stats_cache.get(cache_key)
|
||||
|
|
@ -472,10 +434,58 @@ async def get_forgejo_heatmap(
|
|||
)
|
||||
|
||||
if cached is not None:
|
||||
_, total_additions, total_deletions, has_line_stats = cached
|
||||
_, total_additions, total_deletions, has_line_stats, commit_day_counts = cached
|
||||
else:
|
||||
total_additions = total_deletions = 0
|
||||
has_line_stats = False
|
||||
commit_day_counts = {}
|
||||
|
||||
# Heatmap grid: use commit-per-day counts when available; fall back to
|
||||
# issue-event counts (from DB) while the background fetch is in progress.
|
||||
if has_line_stats and commit_day_counts:
|
||||
days_list = [HeatmapDay(date=k, count=v) for k, v in sorted(commit_day_counts.items())]
|
||||
max_count = max((d.count for d in days_list), default=0)
|
||||
else:
|
||||
counts: dict[str, int] = {}
|
||||
|
||||
created_rows = (
|
||||
await session.exec(
|
||||
select(
|
||||
sa_cast(ForgejoIssue.forgejo_created_at, SADate).label("day"),
|
||||
func.count().label("cnt"),
|
||||
).where(
|
||||
ForgejoIssue.repository_id.in_(repo_ids),
|
||||
ForgejoIssue.is_pull_request.is_(False),
|
||||
ForgejoIssue.forgejo_created_at.is_not(None),
|
||||
ForgejoIssue.forgejo_created_at >= since,
|
||||
).group_by(sa_cast(ForgejoIssue.forgejo_created_at, SADate))
|
||||
)
|
||||
).all()
|
||||
for day, cnt in created_rows:
|
||||
if day:
|
||||
key = str(day)
|
||||
counts[key] = counts.get(key, 0) + int(cnt)
|
||||
|
||||
closed_rows = (
|
||||
await session.exec(
|
||||
select(
|
||||
sa_cast(ForgejoIssue.forgejo_closed_at, SADate).label("day"),
|
||||
func.count().label("cnt"),
|
||||
).where(
|
||||
ForgejoIssue.repository_id.in_(repo_ids),
|
||||
ForgejoIssue.is_pull_request.is_(False),
|
||||
ForgejoIssue.forgejo_closed_at.is_not(None),
|
||||
ForgejoIssue.forgejo_closed_at >= since,
|
||||
).group_by(sa_cast(ForgejoIssue.forgejo_closed_at, SADate))
|
||||
)
|
||||
).all()
|
||||
for day, cnt in closed_rows:
|
||||
if day:
|
||||
key = str(day)
|
||||
counts[key] = counts.get(key, 0) + int(cnt)
|
||||
|
||||
days_list = [HeatmapDay(date=k, count=v) for k, v in sorted(counts.items())]
|
||||
max_count = max((d.count for d in days_list), default=0)
|
||||
|
||||
return HeatmapResponse(
|
||||
days=days_list,
|
||||
|
|
|
|||
|
|
@ -374,15 +374,17 @@ class ForgejoAPIClient:
|
|||
|
||||
async def get_commit_line_stats_since(
|
||||
self, owner: str, repo: str, since_iso: str
|
||||
) -> tuple[int, int]:
|
||||
"""Sum additions and deletions across all commits since ``since_iso``.
|
||||
) -> tuple[int, int, dict[str, int]]:
|
||||
"""Sum additions/deletions and count commits per day since ``since_iso``.
|
||||
|
||||
Uses ``GET /repos/{owner}/{repo}/commits?since=…&stat=true`` which
|
||||
returns per-commit ``stats`` objects and is available on all Forgejo/
|
||||
Gitea versions. Returns ``(total_additions, total_deletions)``.
|
||||
Gitea versions. Returns ``(total_additions, total_deletions, day_counts)``
|
||||
where ``day_counts`` maps "YYYY-MM-DD" → commit count.
|
||||
"""
|
||||
client = await self._get_client()
|
||||
total_adds = total_dels = 0
|
||||
day_counts: dict[str, int] = {}
|
||||
page = 1
|
||||
# Cap at 5 pages (250 commits) — prevents unbounded pagination on
|
||||
# very active repos where each page requires Forgejo to compute
|
||||
|
|
@ -403,10 +405,17 @@ class ForgejoAPIClient:
|
|||
s = commit.get("stats") or {}
|
||||
total_adds += int(s.get("additions") or 0)
|
||||
total_dels += int(s.get("deletions") or 0)
|
||||
# Extract commit date for per-day activity counting
|
||||
commit_obj = commit.get("commit") or {}
|
||||
author_obj = commit_obj.get("author") or {}
|
||||
date_str: str = author_obj.get("date") or commit.get("created") or ""
|
||||
if date_str and len(date_str) >= 10:
|
||||
day = date_str[:10]
|
||||
day_counts[day] = day_counts.get(day, 0) + 1
|
||||
if len(commits) < 50:
|
||||
break # last page
|
||||
page += 1
|
||||
return total_adds, total_dels
|
||||
return total_adds, total_dels, day_counts
|
||||
|
||||
|
||||
def get_forgejo_client(
|
||||
|
|
|
|||
Loading…
Reference in New Issue