PageRenderTime 50ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/Git/git.py

https://bitbucket.org/rafaelmoreira/sublime-text
Python | 333 lines | 280 code | 28 blank | 25 comment | 46 complexity | 90cfe49ce3fa4731dd235e6ea4a95d61 MD5 | raw file
  1. import os
  2. import sublime
  3. import sublime_plugin
  4. import threading
  5. import subprocess
  6. import functools
  7. import os.path
  8. import time
  9. # when sublime loads a plugin it's cd'd into the plugin directory. Thus
  10. # __file__ is useless for my purposes. What I want is "Packages/Git", but
  11. # allowing for the possibility that someone has renamed the file.
  12. # Fun discovery: Sublime on windows still requires posix path separators.
  13. PLUGIN_DIRECTORY = os.getcwd().replace(os.path.normpath(os.path.join(os.getcwd(), '..', '..')) + os.path.sep, '').replace(os.path.sep, '/')
  14. git_root_cache = {}
  15. def main_thread(callback, *args, **kwargs):
  16. # sublime.set_timeout gets used to send things onto the main thread
  17. # most sublime.[something] calls need to be on the main thread
  18. sublime.set_timeout(functools.partial(callback, *args, **kwargs), 0)
  19. def open_url(url):
  20. sublime.active_window().run_command('open_url', {"url": url})
  21. def git_root(directory):
  22. global git_root_cache
  23. retval = False
  24. leaf_dir = directory
  25. if leaf_dir in git_root_cache and git_root_cache[leaf_dir]['expires'] > time.time():
  26. return git_root_cache[leaf_dir]['retval']
  27. while directory:
  28. if os.path.exists(os.path.join(directory, '.git')):
  29. retval = directory
  30. break
  31. parent = os.path.realpath(os.path.join(directory, os.path.pardir))
  32. if parent == directory:
  33. # /.. == /
  34. retval = False
  35. break
  36. directory = parent
  37. git_root_cache[leaf_dir] = {
  38. 'retval': retval,
  39. 'expires': time.time() + 5
  40. }
  41. return retval
  42. # for readability code
  43. def git_root_exist(directory):
  44. return git_root(directory)
  45. def view_contents(view):
  46. region = sublime.Region(0, view.size())
  47. return view.substr(region)
  48. def plugin_file(name):
  49. return os.path.join(PLUGIN_DIRECTORY, name)
  50. def do_when(conditional, callback, *args, **kwargs):
  51. if conditional():
  52. return callback(*args, **kwargs)
  53. sublime.set_timeout(functools.partial(do_when, conditional, callback, *args, **kwargs), 50)
  54. def _make_text_safeish(text, fallback_encoding, method='decode'):
  55. # The unicode decode here is because sublime converts to unicode inside
  56. # insert in such a way that unknown characters will cause errors, which is
  57. # distinctly non-ideal... and there's no way to tell what's coming out of
  58. # git in output. So...
  59. try:
  60. unitext = getattr(text, method)('utf-8')
  61. except (UnicodeEncodeError, UnicodeDecodeError):
  62. unitext = getattr(text, method)(fallback_encoding)
  63. return unitext
  64. class CommandThread(threading.Thread):
  65. def __init__(self, command, on_done, working_dir="", fallback_encoding="", **kwargs):
  66. threading.Thread.__init__(self)
  67. self.command = command
  68. self.on_done = on_done
  69. self.working_dir = working_dir
  70. if "stdin" in kwargs:
  71. self.stdin = kwargs["stdin"]
  72. else:
  73. self.stdin = None
  74. if "stdout" in kwargs:
  75. self.stdout = kwargs["stdout"]
  76. else:
  77. self.stdout = subprocess.PIPE
  78. self.fallback_encoding = fallback_encoding
  79. self.kwargs = kwargs
  80. def run(self):
  81. try:
  82. # Ignore directories that no longer exist
  83. if os.path.isdir(self.working_dir):
  84. # Per http://bugs.python.org/issue8557 shell=True is required to
  85. # get $PATH on Windows. Yay portable code.
  86. shell = os.name == 'nt'
  87. if self.working_dir != "":
  88. os.chdir(self.working_dir)
  89. proc = subprocess.Popen(self.command,
  90. stdout=self.stdout, stderr=subprocess.STDOUT,
  91. stdin=subprocess.PIPE,
  92. shell=shell, universal_newlines=True)
  93. output = proc.communicate(self.stdin)[0]
  94. if not output:
  95. output = ''
  96. # if sublime's python gets bumped to 2.7 we can just do:
  97. # output = subprocess.check_output(self.command)
  98. main_thread(self.on_done,
  99. _make_text_safeish(output, self.fallback_encoding), **self.kwargs)
  100. except subprocess.CalledProcessError, e:
  101. main_thread(self.on_done, e.returncode)
  102. except OSError, e:
  103. if e.errno == 2:
  104. 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'])
  105. else:
  106. raise e
  107. # A base for all commands
  108. class GitCommand(object):
  109. may_change_files = False
  110. def run_command(self, command, callback=None, show_status=True,
  111. filter_empty_args=True, no_save=False, **kwargs):
  112. if filter_empty_args:
  113. command = [arg for arg in command if arg]
  114. if 'working_dir' not in kwargs:
  115. kwargs['working_dir'] = self.get_working_dir()
  116. if 'fallback_encoding' not in kwargs and self.active_view() and self.active_view().settings().get('fallback_encoding'):
  117. kwargs['fallback_encoding'] = self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]
  118. s = sublime.load_settings("Git.sublime-settings")
  119. if s.get('save_first') and self.active_view() and self.active_view().is_dirty() and not no_save:
  120. self.active_view().run_command('save')
  121. if command[0] == 'git' and s.get('git_command'):
  122. command[0] = s.get('git_command')
  123. if command[0] == 'git-flow' and s.get('git_flow_command'):
  124. command[0] = s.get('git_flow_command')
  125. if not callback:
  126. callback = self.generic_done
  127. thread = CommandThread(command, callback, **kwargs)
  128. thread.start()
  129. if show_status:
  130. message = kwargs.get('status_message', False) or ' '.join(command)
  131. sublime.status_message(message)
  132. def generic_done(self, result):
  133. if self.may_change_files and self.active_view() and self.active_view().file_name():
  134. if self.active_view().is_dirty():
  135. result = "WARNING: Current view is dirty.\n\n"
  136. else:
  137. # just asking the current file to be re-opened doesn't do anything
  138. print "reverting"
  139. position = self.active_view().viewport_position()
  140. self.active_view().run_command('revert')
  141. do_when(lambda: not self.active_view().is_loading(), lambda: self.active_view().set_viewport_position(position, False))
  142. # self.active_view().show(position)
  143. view = self.active_view()
  144. if view and view.settings().get('live_git_annotations'):
  145. self.view.run_command('git_annotate')
  146. if not result.strip():
  147. return
  148. self.panel(result)
  149. def _output_to_view(self, output_file, output, clear=False,
  150. syntax="Packages/Diff/Diff.tmLanguage", **kwargs):
  151. output_file.set_syntax_file(syntax)
  152. edit = output_file.begin_edit()
  153. if clear:
  154. region = sublime.Region(0, self.output_view.size())
  155. output_file.erase(edit, region)
  156. output_file.insert(edit, 0, output)
  157. output_file.end_edit(edit)
  158. def scratch(self, output, title=False, position=None, **kwargs):
  159. scratch_file = self.get_window().new_file()
  160. if title:
  161. scratch_file.set_name(title)
  162. scratch_file.set_scratch(True)
  163. self._output_to_view(scratch_file, output, **kwargs)
  164. scratch_file.set_read_only(True)
  165. if position:
  166. sublime.set_timeout(lambda: scratch_file.set_viewport_position(position), 0)
  167. return scratch_file
  168. def panel(self, output, **kwargs):
  169. if not hasattr(self, 'output_view'):
  170. self.output_view = self.get_window().get_output_panel("git")
  171. self.output_view.set_read_only(False)
  172. self._output_to_view(self.output_view, output, clear=True, **kwargs)
  173. self.output_view.set_read_only(True)
  174. self.get_window().run_command("show_panel", {"panel": "output.git"})
  175. def quick_panel(self, *args, **kwargs):
  176. self.get_window().show_quick_panel(*args, **kwargs)
  177. # A base for all git commands that work with the entire repository
  178. class GitWindowCommand(GitCommand, sublime_plugin.WindowCommand):
  179. def active_view(self):
  180. return self.window.active_view()
  181. def _active_file_name(self):
  182. view = self.active_view()
  183. if view and view.file_name() and len(view.file_name()) > 0:
  184. return view.file_name()
  185. @property
  186. def fallback_encoding(self):
  187. if self.active_view() and self.active_view().settings().get('fallback_encoding'):
  188. return self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]
  189. # If there's no active view or the active view is not a file on the
  190. # filesystem (e.g. a search results view), we can infer the folder
  191. # that the user intends Git commands to run against when there's only
  192. # only one.
  193. def is_enabled(self):
  194. if self._active_file_name() or len(self.window.folders()) == 1:
  195. return git_root(self.get_working_dir())
  196. def get_file_name(self):
  197. return ''
  198. def get_relative_file_name(self):
  199. return ''
  200. # If there is a file in the active view use that file's directory to
  201. # search for the Git root. Otherwise, use the only folder that is
  202. # open.
  203. def get_working_dir(self):
  204. file_name = self._active_file_name()
  205. if file_name:
  206. return os.path.realpath(os.path.dirname(file_name))
  207. else:
  208. try: # handle case with no open folder
  209. return self.window.folders()[0]
  210. except IndexError:
  211. return ''
  212. def get_window(self):
  213. return self.window
  214. # A base for all git commands that work with the file in the active view
  215. class GitTextCommand(GitCommand, sublime_plugin.TextCommand):
  216. def active_view(self):
  217. return self.view
  218. def is_enabled(self):
  219. # First, is this actually a file on the file system?
  220. if self.view.file_name() and len(self.view.file_name()) > 0:
  221. return git_root(self.get_working_dir())
  222. def get_file_name(self):
  223. return os.path.basename(self.view.file_name())
  224. def get_relative_file_name(self):
  225. working_dir = self.get_working_dir()
  226. file_path = working_dir.replace(git_root(working_dir), '')[1:]
  227. file_name = os.path.join(file_path, self.get_file_name())
  228. return file_name.replace('\\', '/') # windows issues
  229. def get_working_dir(self):
  230. return os.path.realpath(os.path.dirname(self.view.file_name()))
  231. def get_window(self):
  232. # Fun discovery: if you switch tabs while a command is working,
  233. # self.view.window() is None. (Admittedly this is a consequence
  234. # of my deciding to do async command processing... but, hey,
  235. # got to live with that now.)
  236. # I did try tracking the window used at the start of the command
  237. # and using it instead of view.window() later, but that results
  238. # panels on a non-visible window, which is especially useless in
  239. # the case of the quick panel.
  240. # So, this is not necessarily ideal, but it does work.
  241. return self.view.window() or sublime.active_window()
  242. # A few miscellaneous commands
  243. class GitCustomCommand(GitWindowCommand):
  244. may_change_files = True
  245. def run(self):
  246. self.get_window().show_input_panel("Git command", "",
  247. self.on_input, None, None)
  248. def on_input(self, command):
  249. command = str(command) # avoiding unicode
  250. if command.strip() == "":
  251. self.panel("No git command provided")
  252. return
  253. import shlex
  254. command_splitted = ['git'] + shlex.split(command)
  255. print command_splitted
  256. self.run_command(command_splitted)
  257. class GitGuiCommand(GitTextCommand):
  258. def run(self, edit):
  259. command = ['git', 'gui']
  260. self.run_command(command)
  261. class GitGitkCommand(GitTextCommand):
  262. def run(self, edit):
  263. command = ['gitk']
  264. self.run_command(command)