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

Fix: Run all git commands from within working tree #365

Merged
merged 1 commit into from
Feb 12, 2017
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
18 changes: 12 additions & 6 deletions git_gutter.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def __init__(self, *args, **kwargs):
TextCommand.__init__(self, *args, **kwargs)
self.git_handler = GitGutterHandler(self.view)
self.show_diff_handler = GitGutterShowDiff(self.view, self.git_handler)
# Last enabled state for change detection
self._enabled = False

def is_enabled(self, **kwargs):
"""Determine if `git_gutter` command is _enabled to execute."""
Expand All @@ -50,14 +52,18 @@ def is_enabled(self, **kwargs):
# Don't handle binary files
elif view.encoding() in ('Hexadecimal'):
valid = False
# Don't handle views without valid file
elif not self.git_handler.on_disk():
valid = False
# Don't handle files outside a repository
elif not self.git_handler.git_dir:
elif not self.git_handler.work_tree(validate=True):
valid = False
# Save state for use in other modules
view.settings().set('git_gutter_enabled', valid)
# Handle changed state
if valid != self._enabled:
# File moved out of work-tree or repository gone
if not valid:
self.show_diff_handler.clear()
# Save state for use in other modules
view.settings().set('git_gutter_enabled', valid)
# Save state for internal use
self._enabled = valid
return valid

def run(self, edit, **kwargs):
Expand Down
146 changes: 86 additions & 60 deletions git_gutter_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@
import sublime

try:
from . import git_helper
from .git_gutter_settings import settings
from .promise import Promise
except (ImportError, ValueError):
import git_helper
from git_gutter_settings import settings
from promise import Promise

Expand All @@ -37,9 +35,12 @@ def __init__(self, view):
self.git_temp_file = None
self.buf_temp_file = None

self.git_tree = None
self.git_dir = None
self.git_path = None
# cached view file name to detect renames
self._view_file_name = None
# real path to current work tree
self._git_tree = None
# relative file path in work tree
self._git_path = None
self.git_tracked = False

self._last_refresh_time_git_file = 0
Expand All @@ -61,6 +62,67 @@ def tmp_file():
os.close(fd)
return filepath

@property
def repository_name(self):
"""Return the folder name of the working tree as repository name."""
return os.path.basename(
self._git_tree) if self._git_tree else '(None)'

def work_tree(self, validate=False):
"""Return the real path of a valid work-tree or None.

Arguments:
validate (bool): If True check whether the file is part of a valid
git repository or return the cached working tree
path only on False.
"""
def is_work_tree(path):
"""Return True if `path` contains a `.git` directory or file."""
return path and os.path.exists(os.path.join(path, '.git'))

def split_work_tree(file_path):
"""Split the `file_path` into working tree and relative path.

The file_path is converted to a absolute real path and split into
the working tree part and relative path part.

Note:
This is a local alternitive to calling the git command:

git rev-parse --show-toplevel

Arguments:
file_path (string): full path to a file.

Returns:
A tuble of two the elements (working tree, file path).
"""
if file_path:
path, name = os.path.split(os.path.realpath(file_path))
# files within '.git' path are not part of a work tree
while path and name and name != '.git':
if is_work_tree(path):
return (path, os.path.relpath(
file_path, path).replace('\\', '/'))
path, name = os.path.split(path)
return (None, None)

if validate:
# Check if file exists
file_name = self.view.file_name()
if not file_name or not os.path.isfile(file_name):
self._view_file_name = None
self._git_tree = None
self._git_path = None
return None
# Check if file was renamed
is_renamed = file_name != self._view_file_name
if is_renamed or not is_work_tree(self._git_tree):
self._view_file_name = file_name
self._git_tree, self._git_path = split_work_tree(file_name)
self.clear_git_time()
return self._git_tree

def git_time_cleared(self):
return self._last_refresh_time_git_file == 0

Expand All @@ -75,7 +137,7 @@ def git_time(self):

def get_compare_against(self):
"""Return the branch/commit/tag string the view is compared to."""
return settings.get_compare_against(self.git_dir, self.view)
return settings.get_compare_against(self._git_tree, self.view)

def set_compare_against(self, commit, refresh=False):
"""Apply a new branch/commit/tag string the view is compared to.
Expand All @@ -91,7 +153,7 @@ def set_compare_against(self, commit, refresh=False):
git show-ref
refresh - always call git_gutter command
"""
settings.set_compare_against(self.git_dir, commit)
settings.set_compare_against(self._git_tree, commit)
self.clear_git_time()
if refresh or not any(settings.get(key, True) for key in (
'focus_change_mode', 'live_mode')):
Expand Down Expand Up @@ -136,17 +198,6 @@ def in_repo(self):
"""
return self.git_tracked

def on_disk(self):
"""Determine, if the view is saved to disk."""
file_name = self.view.file_name()
on_disk = file_name is not None and os.path.isfile(file_name)
if on_disk:
self.git_tree = self.git_tree or git_helper.git_tree(self.view)
self.git_dir = self.git_dir or git_helper.git_dir(self.git_tree)
self.git_path = self.git_path or git_helper.git_file_path(
self.view, self.git_tree)
return on_disk

def update_buf_file(self):
"""Write view's content to temporary file as source for git diff."""
# Read from view buffer
Expand Down Expand Up @@ -183,20 +234,12 @@ def write_file(contents):

args = [
settings.git_binary_path,
'--git-dir=' + self.git_dir,
'--work-tree=' + self.git_tree,
'show',
'%s:%s' % (
self.get_compare_against(),
self.git_path),
self._git_path),
]

try:
self.update_git_time()
return GitGutterHandler.run_command(
args=args, decode=False).then(write_file)
except Exception:
pass
return self.run_command(args=args, decode=False).then(write_file)
return Promise.resolve()

def process_diff(self, diff_str):
Expand Down Expand Up @@ -261,13 +304,8 @@ def run_diff(_unused):
self.buf_temp_file,
]
args = list(filter(None, args)) # Remove empty args
return GitGutterHandler.run_command(
args=args, decode=False).then(decode_diff)

if self.on_disk() and self.git_path:
return self.update_git_file().then(run_diff)
else:
return Promise.resolve("")
return self.run_command(args=args, decode=False).then(decode_diff)
return self.update_git_file().then(run_diff)

def process_diff_line_change(self, line_nr, diff_str):
hunk_re = '^@@ \-(\d+),?(\d*) \+(\d+),?(\d*) @@'
Expand Down Expand Up @@ -368,7 +406,7 @@ def ignored(self):

def handle_files(self, additional_args):
"""Run git ls-files to check for untracked or ignored file."""
if self.on_disk() and self.git_path:
if self._git_tree:
def is_nonempty(results):
"""Determine if view's file is in git's index.

Expand All @@ -379,14 +417,12 @@ def is_nonempty(results):

args = [
settings.git_binary_path,
'--git-dir=' + self.git_dir,
'--work-tree=' + self.git_tree,
'ls-files', '--other', '--exclude-standard',
] + additional_args + [
os.path.join(self.git_tree, self.git_path),
os.path.join(self._git_tree, self._git_path),
]
args = list(filter(None, args)) # Remove empty args
return GitGutterHandler.run_command(args).then(is_nonempty)
return self.run_command(args).then(is_nonempty)
return Promise.resolve(False)

def git_commits(self):
Expand All @@ -399,13 +435,11 @@ def git_commits(self):
"""
args = [
settings.git_binary_path,
'--git-dir=' + self.git_dir,
'--work-tree=' + self.git_tree,
'log', '--all',
'--pretty=%h %s\a%an <%aE>\a%ad (%ar)',
'--date=local', '--max-count=9000'
]
return GitGutterHandler.run_command(args)
return self.run_command(args)

def git_file_commits(self):
r"""Query all commits with changes to the attached file.
Expand All @@ -418,51 +452,42 @@ def git_file_commits(self):
"""
args = [
settings.git_binary_path,
'--git-dir=' + self.git_dir,
'--work-tree=' + self.git_tree,
'log',
'--pretty=%at\a%h %s\a%an <%aE>\a%ad (%ar)',
'--date=local', '--max-count=9000',
'--', self.git_path
'--', self._git_path
]
return GitGutterHandler.run_command(args)
return self.run_command(args)

def git_branches(self):
args = [
settings.git_binary_path,
'--git-dir=' + self.git_dir,
'--work-tree=' + self.git_tree,
'for-each-ref',
'--sort=-committerdate',
'--format=%(subject)\a%(refname)\a%(objectname)',
'refs/heads/'
]
return GitGutterHandler.run_command(args)
return self.run_command(args)

def git_tags(self):
args = [
settings.git_binary_path,
'--git-dir=' + self.git_dir,
'--work-tree=' + self.git_tree,
'show-ref',
'--tags',
'--abbrev=7'
]
return GitGutterHandler.run_command(args)
return self.run_command(args)

def git_current_branch(self):
args = [
settings.git_binary_path,
'--git-dir=' + self.git_dir,
'--work-tree=' + self.git_tree,
'rev-parse',
'--abbrev-ref',
'HEAD'
]
return GitGutterHandler.run_command(args)
return self.run_command(args)

@staticmethod
def run_command(args, decode=True):
def run_command(self, args, decode=True):
"""Run a git command asynchronously and return a Promise.

Arguments:
Expand All @@ -479,8 +504,9 @@ def read_output(resolve):
else:
startupinfo = None
proc = subprocess.Popen(
args=args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, startupinfo=startupinfo)
args=args, cwd=self._git_tree, startupinfo=startupinfo,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
if _HAVE_TIMEOUT:
stdout, stderr = proc.communicate(timeout=30)
else:
Expand Down
14 changes: 7 additions & 7 deletions git_gutter_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def load_settings(self):
self._user_settings.get('show_in_minimap') or
self._settings.get('show_in_minimap'))

def get_compare_against(self, git_dir, view):
def get_compare_against(self, git_tree, view):
"""Return the compare target for a view.

If interactivly specified a compare target for the view's repository,
Expand All @@ -115,26 +115,26 @@ def get_compare_against(self, git_dir, view):
fall back to HEAD if everything goes wrong to avoid exceptions.

Arguments:
git_dir - path of the `.git` directory holding the index
git_tree - real root path of the current work-tree
view - the view whose settings to query first
"""
# Interactively specified compare target overrides settings.
if git_dir in self._compare_against_mapping:
return self._compare_against_mapping[git_dir]
if git_tree in self._compare_against_mapping:
return self._compare_against_mapping[git_tree]
# Project settings and Preferences override plugin settings if set.
compare = view.settings().get('git_gutter_compare_against')
if not compare:
compare = self.get('compare_against', 'HEAD')
return compare

def set_compare_against(self, git_dir, new_compare_against):
def set_compare_against(self, git_tree, new_compare_against):
"""Assign a new compare target for current repository.

Arguments:
git_dir - path of the .git directory holding the index
git_tree - real root path of the current work-tree
new_compare_against - new branch/tag/commit to cmpare against
"""
self._compare_against_mapping[git_dir] = new_compare_against
self._compare_against_mapping[git_tree] = new_compare_against

@property
def default_theme_path(self):
Expand Down
7 changes: 6 additions & 1 deletion git_gutter_show_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def __init__(self, view, git_handler):
self.diff_results = None
self.show_untracked = False

def clear(self):
"""Remove all gutter icons and status messages."""
self.view.erase_status('00_git_gutter')
self._clear_all()

def run(self):
"""Run diff and update gutter icons and status message."""

Expand Down Expand Up @@ -108,7 +113,7 @@ def set_status(branch_name):
if template:
# render the template using jinja2 library
text = jinja2.environment.Template(''.join(template)).render(
repo=os.path.basename(self.git_handler.git_tree),
repo=self.git_handler.repository_name,
compare=self.git_handler.format_compare_against(),
branch=branch_name, state=file_state, deleted=len(deleted),
inserted=len(inserted), modified=len(modified))
Expand Down
Loading