/Git/git.py
Python | 333 lines | 280 code | 28 blank | 25 comment | 46 complexity | 90cfe49ce3fa4731dd235e6ea4a95d61 MD5 | raw file
- import os
- import sublime
- import sublime_plugin
- import threading
- import subprocess
- import functools
- import os.path
- import time
- # when sublime loads a plugin it's cd'd into the plugin directory. Thus
- # __file__ is useless for my purposes. What I want is "Packages/Git", but
- # allowing for the possibility that someone has renamed the file.
- # Fun discovery: Sublime on windows still requires posix path separators.
- PLUGIN_DIRECTORY = os.getcwd().replace(os.path.normpath(os.path.join(os.getcwd(), '..', '..')) + os.path.sep, '').replace(os.path.sep, '/')
- git_root_cache = {}
- def main_thread(callback, *args, **kwargs):
- # sublime.set_timeout gets used to send things onto the main thread
- # most sublime.[something] calls need to be on the main thread
- sublime.set_timeout(functools.partial(callback, *args, **kwargs), 0)
- def open_url(url):
- sublime.active_window().run_command('open_url', {"url": url})
- def git_root(directory):
- global git_root_cache
- retval = False
- leaf_dir = directory
- if leaf_dir in git_root_cache and git_root_cache[leaf_dir]['expires'] > time.time():
- return git_root_cache[leaf_dir]['retval']
- while directory:
- if os.path.exists(os.path.join(directory, '.git')):
- retval = directory
- break
- parent = os.path.realpath(os.path.join(directory, os.path.pardir))
- if parent == directory:
- # /.. == /
- retval = False
- break
- directory = parent
- git_root_cache[leaf_dir] = {
- 'retval': retval,
- 'expires': time.time() + 5
- }
- return retval
- # for readability code
- def git_root_exist(directory):
- return git_root(directory)
- def view_contents(view):
- region = sublime.Region(0, view.size())
- return view.substr(region)
- def plugin_file(name):
- return os.path.join(PLUGIN_DIRECTORY, name)
- def do_when(conditional, callback, *args, **kwargs):
- if conditional():
- return callback(*args, **kwargs)
- sublime.set_timeout(functools.partial(do_when, conditional, callback, *args, **kwargs), 50)
- def _make_text_safeish(text, fallback_encoding, method='decode'):
- # The unicode decode here is because sublime converts to unicode inside
- # insert in such a way that unknown characters will cause errors, which is
- # distinctly non-ideal... and there's no way to tell what's coming out of
- # git in output. So...
- try:
- unitext = getattr(text, method)('utf-8')
- except (UnicodeEncodeError, UnicodeDecodeError):
- unitext = getattr(text, method)(fallback_encoding)
- return unitext
- class CommandThread(threading.Thread):
- def __init__(self, command, on_done, working_dir="", fallback_encoding="", **kwargs):
- threading.Thread.__init__(self)
- self.command = command
- self.on_done = on_done
- self.working_dir = working_dir
- if "stdin" in kwargs:
- self.stdin = kwargs["stdin"]
- else:
- self.stdin = None
- if "stdout" in kwargs:
- self.stdout = kwargs["stdout"]
- else:
- self.stdout = subprocess.PIPE
- self.fallback_encoding = fallback_encoding
- self.kwargs = kwargs
- def run(self):
- try:
- # Ignore directories that no longer exist
- if os.path.isdir(self.working_dir):
- # Per http://bugs.python.org/issue8557 shell=True is required to
- # get $PATH on Windows. Yay portable code.
- shell = os.name == 'nt'
- if self.working_dir != "":
- os.chdir(self.working_dir)
- proc = subprocess.Popen(self.command,
- stdout=self.stdout, stderr=subprocess.STDOUT,
- stdin=subprocess.PIPE,
- shell=shell, universal_newlines=True)
- output = proc.communicate(self.stdin)[0]
- if not output:
- output = ''
- # if sublime's python gets bumped to 2.7 we can just do:
- # output = subprocess.check_output(self.command)
- main_thread(self.on_done,
- _make_text_safeish(output, self.fallback_encoding), **self.kwargs)
- except subprocess.CalledProcessError, e:
- main_thread(self.on_done, e.returncode)
- except OSError, e:
- if e.errno == 2:
- main_thread(sublime.error_message, "Git binary could not be found in PATH\n\nConsider using the git_command setting for the Git plugin\n\nPATH is: %s" % os.environ['PATH'])
- else:
- raise e
- # A base for all commands
- class GitCommand(object):
- may_change_files = False
- def run_command(self, command, callback=None, show_status=True,
- filter_empty_args=True, no_save=False, **kwargs):
- if filter_empty_args:
- command = [arg for arg in command if arg]
- if 'working_dir' not in kwargs:
- kwargs['working_dir'] = self.get_working_dir()
- if 'fallback_encoding' not in kwargs and self.active_view() and self.active_view().settings().get('fallback_encoding'):
- kwargs['fallback_encoding'] = self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]
- s = sublime.load_settings("Git.sublime-settings")
- if s.get('save_first') and self.active_view() and self.active_view().is_dirty() and not no_save:
- self.active_view().run_command('save')
- if command[0] == 'git' and s.get('git_command'):
- command[0] = s.get('git_command')
- if command[0] == 'git-flow' and s.get('git_flow_command'):
- command[0] = s.get('git_flow_command')
- if not callback:
- callback = self.generic_done
- thread = CommandThread(command, callback, **kwargs)
- thread.start()
- if show_status:
- message = kwargs.get('status_message', False) or ' '.join(command)
- sublime.status_message(message)
- def generic_done(self, result):
- if self.may_change_files and self.active_view() and self.active_view().file_name():
- if self.active_view().is_dirty():
- result = "WARNING: Current view is dirty.\n\n"
- else:
- # just asking the current file to be re-opened doesn't do anything
- print "reverting"
- position = self.active_view().viewport_position()
- self.active_view().run_command('revert')
- do_when(lambda: not self.active_view().is_loading(), lambda: self.active_view().set_viewport_position(position, False))
- # self.active_view().show(position)
- view = self.active_view()
- if view and view.settings().get('live_git_annotations'):
- self.view.run_command('git_annotate')
- if not result.strip():
- return
- self.panel(result)
- def _output_to_view(self, output_file, output, clear=False,
- syntax="Packages/Diff/Diff.tmLanguage", **kwargs):
- output_file.set_syntax_file(syntax)
- edit = output_file.begin_edit()
- if clear:
- region = sublime.Region(0, self.output_view.size())
- output_file.erase(edit, region)
- output_file.insert(edit, 0, output)
- output_file.end_edit(edit)
- def scratch(self, output, title=False, position=None, **kwargs):
- scratch_file = self.get_window().new_file()
- if title:
- scratch_file.set_name(title)
- scratch_file.set_scratch(True)
- self._output_to_view(scratch_file, output, **kwargs)
- scratch_file.set_read_only(True)
- if position:
- sublime.set_timeout(lambda: scratch_file.set_viewport_position(position), 0)
- return scratch_file
- def panel(self, output, **kwargs):
- if not hasattr(self, 'output_view'):
- self.output_view = self.get_window().get_output_panel("git")
- self.output_view.set_read_only(False)
- self._output_to_view(self.output_view, output, clear=True, **kwargs)
- self.output_view.set_read_only(True)
- self.get_window().run_command("show_panel", {"panel": "output.git"})
- def quick_panel(self, *args, **kwargs):
- self.get_window().show_quick_panel(*args, **kwargs)
- # A base for all git commands that work with the entire repository
- class GitWindowCommand(GitCommand, sublime_plugin.WindowCommand):
- def active_view(self):
- return self.window.active_view()
- def _active_file_name(self):
- view = self.active_view()
- if view and view.file_name() and len(view.file_name()) > 0:
- return view.file_name()
- @property
- def fallback_encoding(self):
- if self.active_view() and self.active_view().settings().get('fallback_encoding'):
- return self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]
- # If there's no active view or the active view is not a file on the
- # filesystem (e.g. a search results view), we can infer the folder
- # that the user intends Git commands to run against when there's only
- # only one.
- def is_enabled(self):
- if self._active_file_name() or len(self.window.folders()) == 1:
- return git_root(self.get_working_dir())
- def get_file_name(self):
- return ''
- def get_relative_file_name(self):
- return ''
- # If there is a file in the active view use that file's directory to
- # search for the Git root. Otherwise, use the only folder that is
- # open.
- def get_working_dir(self):
- file_name = self._active_file_name()
- if file_name:
- return os.path.realpath(os.path.dirname(file_name))
- else:
- try: # handle case with no open folder
- return self.window.folders()[0]
- except IndexError:
- return ''
- def get_window(self):
- return self.window
- # A base for all git commands that work with the file in the active view
- class GitTextCommand(GitCommand, sublime_plugin.TextCommand):
- def active_view(self):
- return self.view
- def is_enabled(self):
- # First, is this actually a file on the file system?
- if self.view.file_name() and len(self.view.file_name()) > 0:
- return git_root(self.get_working_dir())
- def get_file_name(self):
- return os.path.basename(self.view.file_name())
- def get_relative_file_name(self):
- working_dir = self.get_working_dir()
- file_path = working_dir.replace(git_root(working_dir), '')[1:]
- file_name = os.path.join(file_path, self.get_file_name())
- return file_name.replace('\\', '/') # windows issues
- def get_working_dir(self):
- return os.path.realpath(os.path.dirname(self.view.file_name()))
- def get_window(self):
- # Fun discovery: if you switch tabs while a command is working,
- # self.view.window() is None. (Admittedly this is a consequence
- # of my deciding to do async command processing... but, hey,
- # got to live with that now.)
- # I did try tracking the window used at the start of the command
- # and using it instead of view.window() later, but that results
- # panels on a non-visible window, which is especially useless in
- # the case of the quick panel.
- # So, this is not necessarily ideal, but it does work.
- return self.view.window() or sublime.active_window()
- # A few miscellaneous commands
- class GitCustomCommand(GitWindowCommand):
- may_change_files = True
- def run(self):
- self.get_window().show_input_panel("Git command", "",
- self.on_input, None, None)
- def on_input(self, command):
- command = str(command) # avoiding unicode
- if command.strip() == "":
- self.panel("No git command provided")
- return
- import shlex
- command_splitted = ['git'] + shlex.split(command)
- print command_splitted
- self.run_command(command_splitted)
- class GitGuiCommand(GitTextCommand):
- def run(self, edit):
- command = ['git', 'gui']
- self.run_command(command)
- class GitGitkCommand(GitTextCommand):
- def run(self, edit):
- command = ['gitk']
- self.run_command(command)