forked from asottile/git-code-debt
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfile_diff_stat.py
More file actions
136 lines (115 loc) · 4.15 KB
/
Copy pathfile_diff_stat.py
File metadata and controls
136 lines (115 loc) · 4.15 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
from __future__ import annotations
import enum
import os.path
import re
from typing import NamedTuple
Status = enum.Enum('Status', 'ADDED DELETED ALREADY_EXISTING')
SpecialFileType = enum.Enum('SpecialFileType', 'SUBMODULE SYMLINK BINARY')
class SpecialFile(NamedTuple):
file_type: SpecialFileType
added: bytes | None
removed: bytes | None
class FileDiffStat(NamedTuple):
path: bytes
lines_added: list[bytes]
lines_removed: list[bytes]
status: Status
special_file: SpecialFile | None = None
@property
def extension(self) -> bytes:
return os.path.splitext(self.path)[1]
@property
def filename(self) -> bytes:
return os.path.split(self.path)[1]
SUBMODULE_MODE = b'160000'
SYMLINK_MODE = b'120000'
def _to_file_diff_stat(file_diff: bytes) -> FileDiffStat:
lines = file_diff.split(b'\n')
diff_line_filename = lines[0].split()[-1].lstrip(b'b').lstrip(b'/')
is_binary = False
in_diff = False
mode = None
status = None
lines_added = []
lines_removed = []
for line in lines[1:]:
# Mode will be indicated somewhere between diff --git line
# and the file added / removed lines
# It has these forms:
# 1. 'new file mode 100644'
# 2. 'deleted file mode 100644'
# 3. 'index dc7827c..7b8b995 100644'
# 4. 'old mode 100755'
# 'new mode 100644'
if line.startswith(b'new file mode '):
assert status is None
assert mode is None
status = Status.ADDED
mode = line.split()[-1]
elif line.startswith(b'deleted file mode '):
assert status is None
assert mode is None
status = Status.DELETED
mode = line.split()[-1]
elif line.startswith(b'new mode '):
assert status is None
assert mode is None
status = Status.ALREADY_EXISTING
mode = line.split()[-1]
elif line.startswith(b'index') and len(line.split()) == 3:
assert status is None
assert mode is None
status = Status.ALREADY_EXISTING
mode = line.split()[-1]
elif line.startswith(b'Binary files'):
is_binary = True
# A diff contains lines that look like:
# --- foo/bar
# +++ foo/bar
# Which kind of look like diff lines but are definitely not
elif line.startswith(b'--- ') and not in_diff:
pass
elif line.startswith(b'+++ ') and not in_diff:
in_diff = True
elif in_diff and line.startswith(b'+'):
lines_added.append(line[1:])
elif in_diff and line.startswith(b'-'):
lines_removed.append(line[1:])
assert mode is not None
assert status is not None
# Process symlinks and submodules
special_file = None
if mode == SUBMODULE_MODE:
special_file = SpecialFile(
file_type=SpecialFileType.SUBMODULE,
added=lines_added[0].split()[-1] if lines_added else None,
removed=lines_removed[0].split()[-1] if lines_removed else None,
)
lines_added = []
lines_removed = []
elif mode == SYMLINK_MODE:
special_file = SpecialFile(
file_type=SpecialFileType.SYMLINK,
added=lines_added[0] if lines_added else None,
removed=lines_removed[0] if lines_removed else None,
)
lines_added = []
lines_removed = []
elif is_binary:
special_file = SpecialFile(
file_type=SpecialFileType.BINARY,
added=diff_line_filename if status is not Status.DELETED else None,
removed=diff_line_filename if status is not Status.ADDED else None,
)
return FileDiffStat(
diff_line_filename,
lines_added,
lines_removed,
status,
special_file=special_file,
)
GIT_DIFF_RE = re.compile(b'^diff --git', flags=re.MULTILINE)
def get_file_diff_stats_from_output(output: bytes) -> list[FileDiffStat]:
files = GIT_DIFF_RE.split(output)
assert not files[0].strip() or files[0].startswith(b'commit ')
return [_to_file_diff_stat(file_diff) for file_diff in files[1:]]