Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 351 lines (278 sloc) 12.838 kB
4ea0996 @kemayo Initial commit
authored
1 import os
2 import sublime
3 import sublime_plugin
4 import threading
5 import subprocess
6 import functools
d907f7d @buhrmi fixed diff on files in subdirectories
buhrmi authored
7 import os.path
86fc50c @jotson Implements a simple caching mechanism for git_root() to keep the edit…
jotson authored
8 import time
4ea0996 @kemayo Initial commit
authored
9
6d6348c @kemayo Work out the plugin directory
authored
10 # when sublime loads a plugin it's cd'd into the plugin directory. Thus
11 # __file__ is useless for my purposes. What I want is "Packages/Git", but
12 # allowing for the possibility that someone has renamed the file.
83c70df @kemayo Fun discovery: Sublime on windows still requires posix path separators
authored
13 # Fun discovery: Sublime on windows still requires posix path separators.
14 PLUGIN_DIRECTORY = os.getcwd().replace(os.path.normpath(os.path.join(os.getcwd(), '..', '..')) + os.path.sep, '').replace(os.path.sep, '/')
6d6348c @kemayo Work out the plugin directory
authored
15
e51aa66 @kemayo Periodic PEP8 sweep
authored
16 git_root_cache = {}
1246020 @spadgos Adds a quick panel for inserting commit messages
spadgos authored
17
b6c808d @kemayo "All file" in add menu
authored
18
0689079 @kemayo Properly working log and diff
authored
19 def main_thread(callback, *args, **kwargs):
20 # sublime.set_timeout gets used to send things onto the main thread
21 # most sublime.[something] calls need to be on the main thread
22 sublime.set_timeout(functools.partial(callback, *args, **kwargs), 0)
23
b6c808d @kemayo "All file" in add menu
authored
24
0689079 @kemayo Properly working log and diff
authored
25 def open_url(url):
26 sublime.active_window().run_command('open_url', {"url": url})
4ea0996 @kemayo Initial commit
authored
27
b6c808d @kemayo "All file" in add menu
authored
28
6846b53 @kemayo Status diffs on files in different directories
authored
29 def git_root(directory):
86fc50c @jotson Implements a simple caching mechanism for git_root() to keep the edit…
jotson authored
30 global git_root_cache
31
32 retval = False
33 leaf_dir = directory
34
35 if leaf_dir in git_root_cache and git_root_cache[leaf_dir]['expires'] > time.time():
36 return git_root_cache[leaf_dir]['retval']
37
6846b53 @kemayo Status diffs on files in different directories
authored
38 while directory:
39 if os.path.exists(os.path.join(directory, '.git')):
86fc50c @jotson Implements a simple caching mechanism for git_root() to keep the edit…
jotson authored
40 retval = directory
41 break
6846b53 @kemayo Status diffs on files in different directories
authored
42 parent = os.path.realpath(os.path.join(directory, os.path.pardir))
43 if parent == directory:
44 # /.. == /
86fc50c @jotson Implements a simple caching mechanism for git_root() to keep the edit…
jotson authored
45 retval = False
46 break
6846b53 @kemayo Status diffs on files in different directories
authored
47 directory = parent
86fc50c @jotson Implements a simple caching mechanism for git_root() to keep the edit…
jotson authored
48
e51aa66 @kemayo Periodic PEP8 sweep
authored
49 git_root_cache[leaf_dir] = {
50 'retval': retval,
51 'expires': time.time() + 5
52 }
86fc50c @jotson Implements a simple caching mechanism for git_root() to keep the edit…
jotson authored
53
54 return retval
6846b53 @kemayo Status diffs on files in different directories
authored
55
b6c808d @kemayo "All file" in add menu
authored
56
3fe55cd @Proghat add git init command
Proghat authored
57 # for readability code
58 def git_root_exist(directory):
59 return git_root(directory)
b6c808d @kemayo "All file" in add menu
authored
60
9ab4490 @Proghat error message shows in case git init failure
Proghat authored
61
5a09aaa @kemayo Commit staged files
authored
62 def view_contents(view):
63 region = sublime.Region(0, view.size())
64 return view.substr(region)
65
b6c808d @kemayo "All file" in add menu
authored
66
6d6348c @kemayo Work out the plugin directory
authored
67 def plugin_file(name):
68 return os.path.join(PLUGIN_DIRECTORY, name)
69
70
ab3a587 @kemayo Change how attempts to reload work
authored
71 def do_when(conditional, callback, *args, **kwargs):
72 if conditional():
73 return callback(*args, **kwargs)
74 sublime.set_timeout(functools.partial(do_when, conditional, callback, *args, **kwargs), 50)
75
76
1f80050 @zerc fix encode error when writing tmp file with commit msg
zerc authored
77 def _make_text_safeish(text, fallback_encoding, method='decode'):
b6c808d @kemayo "All file" in add menu
authored
78 # The unicode decode here is because sublime converts to unicode inside
79 # insert in such a way that unknown characters will cause errors, which is
80 # distinctly non-ideal... and there's no way to tell what's coming out of
81 # git in output. So...
5d5e75d Add fallback_encoding in addition to UTF8 for git output decoding
Dominique Wahli authored
82 try:
1f80050 @zerc fix encode error when writing tmp file with commit msg
zerc authored
83 unitext = getattr(text, method)('utf-8')
84 except (UnicodeEncodeError, UnicodeDecodeError):
85 unitext = getattr(text, method)(fallback_encoding)
5d5e75d Add fallback_encoding in addition to UTF8 for git output decoding
Dominique Wahli authored
86 return unitext
6516c79 @kemayo Rearrange text encoding
authored
87
b6c808d @kemayo "All file" in add menu
authored
88
4ea0996 @kemayo Initial commit
authored
89 class CommandThread(threading.Thread):
8ca8feb @smacker Git show command
smacker authored
90 def __init__(self, command, on_done, working_dir="", fallback_encoding="", **kwargs):
4ea0996 @kemayo Initial commit
authored
91 threading.Thread.__init__(self)
92 self.command = command
93 self.on_done = on_done
94 self.working_dir = working_dir
4e17f8b @kemayo I have clear stylistic preferences, okay
authored
95 if "stdin" in kwargs:
96 self.stdin = kwargs["stdin"]
97 else:
98 self.stdin = None
99 if "stdout" in kwargs:
100 self.stdout = kwargs["stdout"]
101 else:
102 self.stdout = subprocess.PIPE
5d5e75d Add fallback_encoding in addition to UTF8 for git output decoding
Dominique Wahli authored
103 self.fallback_encoding = fallback_encoding
8ca8feb @smacker Git show command
smacker authored
104 self.kwargs = kwargs
4ea0996 @kemayo Initial commit
authored
105
106 def run(self):
107 try:
822c6da @spence ignore files in non-existent directories
spence authored
108
109 # Ignore directories that no longer exist
110 if os.path.isdir(self.working_dir):
111
112 # Per http://bugs.python.org/issue8557 shell=True is required to
113 # get $PATH on Windows. Yay portable code.
114 shell = os.name == 'nt'
115 if self.working_dir != "":
116 os.chdir(self.working_dir)
117
118 proc = subprocess.Popen(self.command,
119 stdout=self.stdout, stderr=subprocess.STDOUT,
120 stdin=subprocess.PIPE,
f5269ee @bcomnes Work around for OSX 10.10 PATH issues
bcomnes authored
121 shell=shell, universal_newlines=True,
122 env=os.environ)
822c6da @spence ignore files in non-existent directories
spence authored
123 output = proc.communicate(self.stdin)[0]
124 if not output:
125 output = ''
126 # if sublime's python gets bumped to 2.7 we can just do:
127 # output = subprocess.check_output(self.command)
128 main_thread(self.on_done,
129 _make_text_safeish(output, self.fallback_encoding), **self.kwargs)
130
4ea0996 @kemayo Initial commit
authored
131 except subprocess.CalledProcessError, e:
0c75b50 @kemayo Simplify callbacks
authored
132 main_thread(self.on_done, e.returncode)
899e414 @kemayo Error message for missing git binary
authored
133 except OSError, e:
134 if e.errno == 2:
48cf291 @kemayo Slightly more verbose error message
authored
135 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'])
899e414 @kemayo Error message for missing git binary
authored
136 else:
137 raise e
4ea0996 @kemayo Initial commit
authored
138
b6c808d @kemayo "All file" in add menu
authored
139
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
140 # A base for all commands
667e153 @singingwolfboy All classes should inherit from `object`
singingwolfboy authored
141 class GitCommand(object):
ab3a587 @kemayo Change how attempts to reload work
authored
142 may_change_files = False
143
b6c808d @kemayo "All file" in add menu
authored
144 def run_command(self, command, callback=None, show_status=True,
d09923f @buhrmi Annotation is now updated live while editing
buhrmi authored
145 filter_empty_args=True, no_save=False, **kwargs):
f420bc0 @kemayo Blame (I don't like the output, but it works)
authored
146 if filter_empty_args:
147 command = [arg for arg in command if arg]
148 if 'working_dir' not in kwargs:
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
149 kwargs['working_dir'] = self.get_working_dir()
150 if 'fallback_encoding' not in kwargs and self.active_view() and self.active_view().settings().get('fallback_encoding'):
899e414 @kemayo Error message for missing git binary
authored
151 kwargs['fallback_encoding'] = self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]
b6c808d @kemayo "All file" in add menu
authored
152
0c4ee57 @kemayo Some settings
authored
153 s = sublime.load_settings("Git.sublime-settings")
d09923f @buhrmi Annotation is now updated live while editing
buhrmi authored
154 if s.get('save_first') and self.active_view() and self.active_view().is_dirty() and not no_save:
899e414 @kemayo Error message for missing git binary
authored
155 self.active_view().run_command('save')
5a09aaa @kemayo Commit staged files
authored
156 if command[0] == 'git' and s.get('git_command'):
0c4ee57 @kemayo Some settings
authored
157 command[0] = s.get('git_command')
6535742 @vespakoen Added git-flow to config
vespakoen authored
158 if command[0] == 'git-flow' and s.get('git_flow_command'):
159 command[0] = s.get('git_flow_command')
b6c808d @kemayo "All file" in add menu
authored
160 if not callback:
161 callback = self.generic_done
0c4ee57 @kemayo Some settings
authored
162
b6c808d @kemayo "All file" in add menu
authored
163 thread = CommandThread(command, callback, **kwargs)
f420bc0 @kemayo Blame (I don't like the output, but it works)
authored
164 thread.start()
165
166 if show_status:
167 message = kwargs.get('status_message', False) or ' '.join(command)
168 sublime.status_message(message)
03602f9 @kemayo Refactor scratch a little
authored
169
0c75b50 @kemayo Simplify callbacks
authored
170 def generic_done(self, result):
ab3a587 @kemayo Change how attempts to reload work
authored
171 if self.may_change_files and self.active_view() and self.active_view().file_name():
172 if self.active_view().is_dirty():
173 result = "WARNING: Current view is dirty.\n\n"
174 else:
175 # just asking the current file to be re-opened doesn't do anything
176 print "reverting"
177 position = self.active_view().viewport_position()
178 self.active_view().run_command('revert')
179 do_when(lambda: not self.active_view().is_loading(), lambda: self.active_view().set_viewport_position(position, False))
180 # self.active_view().show(position)
f9ac419 @kemayo Fix issue with checkout not refreshing file, properly
authored
181
d276a61 @sheldon quick safety check for contexts that don't have a view
sheldon authored
182 view = self.active_view()
183 if view and view.settings().get('live_git_annotations'):
6ec38a7 @sheldon added auto-on config option, and put back per file toggle
sheldon authored
184 self.view.run_command('git_annotate')
3c54bce @sheldon global annotations
sheldon authored
185
f9ac419 @kemayo Fix issue with checkout not refreshing file, properly
authored
186 if not result.strip():
187 return
ab3a587 @kemayo Change how attempts to reload work
authored
188 self.panel(result)
0c75b50 @kemayo Simplify callbacks
authored
189
b6c808d @kemayo "All file" in add menu
authored
190 def _output_to_view(self, output_file, output, clear=False,
991b819 @sheldon was throwing an error sometimes when passing along stdin as a named p…
sheldon authored
191 syntax="Packages/Diff/Diff.tmLanguage", **kwargs):
f129db9 @kemayo Unicode issues
authored
192 output_file.set_syntax_file(syntax)
193 edit = output_file.begin_edit()
ad23845 @kemayo Fix up commit
authored
194 if clear:
195 region = sublime.Region(0, self.output_view.size())
196 output_file.erase(edit, region)
6516c79 @kemayo Rearrange text encoding
authored
197 output_file.insert(edit, 0, output)
f129db9 @kemayo Unicode issues
authored
198 output_file.end_edit(edit)
199
46f6ddc @singingwolfboy blame: scroll scratch buffer
singingwolfboy authored
200 def scratch(self, output, title=False, position=None, **kwargs):
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
201 scratch_file = self.get_window().new_file()
03602f9 @kemayo Refactor scratch a little
authored
202 if title:
203 scratch_file.set_name(title)
204 scratch_file.set_scratch(True)
f129db9 @kemayo Unicode issues
authored
205 self._output_to_view(scratch_file, output, **kwargs)
03602f9 @kemayo Refactor scratch a little
authored
206 scratch_file.set_read_only(True)
46f6ddc @singingwolfboy blame: scroll scratch buffer
singingwolfboy authored
207 if position:
208 sublime.set_timeout(lambda: scratch_file.set_viewport_position(position), 0)
03602f9 @kemayo Refactor scratch a little
authored
209 return scratch_file
b6c808d @kemayo "All file" in add menu
authored
210
f129db9 @kemayo Unicode issues
authored
211 def panel(self, output, **kwargs):
03602f9 @kemayo Refactor scratch a little
authored
212 if not hasattr(self, 'output_view'):
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
213 self.output_view = self.get_window().get_output_panel("git")
03602f9 @kemayo Refactor scratch a little
authored
214 self.output_view.set_read_only(False)
b6c808d @kemayo "All file" in add menu
authored
215 self._output_to_view(self.output_view, output, clear=True, **kwargs)
03602f9 @kemayo Refactor scratch a little
authored
216 self.output_view.set_read_only(True)
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
217 self.get_window().run_command("show_panel", {"panel": "output.git"})
332a942 @kemayo Fall back to active_window if view.window() is None
authored
218
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
219 def quick_panel(self, *args, **kwargs):
220 self.get_window().show_quick_panel(*args, **kwargs)
221
222
223 # A base for all git commands that work with the entire repository
224 class GitWindowCommand(GitCommand, sublime_plugin.WindowCommand):
225 def active_view(self):
226 return self.window.active_view()
227
228 def _active_file_name(self):
229 view = self.active_view()
230 if view and view.file_name() and len(view.file_name()) > 0:
231 return view.file_name()
232
1f80050 @zerc fix encode error when writing tmp file with commit msg
zerc authored
233 @property
234 def fallback_encoding(self):
235 if self.active_view() and self.active_view().settings().get('fallback_encoding'):
236 return self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]
237
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
238 # If there's no active view or the active view is not a file on the
239 # filesystem (e.g. a search results view), we can infer the folder
240 # that the user intends Git commands to run against when there's only
241 # only one.
242 def is_enabled(self):
243 if self._active_file_name() or len(self.window.folders()) == 1:
244 return git_root(self.get_working_dir())
245
246 def get_file_name(self):
247 return ''
248
536612c @kemayo Split the relative file name stuff out into its own method
authored
249 def get_relative_file_name(self):
250 return ''
251
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
252 # If there is a file in the active view use that file's directory to
253 # search for the Git root. Otherwise, use the only folder that is
254 # open.
255 def get_working_dir(self):
256 file_name = self._active_file_name()
257 if file_name:
b6ab3c8 @sheldon fix symlink issues
sheldon authored
258 return os.path.realpath(os.path.dirname(file_name))
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
259 else:
e51aa66 @kemayo Periodic PEP8 sweep
authored
260 try: # handle case with no open folder
69d1240 @coaxmetal added handling for an IndexError exception (no open folder)
coaxmetal authored
261 return self.window.folders()[0]
50c1373 @coaxmetal changed handling on missing directory to return a blank, added a pref…
coaxmetal authored
262 except IndexError:
263 return ''
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
264
265 def get_window(self):
266 return self.window
267
268
269 # A base for all git commands that work with the file in the active view
270 class GitTextCommand(GitCommand, sublime_plugin.TextCommand):
271 def active_view(self):
272 return self.view
273
274 def is_enabled(self):
275 # First, is this actually a file on the file system?
276 if self.view.file_name() and len(self.view.file_name()) > 0:
277 return git_root(self.get_working_dir())
278
279 def get_file_name(self):
280 return os.path.basename(self.view.file_name())
281
536612c @kemayo Split the relative file name stuff out into its own method
authored
282 def get_relative_file_name(self):
283 working_dir = self.get_working_dir()
284 file_path = working_dir.replace(git_root(working_dir), '')[1:]
285 file_name = os.path.join(file_path, self.get_file_name())
286 return file_name.replace('\\', '/') # windows issues
287
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
288 def get_working_dir(self):
b6ab3c8 @sheldon fix symlink issues
sheldon authored
289 return os.path.realpath(os.path.dirname(self.view.file_name()))
50c30ee @misfo Repo-wide commands without an active file view
misfo authored
290
291 def get_window(self):
332a942 @kemayo Fall back to active_window if view.window() is None
authored
292 # Fun discovery: if you switch tabs while a command is working,
293 # self.view.window() is None. (Admittedly this is a consequence
294 # of my deciding to do async command processing... but, hey,
295 # got to live with that now.)
296 # I did try tracking the window used at the start of the command
297 # and using it instead of view.window() later, but that results
298 # panels on a non-visible window, which is especially useless in
299 # the case of the quick panel.
300 # So, this is not necessarily ideal, but it does work.
301 return self.view.window() or sublime.active_window()
302
4ea0996 @kemayo Initial commit
authored
303
d4d444b @kemayo Rearrange a lot of code
authored
304 # A few miscellaneous commands
3ddedf9 Adding "pull/push current branch" commands -properly this time
Can Yilmaz authored
305
306
a2f8dd1 @whitequark Make Git Custom Command accessible even without an active file.
whitequark authored
307 class GitCustomCommand(GitWindowCommand):
ab3a587 @kemayo Change how attempts to reload work
authored
308 may_change_files = True
309
41f6342 @Starli0n Improve Git Custom Command to accept several Git commands and parameter
Starli0n authored
310 def run(self, command=None):
311 if command is None:
312 self.get_window().show_input_panel("Git command", "",
ff42a5d @iurisilvio Git custom command added.
iurisilvio authored
313 self.on_input, None, None)
41f6342 @Starli0n Improve Git Custom Command to accept several Git commands and parameter
Starli0n authored
314 else:
315 self.on_input(command)
316
ff42a5d @iurisilvio Git custom command added.
iurisilvio authored
317
318 def on_input(self, command):
3d12d7d @kemayo Properly parse the results of ls-tree to handle files with spaces
authored
319 command = str(command) # avoiding unicode
ff42a5d @iurisilvio Git custom command added.
iurisilvio authored
320 if command.strip() == "":
321 self.panel("No git command provided")
322 return
323 import shlex
41f6342 @Starli0n Improve Git Custom Command to accept several Git commands and parameter
Starli0n authored
324 cmds = [c.strip() for c in command.split(';') if c.strip() != '']
325 for cmd in cmds:
326 command_splitted = ['git'] + shlex.split(cmd)
327 print command_splitted
328 self.run_command(command_splitted)
4e17f8b @kemayo I have clear stylistic preferences, okay
authored
329
e51aa66 @kemayo Periodic PEP8 sweep
authored
330
19db703 @jdc0589 Added GitGui and GitGitk text commands. Added them to sublime-commands.
jdc0589 authored
331 class GitGuiCommand(GitTextCommand):
332 def run(self, edit):
333 command = ['git', 'gui']
334 self.run_command(command)
335
336
337 class GitGitkCommand(GitTextCommand):
338 def run(self, edit):
339 command = ['gitk']
340 self.run_command(command)
63ebb24 Added "gitk --all" command
Phil Tsarik authored
341
342 class GitGitkAllCommand(GitTextCommand):
343 def run(self, edit):
344 command = ['gitk', '--all']
345 self.run_command(command)
14da7da @ktaragorn Add command and code to add "Gitk this file" command to sublime-text-git
ktaragorn authored
346
347 class GitGitkThisFileCommand(GitTextCommand):
348 def run(self, edit):
349 command = ['gitk', self.get_file_name()]
f5269ee @bcomnes Work around for OSX 10.10 PATH issues
bcomnes authored
350 self.run_command(command)
Something went wrong with that request. Please try again.