├── .gitignore ├── Context.sublime-menu ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── GitCommand.py ├── Main.sublime-menu └── README.rst /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "-" }, 3 | { 4 | "caption": "Git", 5 | "children": 6 | [ 7 | { "caption": "Commit...", "command": "git_commit" }, 8 | { "caption": "Commit All...", "command": "git_commit_all" }, 9 | { "caption": "Reset", "command": "git_reset" }, 10 | { "caption": "-" }, 11 | { "caption": "Checkout...", "command": "git_checkout" }, 12 | { "caption": "Add", "command": "git_add" }, 13 | { "caption": "Remove", "command": "git_rm" }, 14 | { "caption": "Move...", "command": "git_mv" }, 15 | { "caption": "-" }, 16 | { "caption": "Diff", "command": "git_diff" }, 17 | { "caption": "Log", "command": "git_log" }, 18 | { "caption": "Status", "command": "git_status" }, 19 | { "caption": "Blame", "command": "git_blame" }, 20 | { "caption": "Fetch", "command": "git_fetch" }, 21 | { "caption": "Pull", "command": "git_pull" }, 22 | { "caption": "Push", "command": "git_push" } 23 | ] 24 | } 25 | ] -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+super+c"], "command": "git_commit"}, 3 | { "keys": ["ctrl+super+a"], "command": "git_add"}, 4 | { "keys": ["ctrl+super+t"], "command": "git_reset"}, 5 | { "keys": ["ctrl+super+r"], "command": "git_rm"}, 6 | { "keys": ["ctrl+super+d"], "command": "git_diff"}, 7 | { "keys": ["ctrl+super+s"], "command": "git_status"}, 8 | { "keys": ["ctrl+super+l"], "command": "git_log"}, 9 | { "keys": ["ctrl+super+b"], "command": "git_blame"} 10 | ] -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+super+c"], "command": "git_commit"}, 3 | { "keys": ["ctrl+super+a"], "command": "git_add"}, 4 | { "keys": ["ctrl+super+t"], "command": "git_reset"}, 5 | { "keys": ["ctrl+super+r"], "command": "git_rm"}, 6 | { "keys": ["ctrl+super+d"], "command": "git_diff"}, 7 | { "keys": ["ctrl+super+s"], "command": "git_status"}, 8 | { "keys": ["ctrl+super+l"], "command": "git_log"}, 9 | { "keys": ["ctrl+super+b"], "command": "git_blame"}, 10 | { "keys": ["ctrl+super+f"], "command": "git_fetch"}, 11 | { "keys": ["ctrl+super+p"], "command": "git_pull"}, 12 | { "keys": ["ctrl+super+["], "command": "git_push"} 13 | ] -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+super+c"], "command": "git_commit"}, 3 | { "keys": ["ctrl+super+a"], "command": "git_add"}, 4 | { "keys": ["ctrl+super+t"], "command": "git_reset"}, 5 | { "keys": ["ctrl+super+r"], "command": "git_rm"}, 6 | { "keys": ["ctrl+super+d"], "command": "git_diff"}, 7 | { "keys": ["ctrl+super+s"], "command": "git_status"}, 8 | { "keys": ["ctrl+super+l"], "command": "git_log"}, 9 | { "keys": ["ctrl+super+b"], "command": "git_blame"} 10 | ] -------------------------------------------------------------------------------- /GitCommand.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import os 3 | import subprocess 4 | import thread 5 | 6 | import sublime 7 | import sublime_plugin 8 | 9 | 10 | class ProcessListener(object): 11 | def on_data(self, proc, data): 12 | pass 13 | 14 | def on_finished(self, proc): 15 | pass 16 | 17 | 18 | class AsyncProcess(object): 19 | def __init__(self, arg_list, listener): 20 | self.listener = listener 21 | self.killed = False 22 | 23 | # Hide the console window on Windows 24 | startupinfo = None 25 | if os.name == "nt": 26 | startupinfo = subprocess.STARTUPINFO() 27 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 28 | 29 | proc_env = os.environ.copy() 30 | 31 | self.proc = subprocess.Popen(arg_list, stdout=subprocess.PIPE, 32 | stderr=subprocess.PIPE, startupinfo=startupinfo, env=proc_env) 33 | 34 | if self.proc.stdout: 35 | thread.start_new_thread(self.read_stdout, ()) 36 | 37 | if self.proc.stderr: 38 | thread.start_new_thread(self.read_stderr, ()) 39 | 40 | def kill(self): 41 | if not self.killed: 42 | self.killed = True 43 | self.proc.kill() 44 | self.listener = None 45 | 46 | def poll(self): 47 | return self.proc.poll() == None 48 | 49 | def read_stdout(self): 50 | while True: 51 | data = os.read(self.proc.stdout.fileno(), 2**15) 52 | 53 | if data != "": 54 | if self.listener: 55 | self.listener.on_data(self, data) 56 | else: 57 | self.proc.stdout.close() 58 | if self.listener: 59 | self.listener.on_finished(self) 60 | break 61 | 62 | def read_stderr(self): 63 | while True: 64 | data = os.read(self.proc.stderr.fileno(), 2**15) 65 | 66 | if data != "": 67 | if self.listener: 68 | self.listener.on_data(self, data) 69 | else: 70 | self.proc.stderr.close() 71 | break 72 | 73 | 74 | class GitAddCommand(sublime_plugin.TextCommand): 75 | def is_enabled(self, *args): 76 | if self.view.file_name(): 77 | return True 78 | return False 79 | 80 | def run(self, edit): 81 | if self.view.file_name(): 82 | folder_name, file_name = os.path.split(self.view.file_name()) 83 | 84 | print folder_name, file_name 85 | self.view.window().run_command('exec', {'cmd': ['git', 'add', file_name], 'working_dir': folder_name, 'quiet': True}) 86 | 87 | 88 | class GitCheckoutCommand(sublime_plugin.TextCommand): 89 | def run(self, edit, branch_or_path=''): 90 | if not branch_or_path: 91 | if self.view.file_name(): 92 | folder_name = os.path.dirname(self.view.file_name()) 93 | else: 94 | file_name = '' 95 | self.view.window().show_input_panel('Branch or Path:', file_name, self.on_done, None, None) 96 | else: 97 | self.on_done(branch_or_path) 98 | 99 | def on_done(self, branch_or_path): 100 | if self.view.file_name(): 101 | folder_name = os.path.dirname(self.view.file_name()) 102 | 103 | self.view.window().run_command('exec', {'cmd': ['git', 'checkout', branch_or_path], 'working_dir': folder_name, 'quiet': True}) 104 | self.view.run_command('revert') 105 | 106 | 107 | class GitCommitCommand(sublime_plugin.TextCommand): 108 | def run(self, edit, message='', all=False): 109 | if all: 110 | on_done = self.commit_all 111 | else: 112 | on_done = self.commit_one 113 | 114 | if not message: 115 | self.view.window().show_input_panel('Commit Message:', '', on_done, None, None) 116 | else: 117 | on_done(message) 118 | 119 | def commit_one(self, message): 120 | if self.view.file_name(): 121 | folder_name, file_name = os.path.split(self.view.file_name()) 122 | 123 | self.view.window().run_command('exec', {'cmd': ['git', 'commit', '-m', message, file_name], 'working_dir': folder_name, 'quiet': True}) 124 | 125 | def commit_all(self, message): 126 | if self.view.file_name(): 127 | folder_name = os.path.dirname(self.view.file_name()) 128 | 129 | self.view.window().run_command('exec', {'cmd': ['git', 'commit', '-am', message], 'working_dir': folder_name, 'quiet': True}) 130 | 131 | 132 | class GitDiffCommand(sublime_plugin.TextCommand, ProcessListener): 133 | def run(self, edit, encoding='utf-8', kill=False): 134 | if kill: 135 | if self.proc: 136 | self.proc.kill() 137 | self.proc = None 138 | return 139 | 140 | if self.view.file_name(): 141 | folder_name, file_name = os.path.split(self.view.file_name()) 142 | 143 | if not hasattr(self, 'output_view'): 144 | self.output_view = self.view.window().new_file() 145 | 146 | self.encoding = encoding 147 | self.proc = None 148 | 149 | self.output_view.set_scratch(True) 150 | self.output_view.set_name('%s.diff' % os.path.basename(file_name)) 151 | self.output_view.set_syntax_file('Packages/Diff/Diff.tmLanguage') 152 | 153 | os.chdir(folder_name) 154 | 155 | err_type = OSError 156 | if os.name == "nt": 157 | err_type = WindowsError 158 | 159 | try: 160 | self.proc = AsyncProcess(['git', 'diff'], self) 161 | except err_type as e: 162 | self.append_data(None, str(e) + '\n') 163 | 164 | def is_enabled(self, kill=False): 165 | if kill: 166 | return hasattr(self, 'proc') and self.proc and self.proc.poll() 167 | else: 168 | return True 169 | 170 | def append_data(self, proc, data): 171 | if proc != self.proc: 172 | # a second call to exec has been made before the first one 173 | # finished, ignore it instead of intermingling the output. 174 | if proc: 175 | proc.kill() 176 | return 177 | 178 | try: 179 | str = data.decode(self.encoding) 180 | except: 181 | str = '[Decode error - output not ' + self.encoding + ']' 182 | proc = None 183 | 184 | # Normalize newlines, Sublime Text always uses a single \n separator 185 | # in memory. 186 | str = str.replace('\r\n', '\n').replace('\r', '\n') 187 | 188 | selection_was_at_end = (len(self.output_view.sel()) == 1 189 | and self.output_view.sel()[0] 190 | == sublime.Region(self.output_view.size())) 191 | self.output_view.set_read_only(False) 192 | edit = self.output_view.begin_edit() 193 | self.output_view.insert(edit, self.output_view.size(), str) 194 | if selection_was_at_end: 195 | self.output_view.show(self.output_view.size()) 196 | self.output_view.end_edit(edit) 197 | self.output_view.set_read_only(True) 198 | 199 | def finish(self, proc): 200 | if proc != self.proc: 201 | return 202 | 203 | # Set the selection to the start, so that next_result will work as expected 204 | edit = self.output_view.begin_edit() 205 | self.output_view.sel().clear() 206 | self.output_view.sel().add(sublime.Region(0)) 207 | self.output_view.end_edit(edit) 208 | 209 | def on_data(self, proc, data): 210 | sublime.set_timeout(functools.partial(self.append_data, proc, data), 0) 211 | 212 | def on_finished(self, proc): 213 | sublime.set_timeout(functools.partial(self.finish, proc), 0) 214 | 215 | 216 | class GitInitCommand(sublime_plugin.TextCommand): 217 | def run(self, edit, folder_name=''): 218 | if not folder_name: 219 | if self.view.file_name(): 220 | folder_name = os.path.dirname(self.view.file_name()) 221 | self.view.window().show_input_panel('Folder:', folder_name, self.on_done, None, None) 222 | else: 223 | self.on_done(folder_name) 224 | 225 | def on_done(self, folder_name): 226 | self.view.window().run_command('exec', {'cmd': ['git', 'init'], 'working_dir': folder_name, 'quiet': True}) 227 | 228 | 229 | class GitLogCommand(sublime_plugin.TextCommand): 230 | def is_enabled(self, *args): 231 | if self.view.file_name(): 232 | return True 233 | return False 234 | 235 | def run(self, edit): 236 | if self.view.file_name(): 237 | folder_name, file_name = os.path.split(self.view.file_name()) 238 | 239 | self.view.window().run_command('exec', {'cmd': ['git', 'log', file_name], 'working_dir': folder_name, 'quiet': True}) 240 | 241 | 242 | class GitMvCommand(sublime_plugin.TextCommand): 243 | def run(self, edit, destination=''): 244 | if not destination: 245 | self.view.window().show_input_panel('Destination:', '', self.on_done, None, None) 246 | else: 247 | self.on_done(tag_name) 248 | 249 | def on_done(self, destination): 250 | if self.view.file_name(): 251 | folder_name, source = os.path.split(self.view.file_name()) 252 | 253 | self.view.window().run_command('exec', {'cmd': ['git', 'mv', source, destination], 'working_dir': folder_name, 'quiet': True}) 254 | 255 | 256 | class GitResetCommand(sublime_plugin.TextCommand): 257 | def is_enabled(self, *args): 258 | if self.view.file_name(): 259 | return True 260 | return False 261 | 262 | def run(self, edit, mode='--', commit='HEAD'): 263 | if self.view.file_name(): 264 | folder_name, file_name = os.path.split(self.view.file_name()) 265 | 266 | self.view.window().run_command('exec', {'cmd': ['git', 'reset', mode, commit, file_name], 'working_dir': folder_name, 'quiet': True}) 267 | 268 | 269 | class GitRmCommand(sublime_plugin.TextCommand): 270 | def is_enabled(self, *args): 271 | if self.view.file_name(): 272 | return True 273 | return False 274 | 275 | def run(self, edit): 276 | if self.view.file_name(): 277 | folder_name, file_name = os.path.split(self.view.file_name()) 278 | 279 | self.view.window().run_command('exec', {'cmd': ['git', 'rm', file_name], 'working_dir': folder_name, 'quiet': True}) 280 | 281 | 282 | class GitStatusCommand(sublime_plugin.TextCommand): 283 | def is_enabled(self, *args): 284 | if self.view.file_name(): 285 | return True 286 | return False 287 | 288 | def run(self, edit): 289 | if self.view.file_name(): 290 | folder_name = os.path.dirname(self.view.file_name()) 291 | 292 | self.view.window().run_command('exec', {'cmd': ['git', 'status'], 'working_dir': folder_name, 'quiet': True}) 293 | 294 | 295 | class GitBlameCommand(sublime_plugin.TextCommand): 296 | def is_enabled(self, *args): 297 | if self.view.file_name(): 298 | return True 299 | return False 300 | 301 | def run(self, edit): 302 | if self.view.file_name(): 303 | folder_name, file_name = os.path.split(self.view.file_name()) 304 | 305 | self.view.window().run_command('exec', {'cmd': ['git', 'blame', file_name], 'working_dir': folder_name, 'quiet': True}) 306 | 307 | 308 | class GitBlameCommand(sublime_plugin.TextCommand, ProcessListener): 309 | def run(self, edit, encoding='utf-8', kill=False): 310 | if kill: 311 | if self.proc: 312 | self.proc.kill() 313 | self.proc = None 314 | return 315 | 316 | if self.view.file_name(): 317 | folder_name, file_name = os.path.split(self.view.file_name()) 318 | 319 | if not hasattr(self, 'output_view'): 320 | self.output_view = self.view.window().new_file() 321 | 322 | self.encoding = encoding 323 | self.proc = None 324 | 325 | self.output_view.set_scratch(True) 326 | 327 | os.chdir(folder_name) 328 | 329 | err_type = OSError 330 | if os.name == "nt": 331 | err_type = WindowsError 332 | 333 | try: 334 | self.proc = AsyncProcess(['git', 'blame', file_name], self) 335 | except err_type as e: 336 | self.append_data(None, str(e) + '\n') 337 | 338 | def is_enabled(self, kill=False): 339 | if kill: 340 | return hasattr(self, 'proc') and self.proc and self.proc.poll() 341 | else: 342 | return True 343 | 344 | def append_data(self, proc, data): 345 | if proc != self.proc: 346 | # a second call to exec has been made before the first one 347 | # finished, ignore it instead of intermingling the output. 348 | if proc: 349 | proc.kill() 350 | return 351 | 352 | try: 353 | str = data.decode(self.encoding) 354 | except: 355 | str = '[Decode error - output not ' + self.encoding + ']' 356 | proc = None 357 | 358 | # Normalize newlines, Sublime Text always uses a single \n separator 359 | # in memory. 360 | str = str.replace('\r\n', '\n').replace('\r', '\n') 361 | 362 | selection_was_at_end = (len(self.output_view.sel()) == 1 363 | and self.output_view.sel()[0] 364 | == sublime.Region(self.output_view.size())) 365 | self.output_view.set_read_only(False) 366 | edit = self.output_view.begin_edit() 367 | self.output_view.insert(edit, self.output_view.size(), str) 368 | if selection_was_at_end: 369 | self.output_view.show(self.output_view.size()) 370 | self.output_view.end_edit(edit) 371 | self.output_view.set_read_only(True) 372 | 373 | def finish(self, proc): 374 | if proc != self.proc: 375 | return 376 | 377 | # Set the selection to the start, so that next_result will work as expected 378 | edit = self.output_view.begin_edit() 379 | self.output_view.sel().clear() 380 | self.output_view.sel().add(sublime.Region(0)) 381 | self.output_view.end_edit(edit) 382 | 383 | def on_data(self, proc, data): 384 | sublime.set_timeout(functools.partial(self.append_data, proc, data), 0) 385 | 386 | def on_finished(self, proc): 387 | sublime.set_timeout(functools.partial(self.finish, proc), 0) 388 | 389 | 390 | class GitTagCommand(sublime_plugin.TextCommand): 391 | def run(self, edit, tag_name=''): 392 | if not tag_name: 393 | self.view.window().show_input_panel('Tag Name:', '', self.on_done, None, None) 394 | else: 395 | self.on_done(tag_name) 396 | 397 | def on_done(self, tag_name): 398 | if self.view.file_name(): 399 | folder_name = os.path.dirname(self.view.file_name()) 400 | 401 | self.view.window().run_command('exec', {'cmd': ['git', 'tag', tag_name], 'working_dir': folder_name, 'quiet': True}) 402 | 403 | class GitFetchCommand(sublime_plugin.TextCommand): 404 | def is_enabled(self, *args): 405 | if self.view.file_name(): 406 | return True 407 | return False 408 | 409 | def run(self, edit): 410 | if self.view.file_name(): 411 | folder_name = os.path.dirname(self.view.file_name()) 412 | 413 | self.view.window().run_command('exec', {'cmd': ['git', 'fetch'], 'working_dir': folder_name, 'quiet': False}) 414 | 415 | class GitPullCommand(sublime_plugin.TextCommand): 416 | def is_enabled(self, *args): 417 | if self.view.file_name(): 418 | return True 419 | return False 420 | 421 | def run(self, edit): 422 | if self.view.file_name(): 423 | folder_name = os.path.dirname(sself.view.file_name()) 424 | 425 | self.view.window().run_command('exec', {'cmd': ['git', 'pull'], 'working_dir': folder_name, 'quiet': False}) 426 | 427 | class GitPushCommand(sublime_plugin.TextCommand): 428 | def is_enabled(self, *args): 429 | if self.view.file_name(): 430 | return True 431 | return False 432 | 433 | def run(self, edit): 434 | if self.view.file_name(): 435 | folder_name = os.path.dirname(self.view.file_name()) 436 | 437 | self.view.window().run_command('exec', {'cmd': ['git', 'push'], 'working_dir': folder_name, 'quiet': False}) -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "tools", 4 | "children": 5 | [ 6 | { 7 | "caption": "Git", 8 | "children": 9 | [ 10 | { "caption": "Commit...", "command": "git_commit" }, 11 | { "caption": "Commit All...", "command": "git_commit_all" }, 12 | { "caption": "Reset", "command": "git_reset" }, 13 | { "caption": "-" }, 14 | { "caption": "Checkout...", "command": "git_checkout" }, 15 | { "caption": "Add", "command": "git_add" }, 16 | { "caption": "Remove", "command": "git_rm" }, 17 | { "caption": "Move...", "command": "git_mv" }, 18 | { "caption": "-" }, 19 | { "caption": "Diff", "command": "git_diff" }, 20 | { "caption": "Log", "command": "git_log" }, 21 | { "caption": "Status", "command": "git_status" }, 22 | { "caption": "Blame", "command": "git_blame" }, 23 | { "caption": "-" }, 24 | { "caption": "Pull...", "command": "git_pull" }, 25 | { "caption": "Fetch...", "command": "git_fetch" }, 26 | { "caption": "Push...", "command": "git_push" }, 27 | { "caption": "Rebase...", "command": "git_rebase" }, 28 | { "caption": "-" }, 29 | { "caption": "Tag...", "command": "git_tag" }, 30 | { "caption": "-" }, 31 | { 32 | "caption": "Stash", 33 | "children": 34 | [ 35 | { "caption": "Save", "command": "git_stash_save" }, 36 | { "caption": "Apply", "command": "git_stash_apply" }, 37 | { "caption": "Pop", "command": "git_stash_pop" }, 38 | { "caption": "List", "command": "git_stash_list" }, 39 | { "caption": "Clear", "command": "git_stash_clear" } 40 | ] 41 | }, 42 | { "caption": "-" }, 43 | { "caption": "Init...", "command": "git_init" }, 44 | { "caption": "-" }, 45 | { "caption": "Settings...", "command": "git_settings" }, 46 | { "caption": "Fetch", "command": "git_fetch" }, 47 | { "caption": "Pull", "command": "git_pull" }, 48 | { "caption": "Push", "command": "git_push" } 49 | ] 50 | } 51 | ] 52 | } 53 | ] -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================================= 2 | GitST2 -- Git support in Sublime Text 2 3 | ======================================= 4 | 5 | THIS PROJECT IS NO LONGER IN DEVELOPMENT. - Sublime Text 3 includes built-in support for Git: https://www.sublimetext.com/docs/3/git_integration.html 6 | 7 | Overview 8 | -------- 9 | This plugin adds basic Git support to Sublime Text 2 through a collection of commands, menus and shortcut keys. 10 | 11 | Installation 12 | ------------ 13 | 1. Clone or copy this repository into: 14 | 15 | - OS X: ~/Library/Application Support/Sublime Text 2/Packages/ 16 | - Windows: %APPDATA%/Sublime Text 2/Packages/ 17 | - Linux: ~/.config/sublime-text-2/Packages/ 18 | 19 | Keymaps 20 | ------- 21 | Keymap are provided for OSX, Windows, and Linux, however, they have only been tested on OSX at this time. Feedback would be appreciated as to the usability of these key mappings on other operating systems. 22 | 23 | Note 24 | ---- 25 | This is a work in progress so expect bugs. Please open an issue at https://github.com/notanumber/gitst2 if you discover a problem or would like to see a feature/change implemented. 26 | --------------------------------------------------------------------------------