-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathcommit_fetcher.py
More file actions
124 lines (98 loc) · 3.86 KB
/
Copy pathcommit_fetcher.py
File metadata and controls
124 lines (98 loc) · 3.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""3-layer commit fetch (in-memory, disk cache, GitHub API) + CachedCommit shim.
The enrichment phase fetches per-commit data via PyGithub. Repeated access to the
same SHA within a run (O(n squared) bug in github_data.py) and between runs (no
persistent cache) is the main reason the pipeline is slow. This module unifies
both caches so github_data.py only calls a single function per SHA.
"""
from __future__ import annotations
from datetime import datetime
from types import SimpleNamespace
from github import RateLimitExceededException
from lib.github_cache import GithubCache, extract_commit_data
def _parse_iso(s):
if not s:
return None
if s.endswith("Z"):
s = s[:-1] + "+00:00"
return datetime.fromisoformat(s)
class _CachedComment:
__slots__ = ("user", "created_at", "body")
def __init__(self, d):
self.user = SimpleNamespace(login=d.get("author"))
self.created_at = _parse_iso(d.get("date"))
self.body = d.get("body", "")
class _CachedFile:
__slots__ = ("filename", "additions", "deletions", "changes",
"status", "previous_filename", "patch")
def __init__(self, d):
self.filename = d["filename"]
self.additions = d["additions"]
self.deletions = d["deletions"]
self.changes = d["changes"]
self.status = d["status"]
self.previous_filename = d.get("previous_filename")
self.patch = d.get("patch")
class CachedCommit:
"""Expose the PyGithub Commit interface over a cached dict."""
def __init__(self, data):
self._comments = [_CachedComment(c) for c in data.get("comments", [])]
self._files = [_CachedFile(f) for f in data.get("files", [])]
self.sha = data["sha"]
self.files = self._files
self.stats = SimpleNamespace(
additions=data.get("additions", 0),
deletions=data.get("deletions", 0),
total=data.get("files_changed", 0),
)
self.commit = SimpleNamespace(
sha=data["sha"],
message=data.get("message", ""),
author=SimpleNamespace(
name=data.get("author_name"),
date=_parse_iso(data.get("author_date")),
),
committer=SimpleNamespace(
name=data.get("committer_name"),
date=_parse_iso(data.get("committer_date")),
),
parents=[SimpleNamespace(sha=p) for p in data.get("parents", [])],
verification=SimpleNamespace(verified=bool(data.get("is_signed", False))),
)
def get_comments(self):
return self._comments
def _rotate_token(config):
"""Thin wrapper around utils.get_token so tests can patch this symbol."""
import sys
from pathlib import Path
scripts_dir = Path(__file__).resolve().parent.parent
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
import utils # type: ignore
return utils.get_token(config)
def fetch_commit(repo, sha, git, config, *, cache, sha_cache):
"""Fetch a commit, checking in-memory then disk cache before hitting the API.
Returns a PyGithub Commit (on API miss) or CachedCommit (on disk hit).
Both expose the same attributes consumed by scripts/github_data.py.
"""
sha = sha.strip()
if sha in sha_cache:
return sha_cache[sha]
if cache is not None:
cached = cache.get(sha)
if cached is not None:
obj = CachedCommit(cached)
sha_cache[sha] = obj
return obj
try:
commit = repo.get_commit(sha=sha)
except RateLimitExceededException:
_rotate_token(config)
commit = repo.get_commit(sha=sha)
if cache is not None:
try:
data = extract_commit_data(commit)
cache.put(sha, data)
except Exception:
pass
sha_cache[sha] = commit
return commit