-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathfeature_blame.py
More file actions
178 lines (145 loc) · 5.11 KB
/
feature_blame.py
File metadata and controls
178 lines (145 loc) · 5.11 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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
from pathlib import Path
from typing import Any
import typer
from git import Repo
from git_tool.feature_data.read_feature_data.parse_data import (
get_features_touched_by_commit,
)
from git_tool.feature_data.models_and_context.repo_context import (
repo_context,
)
app = typer.Typer(no_args_is_help=True)
def read_file_lines(file_path: Path) -> list[str]:
"""
Reads the lines of a file and returns them as a list.
"""
with file_path.open("r", encoding="utf-8") as f:
return f.readlines()
def get_line_to_blame_mapping(
repo: Repo, file_path: Path, start_line: int, end_line: int
) -> dict[int, tuple[str, str]]:
"""
Returns a mapping of line numbers to (commit hash, blame line).
"""
blame_output = repo.git.blame(
"-L", f"{start_line},{end_line}", "--date=short", str(file_path)
)
line_to_blame = {}
line_number = start_line
for line in blame_output.splitlines():
if line.startswith(":"):
continue
blame_part = line.split(" ", 1)
short_hash = blame_part[0]
blame_text = blame_part[1] if len(blame_part) > 1 else ""
full_hash = repo.git.rev_parse(short_hash)
line_to_blame[line_number] = (short_hash, blame_text)
line_number += 1
return line_to_blame
def get_commit_to_features_mapping(
line_to_commit: dict[int, tuple[str, str]],
) -> dict[str, str]:
"""
Returns a mapping of commit hashes to features.
"""
unique_commits = {commit for commit, _ in line_to_commit.values()}
commit_to_features = {
commit_id: ", ".join(get_features_touched_by_commit(commit_id))
for commit_id in unique_commits
}
return commit_to_features
def get_line_to_features_mapping(
repo: Repo, file_path: Path, start_line: int, end_line: int
) -> tuple[dict[int, Any], dict[int, tuple[str, str]]]:
"""
Returns a mapping of line numbers to features.
"""
# Get the commit for each line using 'git blame'
line_to_blame = get_line_to_blame_mapping(
repo, file_path, start_line, end_line
)
# for debugging: print("Step 1: ", line_to_blame)
# Get the features for each commit
commit_to_features = get_commit_to_features_mapping(line_to_blame)
# for debugging: print("Step 2: ", commit_to_features)
# Map each line to its corresponding feature
line_to_features = {
line: commit_to_features.get(commit_hash, "UNKNOWN")
for line, (commit_hash, _) in line_to_blame.items()
}
# for debugging: print("Step 3: ", line_to_features)
return line_to_features, line_to_blame
def print_feature_blame_output(
lines: list[str],
mappings: tuple[dict[int, Any], dict[int, tuple[str, str]]],
start_line: int,
end_line: int,
):
"""
Prints the feature blame output similar to git blame.
"""
line_to_features, line_to_blame = mappings
# Get the max width of feature strings for alignment
max_feature_width = max(
(
len(line_to_features.get(commit, "UNKNOWN"))
for commit in line_to_features.values()
),
default=15,
)
for i in range(start_line, end_line + 1):
line = lines[
i - 1
] # Adjust because list is 0-indexed, but line numbers start from 1
commit_hash, blame_text = line_to_blame.get(i)
blame_text = blame_text.replace("(", "", 1)
feature = line_to_features.get(i, "UNKNOWN")
typer.echo(f"{feature:<15} ({commit_hash} {blame_text}")
@app.command(
help="Display features associated with file lines.",
no_args_is_help=True,
name=None,
)
def feature_blame(
filename: str = typer.Argument(
..., help="The file to display feature blame for."
),
line: str = typer.Option(
None, help="Specify a line or range of lines (e.g., '5' or '5-10')."
),
):
"""
Displays features associated with file lines. You can optionally specify a line or a range of lines.
"""
file_path = Path(filename)
if not file_path.exists():
typer.echo(f"Error: file '{filename}' not found.")
raise typer.Exit(code=1)
# Read the file contents
lines = read_file_lines(file_path)
# Default to the entire file if no line argument is provided
start_line = 1
end_line = len(lines)
if line:
if "-" in line:
# Handle a range of lines
start_line, end_line = map(int, line.split("-"))
else:
# Handle a single line
start_line = end_line = int(line)
# Ensure the line range is valid
if start_line < 1 or end_line > len(lines):
typer.echo("Error: Line number out of range.")
raise typer.Exit(code=1)
if start_line > end_line:
typer.echo("Error: Start line must be less than end line.")
raise typer.Exit(code=1)
with repo_context() as repo: # Use repo_context for the git operations
feature_to_line_mapping = get_line_to_features_mapping(
repo, file_path, start_line, end_line
)
print_feature_blame_output(
lines, feature_to_line_mapping, start_line, end_line
)
if __name__ == "__main__":
app()