#!/usr/bin/env python3 import argparse import tomlkit import json from datetime import datetime from git import Repo, Commit from pathlib import Path import logging from typing import Callable import appslib.get_apps_repo as get_apps_repo def git_bisect(repo: Repo, is_newer: Callable[[Commit], bool]) -> Commit | None: # Start with whole repo first_commit = repo.git.rev_list("HEAD", reverse=True, max_parents=0) repo.git.bisect("reset") repo.git.bisect("start", "--no-checkout", "HEAD", first_commit) while True: try: status = "bad" if is_newer(repo.commit("BISECT_HEAD")) else "good" except Exception: status = "skip" result_string = repo.git.bisect(status) if "is the first bad commit" in result_string.splitlines()[0]: return repo.commit(result_string.splitlines()[0].split(" ", 1)[0]) def get_app_info(commit: Commit, filebase: str, name: str) -> dict | None: data = None try: filestream = commit.tree.join(f"{filebase}.toml") filedata = filestream.data_stream.read().decode("utf-8") dictdata = tomlkit.loads(filedata) data = dictdata[name] except KeyError: pass try: filestream = commit.tree.join(f"{filebase}.json") filedata = filestream.data_stream.read().decode("utf-8") dictdata = json.loads(filedata) data = dictdata[name] except KeyError: pass assert isinstance(data, dict) or data is None return data def app_is_present(commit: Commit, name: str) -> bool: info = get_app_info(commit, "apps", name) # if info is None: # info = get_app_info(commit, "graveyard", name) return info is not None def app_is_deprecated(commit: Commit, name: str) -> bool: info = get_app_info(commit, "apps", name) if info is None: return False antifeatures = info.get("antifeatures", []) return "deprecated-software" in antifeatures def date_added(repo: Repo, name: str) -> int | None: result = git_bisect(repo, lambda x: app_is_present(x, name)) print(result) return None if result is None else result.committed_date def date_deprecated(repo: Repo, name: str) -> int | None: result = git_bisect(repo, lambda x: app_is_deprecated(x, name)) print(result) return None if result is None else result.committed_date def add_deprecation_dates(repo: Repo, file: Path) -> None: key = "deprecated_date" document = tomlkit.load(file.open("r", encoding="utf-8")) for app, info in document.items(): if key in info.keys(): continue if "deprecated-software" not in info.get("antifeatures", []): continue date = date_deprecated(repo, app) if date is None: continue info[key] = date info[key].comment(datetime.fromtimestamp(info[key]).strftime("%Y/%m/%d")) info[key].trivia.comment_ws = " " tomlkit.dump(document, file.open("w")) def date_added_to(repo: Repo, match: str, file: Path) -> int | None: commits = repo.git.log( "-S", match, "--first-parent", "--reverse", "--date=unix", "--format=%cd", "--", str(file), ).splitlines() if not commits: return None first_commit = commits[0] return int(first_commit) def add_apparition_dates(repo: Repo, file: Path, key: str) -> None: document = tomlkit.load(file.open("r", encoding="utf-8")) for app, info in document.items(): if key in info.keys(): continue date = date_added_to(repo, f"[{app}]", file) assert date is not None info[key] = date info[key].comment(datetime.fromtimestamp(info[key]).strftime("%Y/%m/%d")) info[key].trivia.comment_ws = " " tomlkit.dump(document, file.open("w")) def main() -> None: parser = argparse.ArgumentParser() get_apps_repo.add_args(parser, allow_temp=False) args = parser.parse_args() logging.basicConfig(level=logging.DEBUG) apps_repo_dir = get_apps_repo.from_args(args) apps_repo = Repo(apps_repo_dir) add_apparition_dates(apps_repo, apps_repo_dir / "apps.toml", key="added_date") add_apparition_dates(apps_repo, apps_repo_dir / "wishlist.toml", key="added_date") add_apparition_dates(apps_repo, apps_repo_dir / "graveyard.toml", key="killed_date") add_deprecation_dates(apps_repo, apps_repo_dir / "apps.toml") add_deprecation_dates(apps_repo, apps_repo_dir / "graveyard.toml") if __name__ == "__main__": main()