-
Notifications
You must be signed in to change notification settings - Fork 0
/
gitblame.py
112 lines (100 loc) · 3.29 KB
/
gitblame.py
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
"""
Module to get Git blame from Github's GraphQL API
"""
import logging
import os
from functools import cache
from datetime import datetime
from typing import Self
import requests
from requests import RequestException
from services import debugme, VERSION
from utils import utc_date
_QUERY = """
query($owner: String!, $repositoryName: String!, $branchName: String!, $filePath: String!) {
repositoryOwner(login: $owner) {
repository(name: $repositoryName) {
object(expression: $branchName) {
... on Commit {
blame(path: $filePath) {
ranges {
startingLine
endingLine
commit {
committedDate
oid
author {
name
email
}
}
}
}
}
}
}
}
}
"""
class GitBlame:
"""
Class to get Git blame from Github's GraphQL API
"""
def __init__(self, repo: str, branch: str, access_token: str) -> None:
self.owner, self.repo = repo.split("/", 1)
self.branch = branch
self.api_url = "https://api.github.com/graphql"
self.session = requests.Session()
self.session.headers["Authorization"] = f"Bearer {access_token}"
self.session.headers["User-Agent"] = f"bugme/{VERSION}"
self.timeout = 30
if os.getenv("DEBUG"):
self.session.hooks["response"].append(debugme)
self.blame_file = cache(self._blame_file)
def __enter__(self) -> Self:
return self
def __exit__(self, exc_type, exc_value, traceback) -> None:
try:
self.session.close()
except RequestException:
pass
self.blame_file.cache_clear()
if exc_type is not None:
logging.error("GitBlame: %s: %s: %s", exc_type, exc_value, traceback)
def _blame_file(self, file: str) -> dict | None:
variables = {
"owner": self.owner,
"repositoryName": self.repo,
"branchName": self.branch,
"filePath": file,
}
try:
response = self.session.post(
self.api_url,
timeout=self.timeout,
json={"query": _QUERY, "variables": variables},
)
response.raise_for_status()
data = response.json()["data"]
return data["repositoryOwner"]["repository"]["object"]["blame"]["ranges"]
except RequestException as exc:
logging.error("%s: %s", file, exc)
except KeyError:
logging.error("%s: %s", file, response.text)
return None
def blame_line(self, file: str, line: int) -> tuple[str, str, str, datetime]:
"""
Blame line
"""
blames = self.blame_file(file)
if blames is None:
raise KeyError(f"No blame for {file}")
commit = {}
for blame in blames:
if blame["startingLine"] <= line <= blame["endingLine"]:
commit = blame["commit"]
author, email = commit["author"]["name"], commit["author"]["email"]
# Sometimes these are swapped
if "@" not in email and "@" in author:
author, email = email, author
return author, email, commit["oid"], utc_date(commit["committedDate"])