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)