Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Differentiate between staged and unstaged changes #136

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
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
125 changes: 110 additions & 15 deletions git_gutter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class GitGutterCommand(sublime_plugin.WindowCommand):
'deleted_dual', 'inserted', 'changed',
'untracked', 'ignored']

qualifiers = ['staged','unstaged','staged_unstaged']

def run(self, force_refresh=False):
self.view = self.window.active_view()
if not self.view:
Expand All @@ -37,18 +39,94 @@ def run(self, force_refresh=False):
elif ViewCollection.ignored(self.view):
self.bind_files('ignored')
else:
# If the file is untracked there is no need to execute the diff
# update
if force_refresh:
ViewCollection.clear_git_time(self.view)
inserted, modified, deleted = ViewCollection.diff(self.view)
self.lines_removed(deleted)
self.bind_icons('inserted', inserted)
self.bind_icons('changed', modified)
ViewCollection.clear_times(self.view)

staged = ViewCollection.has_stages(self.view)
if staged:
# Mark changes qualified with staged/unstaged/staged_unstaged

# Get unstaged changes
u_inserted, u_modified, u_deleted = self.unstaged_changes()

# Get staged changes
s_inserted, s_modified, s_deleted = self.staged_changes()

# Find lines with a mix of staged/unstaged
m_modified = self.mixed_mofified(u_modified,
[s_inserted, s_modified, s_deleted])
m_inserted = self.mixed_mofified(u_inserted,
[s_inserted, s_modified, s_deleted])
m_deleted = self.mixed_mofified(u_deleted,
[s_inserted, s_modified, s_deleted])

m_all = m_inserted + m_modified + m_deleted

# Remove mixed from unstaged
u_inserted = self.list_subtract(u_inserted, m_all)
u_modified = self.list_subtract(u_modified, m_all)
u_deleted = self.list_subtract(u_deleted, m_all)

# Remove mixed from staged
s_inserted = self.list_subtract(s_inserted, m_all)
s_modified = self.list_subtract(s_modified, m_all)
s_deleted = self.list_subtract(s_deleted, m_all)

# Unstaged
self.lines_removed(u_deleted, 'unstaged')
self.bind_icons('inserted', u_inserted, 'unstaged')
self.bind_icons('changed', u_modified, 'unstaged')

# Staged
self.lines_removed(s_deleted, 'staged')
self.bind_icons('inserted', s_inserted, 'staged')
self.bind_icons('changed', s_modified, 'staged')

# Mixed
self.lines_removed(m_deleted, 'staged_unstaged')
self.bind_icons('inserted', m_inserted, 'staged_unstaged')
self.bind_icons('changed', m_modified, 'staged_unstaged')
else:
# Mark changes without a qualifier
inserted, modified, deleted = self.all_changes()

self.lines_removed(deleted)
self.bind_icons('inserted', inserted)
self.bind_icons('changed', modified)

def all_changes(self):
return ViewCollection.diff(self.view)

def unstaged_changes(self):
return ViewCollection.unstaged(self.view)

def staged_changes(self):
return ViewCollection.staged(self.view)

def list_subtract(self, a, b):
subtracted = [elem for elem in a if elem not in b]
return subtracted

def list_intersection(self, a, b):
intersected = [elem for elem in a if elem in b]
return intersected

def mixed_mofified(self, a, lists):
# a is a list of modified lines
# lists is a list of lists (inserted, modified, deleted)
# We want the values in a that are in any of the lists
c = []
for b in lists:
mix = self.list_intersection(a,b)
[c.append(elem) for elem in mix]
return c

def clear_all(self):
for region_name in self.region_names:
self.view.erase_regions('git_gutter_%s' % region_name)
for qualifier in self.qualifiers:
self.view.erase_regions('git_gutter_%s_%s' % (
region_name, qualifier))

def lines_to_regions(self, lines):
regions = []
Expand All @@ -58,7 +136,7 @@ def lines_to_regions(self, lines):
regions.append(region)
return regions

def lines_removed(self, lines):
def lines_removed(self, lines, qualifier=None):
top_lines = lines
bottom_lines = [line - 1 for line in lines if line > 1]
dual_lines = []
Expand All @@ -69,9 +147,9 @@ def lines_removed(self, lines):
bottom_lines.remove(line)
top_lines.remove(line)

self.bind_icons('deleted_top', top_lines)
self.bind_icons('deleted_bottom', bottom_lines)
self.bind_icons('deleted_dual', dual_lines)
self.bind_icons('deleted_top', top_lines, qualifier)
self.bind_icons('deleted_bottom', bottom_lines, qualifier)
self.bind_icons('deleted_dual', dual_lines, qualifier)

def icon_path(self, icon_name):
if icon_name in ['deleted_top','deleted_bottom','deleted_dual']:
Expand All @@ -87,14 +165,31 @@ def icon_path(self, icon_name):

return path + '/GitGutter/icons/' + icon_name + extn

def bind_icons(self, event, lines):
def icon_scope(self, event_scope, qualifier):
scope = 'markup.%s.git_gutter' % event_scope
if qualifier:
scope += "." + qualifier
return scope

def icon_region(self, event, qualifier):
region = 'git_gutter_%s' % event
if qualifier:
region += "_" + qualifier
return region

def bind_icons(self, event, lines, qualifier=None):
regions = self.lines_to_regions(lines)
event_scope = event
if event.startswith('deleted'):
event_scope = 'deleted'
scope = 'markup.%s.git_gutter' % event_scope
icon = self.icon_path(event)
self.view.add_regions('git_gutter_%s' % event, regions, scope, icon)
if qualifier == "staged_unstaged":
icon_name = "staged_unstaged"
else:
icon_name = event
scope = self.icon_scope(event_scope,qualifier)
icon = self.icon_path(icon_name)
key = self.icon_region(event,qualifier)
self.view.add_regions(key, regions, scope, icon)

def bind_files(self, event):
lines = []
Expand Down
7 changes: 3 additions & 4 deletions git_gutter_compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
class GitGutterCompareCommit(sublime_plugin.WindowCommand):
def run(self):
self.view = self.window.active_view()
key = ViewCollection.get_key(self.view)
self.handler = ViewCollection.views[key]
self.handler = ViewCollection.get_handler(self.view)

self.results = self.commit_list()
if self.results:
Expand All @@ -30,7 +29,7 @@ def on_select(self, selected):
item = self.results[selected]
commit = self.item_to_commit(item)
ViewCollection.set_compare(commit)
ViewCollection.clear_git_time(self.view)
ViewCollection.clear_times(self.view)
ViewCollection.add(self.view)

class GitGutterCompareBranch(GitGutterCompareCommit):
Expand Down Expand Up @@ -66,7 +65,7 @@ class GitGutterCompareHead(sublime_plugin.WindowCommand):
def run(self):
self.view = self.window.active_view()
ViewCollection.set_compare("HEAD")
ViewCollection.clear_git_time(self.view)
ViewCollection.clear_times(self.view)
ViewCollection.add(self.view)

class GitGutterShowCompare(sublime_plugin.WindowCommand):
Expand Down
118 changes: 111 additions & 7 deletions git_gutter_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def __init__(self, view):
self.view = view
self.git_temp_file = ViewCollection.git_tmp_file(self.view)
self.buf_temp_file = ViewCollection.buf_tmp_file(self.view)
self.stg_temp_file = ViewCollection.stg_tmp_file(self.view)
if self.on_disk():
self.git_tree = git_helper.git_tree(self.view)
self.git_dir = git_helper.git_dir(self.git_tree)
Expand Down Expand Up @@ -71,6 +72,32 @@ def update_buf_file(self):
f.write(contents)
f.close()

def update_stg_file(self):
# FIXME dry up duplicate in update_stg_file and update_git_file

# the git repo won't change that often
# so we can easily wait 5 seconds
# between updates for performance
if ViewCollection.stg_time(self.view) > 5:
open(self.stg_temp_file.name, 'w').close()
args = [
self.git_binary_path,
'--git-dir=' + self.git_dir,
'--work-tree=' + self.git_tree,
'show',
':' + self.git_path,
]
try:
contents = self.run_command(args)
contents = contents.replace(b'\r\n', b'\n')
contents = contents.replace(b'\r', b'\n')
f = open(self.stg_temp_file.name, 'wb')
f.write(contents)
f.close()
ViewCollection.update_stg_time(self.view)
except Exception:
pass

def update_git_file(self):
# the git repo won't change that often
# so we can easily wait 5 seconds
Expand Down Expand Up @@ -117,26 +144,32 @@ def process_diff(self, diff_str):
inserted = []
modified = []
deleted = []
adj_map = {0:0}
hunk_re = '^@@ \-(\d+),?(\d*) \+(\d+),?(\d*) @@'
hunks = re.finditer(hunk_re, diff_str, re.MULTILINE)
for hunk in hunks:
start = int(hunk.group(3))
old_start = int(hunk.group(1))
new_start = int(hunk.group(3))
old_size = int(hunk.group(2) or 1)
new_size = int(hunk.group(4) or 1)
if not old_size:
inserted += range(start, start + new_size)
inserted += range(new_start, new_start + new_size)
elif not new_size:
deleted += [start + 1]
deleted += [new_start + 1]
else:
modified += range(start, start + new_size)
modified += range(new_start, new_start + new_size)
# Add values to adjustment map
k = old_start + sum(adj_map.values())
v = new_size - old_size
adj_map[k] = v
if len(inserted) == self.total_lines() and not self.show_untracked:
# All lines are "inserted"
# this means this file is either:
# - New and not being tracked *yet*
# - Or it is a *gitignored* file
return ([], [], [])
return ([], [], [], {0:0})
else:
return (inserted, modified, deleted)
return (inserted, modified, deleted, adj_map)

def diff(self):
if self.on_disk() and self.git_path:
Expand All @@ -156,10 +189,70 @@ def diff(self):
decoded_results = results.decode(encoding.replace(' ', ''))
except UnicodeError:
decoded_results = results.decode("utf-8")
return self.process_diff(decoded_results)
return self.process_diff(decoded_results)[:3]
else:
return ([], [], [])

# FIXME
# Refactor staged/diff methods to dry up duplicated code
def unstaged(self):
if self.on_disk() and self.git_path:
self.update_stg_file()
self.update_buf_file()
args = [
self.git_binary_path, 'diff', '-U0', '--no-color',
self.stg_temp_file.name,
self.buf_temp_file.name
]
args = list(filter(None, args)) # Remove empty args
results = self.run_command(args)
encoding = self._get_view_encoding()
try:
decoded_results = results.decode(encoding.replace(' ', ''))
except UnicodeError:
decoded_results = results.decode("utf-8")
processed = self.process_diff(decoded_results)
ViewCollection.set_line_adjustment_map(self.view,processed[3])
return processed[:3]
else:
return ([], [], [])

def staged(self):
if self.on_disk() and self.git_path:
self.update_stg_file()
self.update_buf_file()
args = [
self.git_binary_path, 'diff', '-U0', '--no-color', '--staged',
ViewCollection.get_compare(),
self.git_path
]
args = list(filter(None, args)) # Remove empty args
results = self.run_command(args)
encoding = self._get_view_encoding()
try:
decoded_results = results.decode(encoding.replace(' ', ''))
except UnicodeError:
decoded_results = results.decode("utf-8")
diffs = self.process_diff(decoded_results)[:3]
return self.apply_line_adjustments(*diffs)
else:
return ([], [], [])

def apply_line_adjustments(self, inserted, modified, deleted):
adj_map = ViewCollection.get_line_adjustment_map(self.view)
i = inserted
m = modified
d = deleted
for k in sorted(adj_map.keys()):
at_line = k
lines_added = adj_map[k]
# `lines_added` lines were added at line `at_line`
# Each line in the diffs that are above `at_line` add `lines_added`
i = [l + lines_added if l > at_line else l for l in i]
m = [l + lines_added if l > at_line else l for l in m]
d = [l + lines_added if l > at_line else l for l in d]
return (i,m,d)

def untracked(self):
return self.handle_files([])

Expand Down Expand Up @@ -187,6 +280,17 @@ def handle_files(self, additionnal_args):
else:
return False

def has_stages(self):
args = [self.git_binary_path,
'--git-dir=' + self.git_dir,
'--work-tree=' + self.git_tree,
'diff', '--staged']
results = self.run_command(args)
if len(results):
return True
else:
return False

def git_commits(self):
args = [
self.git_binary_path,
Expand Down
Binary file added icons/staged_unstaged.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading