Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions Default.sublime-commands
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
[
{
"caption": "Git Blame",
"command": "blame"
}
{
"caption": "Git Blame",
"command": "blame"
},
{
"caption": "Git Blame Show All",
"command": "blame_show_all"
},
{
"caption": "Git Blame Erase All",
"command": "blame_erase_all"
}
]
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,28 @@
[![GitHub issues](https://img.shields.io/github/issues/psykzz/st3-gitblame.svg)](https://github.com/psykzz/st3-gitblame/issues)


# Git blame - Sublime text 3 plugin
# Git blame - Sublime text 3 plugin
Sublime text 3 - Git blame the line

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.
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.

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


## Usage

### Checking the results of `git blame` for selected lines

> CTRL + ALT + B (Q on linux)

> Right click > Git blame


### Checking the result of `git blame` for the whole file

To show the `git blame` result: Open the command pallette and select `Git Blame Show All`.

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.

## Example

<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">
Expand Down
183 changes: 180 additions & 3 deletions git-blame.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import sublime
import sublime_plugin
import os
import re
import functools
import subprocess
from subprocess import check_output as shell

stylesheet = '''
PHANTOM_KEY_ALL = 'git-blame-all'

stylesheet_one = '''
<style>
div.phantom-arrow {
border-top: 0.4rem solid transparent;
Expand Down Expand Up @@ -41,7 +44,7 @@
</style>
'''

template = '''
template_one = '''
<body id="inline-git-blame">
{stylesheet}
<div class="phantom-arrow"></div>
Expand All @@ -58,13 +61,51 @@
</body>
'''

stylesheet_all = '''
<style>
div.phantom {
padding: 0;
margin: 0;
background-color: color(var(--bluish) blend(var(--background) 30%));
}
div.phantom .user {
width: 10em;
}
div.phantom a.close {
padding: 0.35rem 0.7rem 0.45rem 0.8rem;
position: relative;
bottom: 0.05rem;
font-weight: bold;
}
html.dark div.phantom a.close {
background-color: #00000018;
}
html.light div.phantom a.close {
background-color: #ffffff18;
}
</style>
'''

template_all = '''
<body id="inline-git-blame">
{stylesheet}
<div class="phantom">
<span class="message">
{sha} (<span class="user">{user}</span> {date} {time})
<a class="close" href="close">''' + chr(0x00D7) + '''</a>
</span>
</div>
</body>
'''

# Sometimes this fails on other OS, just error silently
try:
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
except:
si = None


class BlameCommand(sublime_plugin.TextCommand):

def __init__(self, view):
Expand Down Expand Up @@ -148,13 +189,149 @@ def run(self, edit):

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

body = template.format(sha=sha, user=user, date=date, time=time, stylesheet=stylesheet)
body = template_one.format(sha=sha, user=user, date=date, time=time, stylesheet=stylesheet_one)

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


class BlameShowAllCommand(sublime_plugin.TextCommand):

# The fixed length for author names
NAME_LENGTH = 10

def __init__(self, view):
super().__init__(view)
self.phantom_set = sublime.PhantomSet(self.view, PHANTOM_KEY_ALL)
self.pattern = None

def run(self, edit):
if self.view.is_dirty():
sublime.status_message("The file needs to be saved for git blame.")
return

self.view.erase_phantoms(PHANTOM_KEY_ALL)

blame_lines = self.get_blame_lines(self.view.file_name())

if not blame_lines:
return

phantoms = []
for l in blame_lines:
parsed = self.parse_blame(l)
if not parsed:
continue

sha, author, date, time, line_number = parsed

body = template_all.format(sha=sha,
user=self.format_name(author),
date=date,
time=time,
stylesheet=stylesheet_all)

line_point = self.get_line_point(line_number - 1)
phantom = sublime.Phantom(line_point,
body,
sublime.LAYOUT_INLINE,
self.on_phantom_close)
phantoms.append(phantom)

self.phantom_set.update(phantoms)

def get_blame_lines(self, path):
'''Run `git blame` and get the output lines.
'''
try:
# The option --show-name is necessary to force file name display.
command = ["git", "blame", "--show-name", "--minimal", "-w", path]
output = shell(command,
cwd=os.path.dirname(os.path.realpath(path)),
startupinfo=si,
stderr=subprocess.STDOUT)
return output.decode("UTF-8").splitlines()
except subprocess.CalledProcessError as e:
print("Git blame: git error {}:\n{}".format(e.returncode, e.output.decode("UTF-8")))
except Exception as e:
print("Git blame: Unexpected error:", e)

def parse_blame(self, blame):
'''Parses git blame output.
'''
if not self.pattern:
self.prepare_pattern()

m = self.pattern.match(blame)
if m:
sha = m.group('sha')
# Currently file is not used.
# file = m.group('file')
author = m.group('author')
date = m.group('date')
time = m.group('time')
line_number = int(m.group('line_number'))
return sha, author, date, time, line_number
else:
return None

def prepare_pattern(self):
'''Prepares the regex pattern to parse git blame output.
'''
# The SHA output by git-blame may have a leading caret to indicate
# that it is a "boundary commit".
p_sha = r'(?P<sha>\^?\w+)'
p_file = r'((?P<file>[\S ]+)\s+)'
p_author = r'(?P<author>.+?)'
p_date = r'(?P<date>\d{4}-\d{2}-\d{2})'
p_time = r'(?P<time>\d{2}:\d{2}:\d{2})'
p_timezone = r'(?P<timezone>[\+-]\d+)'
p_line = r'(?P<line_number>\d+)'
s = r'\s+'

self.pattern = re.compile(r'^' + p_sha + s + p_file + r'\(' +
p_author + s + p_date + s + p_time + s +
p_timezone + s + p_line + r'\) ')

def format_name(self, name):
'''Formats author names so that widths of phantoms become equal.
'''
ellipsis = '...'
if len(name) > self.NAME_LENGTH:
return name[:self.NAME_LENGTH] + ellipsis
else:
return name + '.' * (self.NAME_LENGTH - len(name)) + ellipsis

def get_line_point(self, line):
'''Get the point of specified line in a view.
'''
return self.view.line(self.view.text_point(line, 0))

def on_phantom_close(self, href):
'''Closes opened phantoms.
'''
if href == 'close':
self.view.run_command('blame_erase_all')


class BlameEraseAllCommand(sublime_plugin.TextCommand):

def run(self, edit):
'''Erases the blame results.
'''
sublime.status_message("The git blame result is cleared.")
self.view.erase_phantoms(PHANTOM_KEY_ALL)


class BlameEraseAllListener(sublime_plugin.ViewEventListener):

def on_modified(self):
'''Automatically erases the blame results to prevent mismatches.
'''
self.view.run_command('blame_erase_all')


class InsertCommitDescriptionCommand(sublime_plugin.TextCommand):

def run(self, edit, desc):
Expand Down