post to .profile
This commit is contained in:
+79
-12
@@ -2,9 +2,9 @@
|
||||
"""
|
||||
Gitea Language Stats
|
||||
Calculates the percentage of every programming language used
|
||||
in all repositories of a given account and prints a markdown
|
||||
table to stdout, delimited by the
|
||||
markers <!--LANG_STATS_START--> … <!--LANG_STATS_END-->.
|
||||
in all repositories of a given account and updates the README.md
|
||||
in the .profile repository between the markers
|
||||
<!--LANG_STATS_START--> and <!--LANG_STATS_END-->.
|
||||
|
||||
When run as a systemd job, it checks for new commits since the last run
|
||||
and only regenerates if there are changes. Use --force to always regenerate.
|
||||
@@ -12,7 +12,7 @@ and only regenerates if there are changes. Use --force to always regenerate.
|
||||
Required environment variables
|
||||
--------------------------------
|
||||
GITEA_BASE_URL e.g. https://gitea.example.com
|
||||
GITEA_TOKEN personal access-token with `repo` scope
|
||||
GITEA_TOKEN personal access-token with `repo` scope (needs write access)
|
||||
GITEA_USERNAME the account to analyse
|
||||
|
||||
Optional environment variables
|
||||
@@ -20,8 +20,10 @@ Optional environment variables
|
||||
STATE_FILE path to store last-run state (default: ~/.local/state/language_stats.json)
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
@@ -38,6 +40,14 @@ USERNAME = os.getenv("GITEA_USERNAME")
|
||||
DEFAULT_STATE_DIR = Path.home() / ".local" / "state"
|
||||
STATE_FILE = Path(os.getenv("STATE_FILE", DEFAULT_STATE_DIR / "language_stats.json"))
|
||||
|
||||
# Profile repository where README will be updated
|
||||
PROFILE_REPO = ".profile"
|
||||
README_PATH = "README.md"
|
||||
|
||||
# Markers for content replacement
|
||||
MARKER_START = "<!--LANG_STATS_START-->"
|
||||
MARKER_END = "<!--LANG_STATS_END-->"
|
||||
|
||||
# List of files whose byte-counts should be excluded from the totals.
|
||||
# Each tuple: (repository_name, file_path, language_name)
|
||||
EXCLUDE_FILES: list[tuple[str, str, str]] = [
|
||||
@@ -80,8 +90,16 @@ def fetch_repos(session: requests.Session) -> list[dict]:
|
||||
|
||||
|
||||
def get_repo_timestamps(repos: list[dict]) -> dict[str, str]:
|
||||
"""Extract repository names and their updated_at timestamps."""
|
||||
return {repo["name"]: repo.get("updated_at", "") for repo in repos}
|
||||
"""Extract repository names and their updated_at timestamps.
|
||||
|
||||
Excludes the profile repo to avoid infinite loops (updating it
|
||||
would trigger another update).
|
||||
"""
|
||||
return {
|
||||
repo["name"]: repo.get("updated_at", "")
|
||||
for repo in repos
|
||||
if repo["name"] != PROFILE_REPO
|
||||
}
|
||||
|
||||
|
||||
def load_state() -> dict:
|
||||
@@ -151,11 +169,11 @@ def subtract_excluded_files(session: requests.Session, totals: dict[str, int]) -
|
||||
return totals
|
||||
|
||||
|
||||
def generate_markdown(totals: dict[str, int]) -> list[str]:
|
||||
def generate_markdown(totals: dict[str, int]) -> str:
|
||||
"""Generate markdown output for language statistics."""
|
||||
total_bytes = sum(totals.values())
|
||||
if total_bytes == 0:
|
||||
return []
|
||||
return ""
|
||||
|
||||
pairs = sorted(totals.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
@@ -165,7 +183,51 @@ def generate_markdown(totals: dict[str, int]) -> list[str]:
|
||||
bar = "█" * int(pct / 2) # up to 50 chars
|
||||
lines.append(f"{bar} {lang}{pct:5.1f}%\\")
|
||||
|
||||
return lines
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def fetch_readme(session: requests.Session) -> tuple[str, str]:
|
||||
"""Fetch the README from the profile repository.
|
||||
|
||||
Returns:
|
||||
Tuple of (content, sha) where sha is needed for updating.
|
||||
"""
|
||||
resp = session.get(
|
||||
f"{BASE_URL}/api/v1/repos/{USERNAME}/{PROFILE_REPO}/contents/{README_PATH}",
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
content = base64.b64decode(data["content"]).decode("utf-8")
|
||||
return content, data["sha"]
|
||||
|
||||
|
||||
def replace_between_markers(content: str, new_content: str) -> str:
|
||||
"""Replace content between LANG_STATS markers."""
|
||||
pattern = re.compile(
|
||||
rf"({re.escape(MARKER_START)})(.*?)({re.escape(MARKER_END)})",
|
||||
re.DOTALL,
|
||||
)
|
||||
replacement = rf"\1\n{new_content}\n\3"
|
||||
new_readme, count = pattern.subn(replacement, content)
|
||||
if count == 0:
|
||||
raise ValueError(f"Markers {MARKER_START} and {MARKER_END} not found in README")
|
||||
return new_readme
|
||||
|
||||
|
||||
def update_readme(session: requests.Session, content: str, sha: str) -> None:
|
||||
"""Update the README in the profile repository."""
|
||||
encoded_content = base64.b64encode(content.encode("utf-8")).decode("ascii")
|
||||
resp = session.put(
|
||||
f"{BASE_URL}/api/v1/repos/{USERNAME}/{PROFILE_REPO}/contents/{README_PATH}",
|
||||
json={
|
||||
"content": encoded_content,
|
||||
"sha": sha,
|
||||
"message": "Update language statistics",
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -213,9 +275,14 @@ def main() -> int:
|
||||
if sum(totals.values()) == 0:
|
||||
sys.exit("No language data returned from API.")
|
||||
|
||||
lines = generate_markdown(totals)
|
||||
for line in lines:
|
||||
print(line)
|
||||
markdown = generate_markdown(totals)
|
||||
|
||||
# Fetch current README, update it, and push
|
||||
readme_content, readme_sha = fetch_readme(session)
|
||||
updated_readme = replace_between_markers(readme_content, markdown)
|
||||
update_readme(session, updated_readme, readme_sha)
|
||||
|
||||
print("README.md updated successfully.", file=sys.stderr)
|
||||
|
||||
# Save current state for next run
|
||||
new_state = {"repo_timestamps": current_timestamps}
|
||||
|
||||
Reference in New Issue
Block a user