Created
July 2, 2014 08:29
-
-
Save manuelmeurer/302524da392a12953a7a to your computer and use it in GitHub Desktop.
git.py from kemayo/sublime-text-git ST plugin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, command=None): | |
if command is None: | |
self.get_window().show_input_panel("Git command", "", | |
self.on_input, None, None) | |
else: | |
self.on_input(command) | |
def on_input(self, command): | |
command = str(command) # avoiding unicode | |
if command.strip() == "": | |
self.panel("No git command provided") | |
return | |
import shlex | |
cmds = [c.strip() for c in command.split(';') if c.strip() != ''] | |
for cmd in cmds: | |
command_splitted = ['git'] + shlex.split(cmd) | |
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) | |
class GitGitkAllCommand(GitTextCommand): | |
def run(self, edit): | |
command = ['gitk', '--all'] | |
self.run_command(command) | |
class GitGitkThisFileCommand(GitTextCommand): | |
def run(self, edit): | |
command = ['gitk', self.get_file_name()] | |
self.run_command(command) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Is this a fork that is ST3 Compatible?