forked from kemayo/sublime-text-git
-
Notifications
You must be signed in to change notification settings - Fork 0
/
history.py
235 lines (182 loc) · 8.37 KB
/
history.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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import sublime_plugin
import functools
import re
import sublime
from .git import GitTextCommand, GitWindowCommand, plugin_file
class GitBlameCommand(GitTextCommand):
def run(self, edit):
# somewhat custom blame command:
# -w: ignore whitespace changes
# -M: retain blame when moving lines
# -C: retain blame when copying lines between files
command = ['git', 'blame', '-w', '-M', '-C']
lines = self.get_lines()
if lines:
command.extend(('-L', str(lines[0]) + ',' + str(lines[1])))
callback = self.blame_done
else:
callback = functools.partial(self.blame_done,
position=self.view.viewport_position())
command.append(self.get_file_name())
self.run_command(command, callback)
def get_lines(self):
selection = self.view.sel()[0] # todo: multi-select support?
if selection.empty():
return False
# just the lines we have a selection on
begin_line, begin_column = self.view.rowcol(selection.begin())
end_line, end_column = self.view.rowcol(selection.end())
# blame will fail if last line is empty and is included in the selection
if end_line > begin_line and end_column == 0:
end_line -= 1
# add one to each, to line up sublime's index with git's
return begin_line + 1, end_line + 1
def blame_done(self, result, position=None):
self.scratch(result, title="Git Blame", position=position,
syntax=plugin_file("syntax/Git Blame.tmLanguage"))
class GitLog(object):
def run(self, edit=None):
fn = self.get_file_name()
return self.run_log(fn != '', '--', fn)
def run_log(self, follow, *args):
# the ASCII bell (\a) is just a convenient character I'm pretty sure
# won't ever come up in the subject of the commit (and if it does then
# you positively deserve broken output...)
# 9000 is a pretty arbitrarily chosen limit; picked entirely because
# it's about the size of the largest repo I've tested this on... and
# there's a definite hiccup when it's loading that
command = ['git', 'log', '--no-color', '--pretty=%s (%h)\a%an <%aE>\a%ad (%ar)',
'--date=local', '--max-count=9000', '--follow' if follow else None]
command.extend(args)
self.run_command(
command,
self.log_done)
def log_done(self, result):
self.results = [r.split('\a', 2) for r in result.strip().split('\n')]
self.quick_panel(self.results, self.log_panel_done)
def log_panel_done(self, picked):
if 0 > picked < len(self.results):
return
item = self.results[picked]
# the commit hash is the last thing on the first line, in brackets
ref = item[0].split(' ')[-1].strip('()')
self.log_result(ref)
def log_result(self, ref):
# I'm not certain I should have the file name here; it restricts the
# details to just the current file. Depends on what the user expects...
# which I'm not sure of.
self.run_command(
['git', 'log', '--no-color', '-p', '-1', ref, '--', self.get_file_name()],
self.details_done)
def details_done(self, result):
self.scratch(result, title="Git Commit Details", syntax=plugin_file("syntax/Git Commit Message.tmLanguage"))
class GitLogCommand(GitLog, GitTextCommand):
pass
class GitLogAllCommand(GitLog, GitWindowCommand):
pass
class GitShow(object):
def run(self, edit=None):
# GitLog Copy-Past
self.run_command(
['git', 'log', '--no-color', '--pretty=%s (%h)\a%an <%aE>\a%ad (%ar)',
'--date=local', '--max-count=9000', '--', self.get_file_name()],
self.show_done)
def show_done(self, result):
# GitLog Copy-Past
self.results = [r.split('\a', 2) for r in result.strip().split('\n')]
self.quick_panel(self.results, self.panel_done)
def panel_done(self, picked):
if 0 > picked < len(self.results):
return
item = self.results[picked]
# the commit hash is the last thing on the first line, in brackets
ref = item[0].split(' ')[-1].strip('()')
self.run_command(
['git', 'show', '%s:%s' % (ref, self.get_relative_file_name())],
self.details_done,
ref=ref)
def details_done(self, result, ref):
syntax = self.view.settings().get('syntax')
self.scratch(result, title="%s:%s" % (ref, self.get_file_name()), syntax=syntax)
class GitShowCommand(GitShow, GitTextCommand):
pass
class GitShowAllCommand(GitShow, GitWindowCommand):
pass
class GitGraph(object):
def run(self, edit=None):
filename = self.get_file_name()
self.run_command(
['git', 'log', '--graph', '--pretty=%h -%d (%cr) (%ci) <%an> %s', '--abbrev-commit', '--no-color', '--decorate', '--date=relative', '--follow' if filename else None, '--', filename],
self.log_done
)
def log_done(self, result):
self.scratch(result, title="Git Log Graph", syntax=plugin_file("syntax/Git Graph.tmLanguage"))
class GitGraphCommand(GitGraph, GitTextCommand):
pass
class GitGraphAllCommand(GitGraph, GitWindowCommand):
pass
class GitOpenFileCommand(GitLog, GitWindowCommand):
def run(self):
self.run_command(['git', 'branch', '-a', '--no-color'], self.branch_done)
def branch_done(self, result):
self.results = result.rstrip().split('\n')
self.quick_panel(self.results, self.branch_panel_done,
sublime.MONOSPACE_FONT)
def branch_panel_done(self, picked):
if 0 > picked < len(self.results):
return
self.branch = self.results[picked].split(' ')[-1]
self.run_log(False, self.branch)
def log_result(self, result_hash):
self.ref = result_hash
self.run_command(
['git', 'ls-tree', '-r', '--full-tree', self.ref],
self.ls_done)
def ls_done(self, result):
# Last two items are the ref and the file name
# p.s. has to be a list of lists; tuples cause errors later
self.results = [[match.group(2), match.group(1)] for match in re.finditer(r"\S+\s(\S+)\t(.+)", result)]
self.quick_panel(self.results, self.ls_panel_done)
def ls_panel_done(self, picked):
if 0 > picked < len(self.results):
return
item = self.results[picked]
self.filename = item[0]
self.fileRef = item[1]
self.run_command(
['git', 'show', self.fileRef],
self.show_done)
def show_done(self, result):
self.scratch(result, title="%s:%s" % (self.fileRef, self.filename))
class GitDocumentCommand(GitBlameCommand):
def get_lines(self):
selection = self.view.sel()[0] # todo: multi-select support?
# just the lines we have a selection on
begin_line, begin_column = self.view.rowcol(selection.begin())
end_line, end_column = self.view.rowcol(selection.end())
# blame will fail if last line is empty and is included in the selection
if end_line > begin_line and end_column == 0:
end_line -= 1
# add one to each, to line up sublime's index with git's
return begin_line + 1, end_line + 1
def blame_done(self, result, position=None):
shas = set((sha for sha in re.findall(r'^[0-9a-f]+', result, re.MULTILINE) if not re.match(r'^0+$', sha)))
command = ['git', 'show', '-s', '-z', '--no-color', '--date=iso']
command.extend(shas)
self.run_command(command, self.show_done)
def show_done(self, result):
commits = []
for commit in result.split('\0'):
match = re.search(r'^Date:\s+(.+)$', commit, re.MULTILINE)
if match:
commits.append((match.group(1), commit))
commits.sort(reverse=True)
commits = [commit for d, commit in commits]
self.scratch('\n\n'.join(commits), title="Git Commit Documentation")
class GitGotoBlame(sublime_plugin.TextCommand):
def run(self, edit):
line = self.view.substr(self.view.line(self.view.sel()[0].a))
commit = line.split(" ")[0]
if not commit or commit == "00000000":
return
self.view.window().run_command("git_raw", {"command": "git show %s" % commit, "show_in": "new_tab", "may_change_files": False})