/
repo.py
192 lines (169 loc) · 8.75 KB
/
repo.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
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# coding: utf-8
import os
import cPickle as pickle
import json
import cmds
import plot
import commitgraph
import dirtree
class Repo(object):
""" git-репозиторий """
@classmethod
def open(_class, path):
""" Возвращает объект Repo для git-репозитория по указанному пути,
загружая ранее сохраненные результаты вычислений
"""
pickled_path = os.path.join(path, '.git', 'blamer.pickled')
if os.path.isfile(pickled_path):
try:
repo = pickle.load(open(pickled_path))
new_head = repo.get_head_sha1()
if new_head != repo.head:
# репозиторий обновился с прошлого раза
repo.head = new_head
repo.read_commits()
except pickle.UnpicklingError:
repo = Repo(path)
else:
repo = Repo(path)
return repo
def save(self):
""" Сохраняет результаты вычислений на диск для ускорения последующих
обращений к этому же репозиторию
"""
pickled_path = os.path.join(self.path, '.git', 'blamer.pickled')
f = open(pickled_path, 'w')
pickle.dump(self, f)
f.close()
def __init__(self, path):
""" Использовать Repo.open вместо прямого вызова конструктора
path: путь к репозиторию
"""
self.path = path
self.commits = {} # sha1 -> Commit
# выясняем sha1 у коммита HEAD
self.head = self.get_head_sha1()
self.commit_order = [] # sha1 коммитов в порядке от самого свежего до самого старого
# первый коммит пока неизвестен
self.first_commit = None
self.read_commits()
def get_head_sha1(self):
""" Reads HEAD's sha1 from file """
head_ref = open(os.path.join(self.path, '.git', 'HEAD')).read().split(': ')[1]
head_ref = head_ref.strip().replace('.', '') # убираем точку в конце
head_ref = head_ref.replace('/', os.sep)
head = open(os.path.join(self.path, '.git', head_ref)).read().replace('.', '').strip()
return head
def blame_stats(self, file_path, rev_sha1):
""" Число строк, принадлежащих каждому автору, в конкретном файле
и ревизии.
Возвращает словарь {имя автора: число строк}
"""
return cmds.blame_stats(self.path, file_path, rev_sha1)
def read_commits(self):
""" Считывает коммиты из git log, заполняя self.commits """
self.commit_order = [] # новые коммиты могли появиться в середине
# после merge с момента предыдущего обращения
# к репозиторию
for commit in cmds.read_commits(self.path):
if commit.sha1 not in self.commits:
self.commits[commit.sha1] = commit
commit.repo = self
self.commit_order.append(commit.sha1)
self.first_commit = commit.sha1
def ignore_file(self, file_path):
""" Возвращает true, если файл с указанным именем не должен учитываться
при подсчете метрик (например, является бинарным)
"""
return not file_path.endswith('.py')
# TODO: считывать конфиг из репозитория
if 'aot_seman' in file_path:
return True
bad_extensions = (".pdf", ) # Git считает pdf текстовыми
for ext in bad_extensions:
if file_path.endswith(ext):
return True
return False
def compute_blame(self):
""" Вычисляет информацию об авторстве для всех файлов и коммитов
в репозитории
"""
# вычисление должно идти от более старых коммитов к более новым,
# т.к. новые используют информацию об авторстве из старых
for i, sha1 in enumerate(self.commit_order[::-1]):
commit = self.commits[sha1]
if commit.snapshot_blame is not None:
continue # пропускаем коммиты, обработанные при прошлой загрузке
# данные об авторстве файлов, измененных в этом коммите
for file_path, _, __ in commit.changes:
if self.ignore_file(file_path):
continue
try:
blame = cmds.blame_stats(self.path, file_path, commit.sha1)
except Exception as e:
if 'no such path' in unicode(e):
blame = None
else:
raise
commit.blames[file_path] = blame
# интеграция данных
commit.compute_snapshot_blame()
print "completed", i, "of", len(self.commit_order)
def get_longest_path(self):
""" Ищет путь наибольшей длины из HEAD в первый коммит
(длина = число коммитов).
Возвращает список SHA1.
"""
# выполним поиск в глубину без отмечания посещенных вершин.
longest_path = []
stack = []
stack.append([self.head, 0]) # текущий коммит и номер родителя,
# к которому перейдем
# max_prefix_len[sha1] = k <=> самый длинный путь в коммит sha1 имеет длину k
max_prefix_len = {}
while len(stack) > 0:
sha1, parent_i = stack[-1]
if sha1 not in max_prefix_len:
# первый раз заходим в этот коммит
max_prefix_len[sha1] = len(stack)-1
elif len(stack)-1 < max_prefix_len[sha1]:
# очевидно, что мы не можем улучшить путь
# ("<=" поставить нельзя из-за возврата от предков)
stack.pop()
continue
else:
# путь в этот коммит удлиннился
max_prefix_len[sha1] = len(stack)-1
commit = self.commits[sha1]
if parent_i >= len(commit.parents):
# перебрали всех предков коммита
if parent_i == 0: # достигли корневого коммита
if len(stack) > len(longest_path):
longest_path = list(stack)
stack.pop()
else:
parent_sha = commit.parents[parent_i]
stack[-1][1] += 1
stack.append([parent_sha, 0])
return [sha1 for sha1, _ in longest_path]
def dump_commit_info_js(self, fileobject, commit_coords):
""" Записывает структуры данных с описанием коммитов на языке JavaScript
в файлоподобный объект fileobject.
"""
f = fileobject
for sha1 in self.commits:
x, y = commit_coords[sha1]
commit = self.commits[sha1]
print >>f, 'Commits.add(%d, %d, {' % (x, y)
print >>f, '\tx: %d, y: %d,' % (x, y)
print >>f, '\tsha1: "%s",' % sha1
print >>f, '\tauthor: "%s",' % commit.author
print >>f, '\tdate: "%s",' % str(commit.date)
print >>f, '\tmessage: "%s",' % commit.message
print >>f, '\tblame: %s,' % json.dumps(commit.snapshot_blame.data)
if len(commit.parents) > 1:
changes = []
else:
changes = [ [la, ld, path] for path, la, ld in commit.changes ]
print >>f, '\tchanges: %s' % json.dumps(changes)
print >>f, '});'