Skip to content

Commit 03519f9

Browse files
authored
Merge pull request #16 from gh640/feature/add-multiline-blame
Issue #15: add the function to show all blame output.
2 parents 6807492 + 5c14560 commit 03519f9

File tree

3 files changed

+204
-11
lines changed

3 files changed

+204
-11
lines changed

Default.sublime-commands

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
[
2-
{
3-
"caption": "Git Blame",
4-
"command": "blame"
5-
}
2+
{
3+
"caption": "Git Blame",
4+
"command": "blame"
5+
},
6+
{
7+
"caption": "Git Blame Show All",
8+
"command": "blame_show_all"
9+
},
10+
{
11+
"caption": "Git Blame Erase All",
12+
"command": "blame_erase_all"
13+
}
614
]

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,28 @@
22
[![GitHub issues](https://img.shields.io/github/issues/psykzz/st3-gitblame.svg)](https://github.com/psykzz/st3-gitblame/issues)
33

44

5-
# Git blame - Sublime text 3 plugin
5+
# Git blame - Sublime text 3 plugin
66
Sublime text 3 - Git blame the line
77

8-
Small plugin to parse git blame and add a view to show the user and datetime of the blame. You can click the sha to copy it or click the ✖ to close.
8+
Small plugin to parse git blame and add a view to show the user and datetime of the blame. You can click the sha to copy it or click the ✖ to close. Also, this package provides a command to see all the blame result of a file.
99

1010
View on [packagecontrol.io](https://packagecontrol.io/packages/Git%20blame)
1111

1212

1313
## Usage
1414

15+
### Checking the results of `git blame` for selected lines
16+
1517
> CTRL + ALT + B (Q on linux)
16-
18+
1719
> Right click > Git blame
18-
20+
21+
### Checking the result of `git blame` for the whole file
22+
23+
To show the `git blame` result: Open the command pallette and select `Git Blame Show All`.
24+
25+
To erase the `git blame` result: Open the command pallette and select `Git Blame Erase All`. Or, you can click the ✖ icon to erase it. Also, the result is automatically erased when you start to modify the file.
26+
1927
## Example
2028

2129
<img width="645" alt="screen shot 2017-07-20 at 11 12 51" src="https://user-images.githubusercontent.com/2543659/28410198-331b1ec8-6d3d-11e7-9ac1-57d43fb6ab60.png">

git-blame.py

Lines changed: 180 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import sublime
22
import sublime_plugin
33
import os
4+
import re
45
import functools
56
import subprocess
67
from subprocess import check_output as shell
78

8-
stylesheet = '''
9+
PHANTOM_KEY_ALL = 'git-blame-all'
10+
11+
stylesheet_one = '''
912
<style>
1013
div.phantom-arrow {
1114
border-top: 0.4rem solid transparent;
@@ -41,7 +44,7 @@
4144
</style>
4245
'''
4346

44-
template = '''
47+
template_one = '''
4548
<body id="inline-git-blame">
4649
{stylesheet}
4750
<div class="phantom-arrow"></div>
@@ -58,13 +61,51 @@
5861
</body>
5962
'''
6063

64+
stylesheet_all = '''
65+
<style>
66+
div.phantom {
67+
padding: 0;
68+
margin: 0;
69+
background-color: color(var(--bluish) blend(var(--background) 30%));
70+
}
71+
div.phantom .user {
72+
width: 10em;
73+
}
74+
div.phantom a.close {
75+
padding: 0.35rem 0.7rem 0.45rem 0.8rem;
76+
position: relative;
77+
bottom: 0.05rem;
78+
font-weight: bold;
79+
}
80+
html.dark div.phantom a.close {
81+
background-color: #00000018;
82+
}
83+
html.light div.phantom a.close {
84+
background-color: #ffffff18;
85+
}
86+
</style>
87+
'''
88+
89+
template_all = '''
90+
<body id="inline-git-blame">
91+
{stylesheet}
92+
<div class="phantom">
93+
<span class="message">
94+
{sha} (<span class="user">{user}</span> {date} {time})
95+
<a class="close" href="close">''' + chr(0x00D7) + '''</a>
96+
</span>
97+
</div>
98+
</body>
99+
'''
100+
61101
# Sometimes this fails on other OS, just error silently
62102
try:
63103
si = subprocess.STARTUPINFO()
64104
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
65105
except:
66106
si = None
67107

108+
68109
class BlameCommand(sublime_plugin.TextCommand):
69110

70111
def __init__(self, view):
@@ -148,13 +189,149 @@ def run(self, edit):
148189

149190
sha, user, date, time = self.parse_blame(result)
150191

151-
body = template.format(sha=sha, user=user, date=date, time=time, stylesheet=stylesheet)
192+
body = template_one.format(sha=sha, user=user, date=date, time=time, stylesheet=stylesheet_one)
152193

153194
phantom = sublime.Phantom(line, body, sublime.LAYOUT_BLOCK, self.on_phantom_close)
154195
phantoms.append(phantom)
155196
self.phantom_set.update(phantoms)
156197

157198

199+
class BlameShowAllCommand(sublime_plugin.TextCommand):
200+
201+
# The fixed length for author names
202+
NAME_LENGTH = 10
203+
204+
def __init__(self, view):
205+
super().__init__(view)
206+
self.phantom_set = sublime.PhantomSet(self.view, PHANTOM_KEY_ALL)
207+
self.pattern = None
208+
209+
def run(self, edit):
210+
if self.view.is_dirty():
211+
sublime.status_message("The file needs to be saved for git blame.")
212+
return
213+
214+
self.view.erase_phantoms(PHANTOM_KEY_ALL)
215+
216+
blame_lines = self.get_blame_lines(self.view.file_name())
217+
218+
if not blame_lines:
219+
return
220+
221+
phantoms = []
222+
for l in blame_lines:
223+
parsed = self.parse_blame(l)
224+
if not parsed:
225+
continue
226+
227+
sha, author, date, time, line_number = parsed
228+
229+
body = template_all.format(sha=sha,
230+
user=self.format_name(author),
231+
date=date,
232+
time=time,
233+
stylesheet=stylesheet_all)
234+
235+
line_point = self.get_line_point(line_number - 1)
236+
phantom = sublime.Phantom(line_point,
237+
body,
238+
sublime.LAYOUT_INLINE,
239+
self.on_phantom_close)
240+
phantoms.append(phantom)
241+
242+
self.phantom_set.update(phantoms)
243+
244+
def get_blame_lines(self, path):
245+
'''Run `git blame` and get the output lines.
246+
'''
247+
try:
248+
# The option --show-name is necessary to force file name display.
249+
command = ["git", "blame", "--show-name", "--minimal", "-w", path]
250+
output = shell(command,
251+
cwd=os.path.dirname(os.path.realpath(path)),
252+
startupinfo=si,
253+
stderr=subprocess.STDOUT)
254+
return output.decode("UTF-8").splitlines()
255+
except subprocess.CalledProcessError as e:
256+
print("Git blame: git error {}:\n{}".format(e.returncode, e.output.decode("UTF-8")))
257+
except Exception as e:
258+
print("Git blame: Unexpected error:", e)
259+
260+
def parse_blame(self, blame):
261+
'''Parses git blame output.
262+
'''
263+
if not self.pattern:
264+
self.prepare_pattern()
265+
266+
m = self.pattern.match(blame)
267+
if m:
268+
sha = m.group('sha')
269+
# Currently file is not used.
270+
# file = m.group('file')
271+
author = m.group('author')
272+
date = m.group('date')
273+
time = m.group('time')
274+
line_number = int(m.group('line_number'))
275+
return sha, author, date, time, line_number
276+
else:
277+
return None
278+
279+
def prepare_pattern(self):
280+
'''Prepares the regex pattern to parse git blame output.
281+
'''
282+
# The SHA output by git-blame may have a leading caret to indicate
283+
# that it is a "boundary commit".
284+
p_sha = r'(?P<sha>\^?\w+)'
285+
p_file = r'((?P<file>[\S ]+)\s+)'
286+
p_author = r'(?P<author>.+?)'
287+
p_date = r'(?P<date>\d{4}-\d{2}-\d{2})'
288+
p_time = r'(?P<time>\d{2}:\d{2}:\d{2})'
289+
p_timezone = r'(?P<timezone>[\+-]\d+)'
290+
p_line = r'(?P<line_number>\d+)'
291+
s = r'\s+'
292+
293+
self.pattern = re.compile(r'^' + p_sha + s + p_file + r'\(' +
294+
p_author + s + p_date + s + p_time + s +
295+
p_timezone + s + p_line + r'\) ')
296+
297+
def format_name(self, name):
298+
'''Formats author names so that widths of phantoms become equal.
299+
'''
300+
ellipsis = '...'
301+
if len(name) > self.NAME_LENGTH:
302+
return name[:self.NAME_LENGTH] + ellipsis
303+
else:
304+
return name + '.' * (self.NAME_LENGTH - len(name)) + ellipsis
305+
306+
def get_line_point(self, line):
307+
'''Get the point of specified line in a view.
308+
'''
309+
return self.view.line(self.view.text_point(line, 0))
310+
311+
def on_phantom_close(self, href):
312+
'''Closes opened phantoms.
313+
'''
314+
if href == 'close':
315+
self.view.run_command('blame_erase_all')
316+
317+
318+
class BlameEraseAllCommand(sublime_plugin.TextCommand):
319+
320+
def run(self, edit):
321+
'''Erases the blame results.
322+
'''
323+
sublime.status_message("The git blame result is cleared.")
324+
self.view.erase_phantoms(PHANTOM_KEY_ALL)
325+
326+
327+
class BlameEraseAllListener(sublime_plugin.ViewEventListener):
328+
329+
def on_modified(self):
330+
'''Automatically erases the blame results to prevent mismatches.
331+
'''
332+
self.view.run_command('blame_erase_all')
333+
334+
158335
class InsertCommitDescriptionCommand(sublime_plugin.TextCommand):
159336

160337
def run(self, edit, desc):

0 commit comments

Comments
 (0)