├── .editorconfig ├── .flake8 ├── .gitignore ├── .travis.yml ├── CONTRIBUTORS ├── Default.sublime-commands ├── Default.sublime-keymap ├── Git.sublime-settings ├── LICENSE ├── Main.sublime-menu ├── README.markdown ├── git ├── __init__.py ├── add.py ├── annotate.py ├── commit.py ├── config.py ├── core.py ├── diff.py ├── file.py ├── flow.py ├── history.py ├── ignore.py ├── index.py ├── repo.py ├── stash.py ├── status.py └── statusbar.py ├── git_commands.py └── syntax ├── Git Blame.JSON-tmLanguage ├── Git Blame.tmLanguage ├── Git Commit Message.JSON-tmLanguage ├── Git Commit Message.tmLanguage ├── Git Commit Message.tmPreferences ├── Git Commit View.tmLanguage ├── Git Diff.sublime-syntax ├── Git Graph.JSON-tmLanguage └── Git Graph.tmLanguage /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.py] 9 | indent_style = space 10 | indent_size = 4 11 | charset = utf-8 12 | 13 | [{package.json,.travis.yml}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # E128 continuation line under-indented for visual indent 4 | E501 line too long 5 | W503 line break before binary operator 6 | exclude = .git,__pycache__,syntax 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.tmLanguage.cache 3 | *.tmPreferences.cache 4 | .DS_Store 5 | package-metadata.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.8" 5 | 6 | install: 7 | - pip install flake8 8 | 9 | script: 10 | - flake8 . 11 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | These are the people who helped make this plugin: 2 | 3 | David Lynch 4 | Sheldon Els 5 | Nick Fisher 6 | Can Yilmaz 7 | Stefan Buhrmester 8 | Rafal Chlodnicki 9 | Daniël de Kok 10 | David Baumgold 11 | Iuri de Silvio 12 | joshuacc 13 | misfo 14 | Kevin Smith 15 | Κώστας Καραχάλιος 16 | Dominique Wahli 17 | Fraser Graham 18 | Hamid Nazari 19 | Jeff Sandberg 20 | Joshua Clanton 21 | Maxim Sukharev 22 | Niklas Hambüchen 23 | Patrik Ring 24 | Scott Bowers 25 | Weslly Honorato 26 | brcooley 27 | jdc0589 28 | Adam Venturella 29 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "Git: Init", 3 | "command": "git_init" 4 | } 5 | ,{ 6 | "caption": "Git: Blame", 7 | "command": "git_blame" 8 | } 9 | ,{ 10 | "caption": "Git: Document Selection", 11 | "command": "git_document" 12 | } 13 | ,{ 14 | "caption": "Git: New Tag", 15 | "command": "git_new_tag" 16 | } 17 | ,{ 18 | "caption": "Git: Delete Tag", 19 | "command": "git_delete_tag" 20 | } 21 | ,{ 22 | "caption": "Git: Show Tags", 23 | "command": "git_show_tags" 24 | } 25 | ,{ 26 | "caption": "Git: Push Tags", 27 | "command": "git_raw", "args": { "command": "git push --tags", "may_change_files": false } 28 | } 29 | ,{ 30 | "caption": "Git: Checkout Tag", 31 | "command": "git_checkout_tag" 32 | } 33 | ,{ 34 | "caption": "Git: Log Current File", 35 | "command": "git_log" 36 | } 37 | ,{ 38 | "caption": "Git: Log All", 39 | "command": "git_log_all" 40 | } 41 | ,{ 42 | "caption": "Git: Graph Current File", 43 | "command": "git_graph" 44 | } 45 | ,{ 46 | "caption": "Git: Graph All", 47 | "command": "git_graph_all" 48 | } 49 | ,{ 50 | "caption": "Git: View selected commits", 51 | "command": "git_goto_commit" 52 | } 53 | ,{ 54 | "caption": "Git: Diff Current File", 55 | "command": "git_diff" 56 | } 57 | ,{ 58 | "caption": "Git: Diff All Files", 59 | "command": "git_diff_all" 60 | } 61 | ,{ 62 | "caption": "Git: Diff Staged Files", 63 | "command": "git_diff_commit" 64 | } 65 | ,{ 66 | "caption": "Git: Diff Current File (Words)", 67 | "command": "git_diff", 68 | "args": { "word_diff": true } 69 | } 70 | ,{ 71 | "caption": "Git: Diff All Files (Words)", 72 | "command": "git_diff_all", 73 | "args": { "word_diff": true } 74 | } 75 | ,{ 76 | "caption": "Git: Diff Staged Files (Words)", 77 | "command": "git_diff_commit", 78 | "args": { "word_diff": true } 79 | } 80 | ,{ 81 | "caption": "Git: Diff Current File (Ignore Whitespace)", 82 | "command": "git_diff", 83 | "args": { "ignore_whitespace": true } 84 | } 85 | ,{ 86 | "caption": "Git: Diff All Files (Ignore Whitespace)", 87 | "command": "git_diff_all", 88 | "args": { "ignore_whitespace": true } 89 | } 90 | ,{ 91 | "caption": "Git: Diff Staged Files (Ignore Whitespace)", 92 | "command": "git_diff_commit", 93 | "args": { "ignore_whitespace": true } 94 | } 95 | ,{ 96 | "caption": "Git: Diff Tool Current File", 97 | "command": "git_raw", "args": { "command": "git difftool", "append_current_file": true, "may_change_files": false } 98 | } 99 | ,{ 100 | "caption": "Git: Diff Tool All", 101 | "command": "git_raw", "args": { "command": "git difftool", "may_change_files": false } 102 | } 103 | ,{ 104 | "caption": "Git: Diff Tool Current File Staged", 105 | "command": "git_diff_tool_commit" 106 | } 107 | ,{ 108 | "caption": "Git: Diff Tool Staged", 109 | "command": "git_diff_tool_commit_all" 110 | } 111 | ,{ 112 | "caption": "Git: Commit", 113 | "command": "git_commit" 114 | } 115 | ,{ 116 | "caption": "Git: Amend Commit", 117 | "command": "git_commit_amend" 118 | } 119 | ,{ 120 | "caption": "Git: Quick Commit (current file)", 121 | "command": "git_quick_commit" 122 | } 123 | ,{ 124 | "caption": "Git: Quick Commit (repo)", 125 | "command": "git_quick_commit", "args": { "target": "*" } 126 | } 127 | ,{ 128 | "caption": "Git: Quick Commit (repo, only already added files)", 129 | "command": "git_quick_commit", "args": { "target": false } 130 | } 131 | ,{ 132 | "caption": "Git: Status", 133 | "command": "git_status" 134 | } 135 | ,{ 136 | "caption": "Git: Open Modified Files", 137 | "command": "git_open_modified_files" 138 | } 139 | ,{ 140 | "caption": "Git: New Branch", 141 | "command": "git_new_branch" 142 | } 143 | ,{ 144 | "caption": "Git: Change Branch", 145 | "command": "git_branch" 146 | } 147 | ,{ 148 | "caption": "Git: Checkout Remote Tracking Branch", 149 | "command": "git_track_remote_branch" 150 | } 151 | ,{ 152 | "caption": "Git: Set Upstream Tracking Branch", 153 | "command": "git_set_upstream_branch" 154 | } 155 | ,{ 156 | "caption": "Git: Merge Branch", 157 | "command": "git_merge" 158 | } 159 | ,{ 160 | "caption": "Git: Delete Branch", 161 | "command": "git_delete_branch" 162 | } 163 | ,{ 164 | "caption": "Git: Delete Branch (Force)", 165 | "command": "git_force_delete_branch" 166 | } 167 | ,{ 168 | "caption": "Git: Stash Changes", 169 | "command": "git_raw", "args": { "command": "git stash" } 170 | } 171 | ,{ 172 | "caption": "Git: Stash Pop", 173 | "command": "git_raw", "args": { "command": "git stash pop" } 174 | } 175 | ,{ 176 | "caption": "Git: Stash Apply", 177 | "command": "git_stash_apply" 178 | } 179 | ,{ 180 | "caption": "Git: Stash Drop", 181 | "command": "git_stash_drop" 182 | } 183 | ,{ 184 | "caption": "Git: Stash List", 185 | "command": "git_stash_list" 186 | } 187 | ,{ 188 | "caption": "Git: Add Current File", 189 | "command": "git_raw", "args": { "command": "git add", "append_current_file": true } 190 | } 191 | ,{ 192 | "caption": "Git: Add...", 193 | "command": "git_add_choice" 194 | } 195 | ,{ 196 | "caption": "Git: Add All", 197 | "command": "git_raw", "args": { "command": "git add -A" } 198 | } 199 | ,{ 200 | "caption": "Git: Checkout Current File", 201 | "command": "git_raw", "args": { "command": "git checkout", "append_current_file": true } 202 | } 203 | ,{ 204 | "caption": "Git: Fetch", 205 | "command": "git_raw", "args": { "command": "git fetch", "may_change_files": false } 206 | } 207 | ,{ 208 | "caption": "Git: Pull", 209 | "command": "git_raw", "args": { "command": "git pull" } 210 | } 211 | ,{ 212 | "caption": "Git: Pull Using Rebase", 213 | "command": "git_raw", "args": { "command": "git pull --rebase" } 214 | } 215 | ,{ 216 | "caption": "Git: Pull Current Branch", 217 | "command": "git_pull_current_branch" 218 | } 219 | ,{ 220 | "caption": "Git: Push", 221 | "command": "git_raw", "args": { "command": "git push", "may_change_files": false } 222 | } 223 | ,{ 224 | "caption": "Git: Push Current Branch", 225 | "command": "git_push_current_branch" 226 | } 227 | ,{ 228 | "caption": "Git: Show Previous Version of Current File", 229 | "command": "git_show" 230 | } 231 | ,{ 232 | "caption": "Git: Show Commit By Hash", 233 | "command": "git_show_commit" 234 | } 235 | ,{ 236 | "caption": "Git: Toggle Annotations", 237 | "command": "git_toggle_annotations" 238 | } 239 | ,{ 240 | "caption": "Git: Custom Command", 241 | "command": "git_custom" 242 | } 243 | ,{ 244 | "caption": "Git Flow: Feature Start", 245 | "command": "git_flow_feature_start" 246 | } 247 | ,{ 248 | "caption": "Git Flow: Feature Finish", 249 | "command": "git_flow_feature_finish" 250 | } 251 | ,{ 252 | "caption": "Git Flow: Release Start", 253 | "command": "git_flow_release_start" 254 | } 255 | ,{ 256 | "caption": "Git Flow: Release Finish", 257 | "command": "git_flow_release_finish" 258 | } 259 | ,{ 260 | "caption": "Git Flow: Hotfix Start", 261 | "command": "git_flow_hotfix_start" 262 | } 263 | ,{ 264 | "caption": "Git Flow: Hotfix Finish", 265 | "command": "git_flow_hotfix_finish" 266 | } 267 | ,{ 268 | "caption": "Git: Open...", 269 | "command": "git_open_file" 270 | } 271 | ,{ 272 | "caption": "Git: Reset (unstage) Current File", 273 | "command": "git_raw", "args": { "command": "git reset HEAD", "append_current_file": true, "show_in": "suppress" } 274 | } 275 | ,{ 276 | "caption": "Git: Reset (unstage) All", 277 | "command": "git_raw", "args": { "command": "git reset HEAD", "show_in": "suppress" } 278 | } 279 | ,{ 280 | "caption": "Git: Reset (hard) HEAD", 281 | "command": "git_reset_hard_head" 282 | } 283 | ,{ 284 | "caption": "Git: Add Selected Hunk", 285 | "command": "git_add_selected_hunk" 286 | } 287 | ,{ 288 | "caption": "Git: Commit Selected Hunk", 289 | "command": "git_commit_selected_hunk" 290 | } 291 | ,{ 292 | "caption": "Git: Gui", 293 | "command": "git_gui" 294 | } 295 | ,{ 296 | "caption": "Git: Gitk This File", 297 | "command": "git_gitk_this_file" 298 | } 299 | ,{ 300 | "caption": "Git: Gitk", 301 | "command": "git_gitk" 302 | } 303 | ,{ 304 | "caption": "Git: Gitk All", 305 | "command": "git_gitk_all" 306 | } 307 | ,{ 308 | "caption": "Git: Commit history", 309 | "command": "git_commit_history" 310 | } 311 | ,{ 312 | "caption": "Git: Update Project Ignored Files", 313 | "command": "git_update_ignore" 314 | } 315 | ,{ 316 | "caption": "Git: Move Current File", 317 | "command": "git_file_move" 318 | } 319 | ,{ 320 | "caption": "Git: Rename / Move Current File", 321 | "command": "git_file_move", "args": { "rename": true } 322 | } 323 | ,{ 324 | "caption": "Git: Remove / Delete Current File", 325 | "command": "git_raw", "args": { "command": "git rm", "append_current_file": true } 326 | } 327 | ,{ 328 | "caption": "Git: Assume Unchanged", 329 | "command": "git_update_index_assume_unchanged" 330 | } 331 | ,{ 332 | "caption": "Git: No Assume Unchanged", 333 | "command": "git_update_index_no_assume_unchanged" 334 | } 335 | ,{ 336 | "caption": "Git: Open Local Config", 337 | "command": "git_open_config_file" 338 | } 339 | ,{ 340 | "caption": "Git: Open Url", 341 | "command": "git_open_config_url", "args": { "url_param": "remote.origin.url" } 342 | } 343 | ] 344 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["enter"], "command": "git_goto_diff", 3 | "context": [{"key": "selector", "operand": "markup.inserted.diff"}]}, 4 | {"keys": ["enter"], "command": "git_goto_diff", 5 | "context": [{"key": "selector", "operand": "markup.deleted.diff"}]}, 6 | {"keys": ["enter"], "command": "git_goto_commit", 7 | "context": [{"key": "selector", "operand": "text.git-blame"}]}, 8 | {"keys": ["enter"], "command": "git_goto_commit", 9 | "context": [{"key": "selector", "operand": "text.git-graph"}]} 10 | ] 11 | -------------------------------------------------------------------------------- /Git.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // save before running commands 3 | "save_first": true 4 | 5 | // if present, use this command instead of plain "git" 6 | // e.g. "/Users/kemayo/bin/git" or "C:\bin\git.exe" 7 | ,"git_command": false 8 | 9 | // if present, use this command instead of plain "gitk" 10 | // e.g. "/Users/kemayo/bin/gitk" or "C:\bin\gitk.exe" 11 | ,"gitk_command": false 12 | 13 | // point this the installation location of git-flow 14 | ,"git_flow_command": "/usr/local/bin/git-flow" 15 | 16 | // use the panel for diff output, rather than a new scratch window (new tab) 17 | ,"diff_panel": false 18 | 19 | // If you'd rather have your status command open files instead of show you a 20 | // diff, set this to true. You can still do `Git: Status` followed by 21 | // 'Git: Diff Current File' to get a file diff 22 | ,"status_opens_file": false 23 | 24 | // Use --verbose flag for commit messages 25 | ,"verbose_commits": true 26 | 27 | // How many commit messages to store in the history. Set to 0 to disable. 28 | ,"history_size": 5 29 | 30 | // Show git flow commands 31 | ,"flow": false 32 | 33 | // By default git flow release and hotfix will tag a version. Set to true to disable. 34 | ,"flow-notag": false 35 | 36 | // Annotations default to being on for all files. Can be slow in some cases. 37 | ,"annotations": false 38 | 39 | // statusbar 40 | ,"statusbar_branch": true 41 | // Symbols for quick git status in status bar 42 | ,"statusbar_status": true 43 | ,"statusbar_status_symbols" : {"modified": "≠", "added": "+", "deleted": "×", "untracked": "?", "conflicts": "‼", "renamed":"R", "copied":"C", "clean": "✓", "separator": " "} 44 | 45 | // e.g. "Packages/Git/syntax/Git Commit Message.tmLanguage" 46 | ,"diff_syntax": "Packages/Git/syntax/Git Diff.sublime-syntax" 47 | 48 | // Rulers for commit view 49 | ,"commit_rulers": [70] 50 | 51 | // Watch for gitignore changes? 52 | // When found, import them. This will hide the ignored files from the sidebar. 53 | ,"gitignore_sync": false 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Lynch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "tools", 4 | "children": 5 | [ 6 | { 7 | "caption": "Git", 8 | "children": 9 | [ 10 | { 11 | "caption": "This file", 12 | "children": 13 | [ 14 | { "caption": "Log", "command": "git_log" } 15 | ,{ "caption": "Graph", "command": "git_graph" } 16 | ,{ "caption": "-" } 17 | ,{ "caption": "Diff", "command": "git_diff" } 18 | ,{ "caption": "Diff (word diff)", "command": "git_diff", "args": { "word_diff": true } } 19 | ,{ "caption": "Diff (no whitespace)", "command": "git_diff", "args": { "ignore_whitespace": true } } 20 | ,{ "caption": "DiffTool", "command": "git_raw", "args": { "command": "git difftool", "append_current_file": true, "may_change_files": false } } 21 | ,{ "caption": "-" } 22 | ,{ "caption": "Add", "command": "git_raw", "args": { "command": "git add", "append_current_file": true } } 23 | ,{ "caption": "Add Selected Hunk", "command": "git_add_selected_hunk" } 24 | ,{ "caption": "-" } 25 | ,{ "caption": "Move/Rename...", "command": "git_mv"} 26 | ,{ "caption": "Remove/Delete", "command": "git_raw", "args": { "command": "git rm", "append_current_file": true } } 27 | ,{ "caption": "-" } 28 | ,{ "caption": "Reset", "command": "git_raw", "args": { "command": "git reset HEAD", "append_current_file": true, "show_in": "suppress" } } 29 | ,{ "caption": "Checkout (Discard Changes)", "command": "git_raw", "args": { "command": "git checkout", "append_current_file": true } } 30 | ,{ "caption": "-" } 31 | ,{ "caption": "Quick Commit Current File", "command": "git_quick_commit" } 32 | ,{ "caption": "Commit Selected Hunk", "command": "git_commit_selected_hunk" } 33 | ,{ "caption": "-" } 34 | ,{ "caption": "Blame", "command": "git_blame" } 35 | ,{ "caption": "-" } 36 | ,{ "caption": "Toggle Annotations", "command": "git_toggle_annotations" } 37 | ] 38 | } 39 | ,{ 40 | "caption": "Whole repo", 41 | "children": 42 | [ 43 | { "caption": "Log", "command": "git_log_all" } 44 | ,{ "caption": "Graph", "command": "git_graph_all" } 45 | ,{ "caption": "-" } 46 | ,{ "caption": "Diff", "command": "git_diff_all" } 47 | ,{ "caption": "Diff (word diff)", "command": "git_diff_all", "args": { "word_diff": true } } 48 | ,{ "caption": "Diff (no whitespace)", "command": "git_diff_all", "args": { "ignore_whitespace": true } } 49 | ,{ "caption": "Diff Staged", "command": "git_diff_commit" } 50 | ,{ "caption": "Diff Staged (word diff)", "command": "git_diff_commit", "args": { "word_diff": true } } 51 | ,{ "caption": "Diff Staged (no whitespace)", "command": "git_diff_commit", "args": { "ignore_whitespace": true } } 52 | ,{ "caption": "Diff Tool", "command": "git_raw", "args": { "command": "git difftool", "may_change_files": false } } 53 | ,{ "caption": "Reset Hard", "command": "git_reset_hard_head" } 54 | ,{ "caption": "-" } 55 | ,{ "caption": "Add...", "command": "git_add_choice" } 56 | ,{ "caption": "-" } 57 | ,{ "caption": "Reset", "command": "git_raw", "args": { "command": "git reset HEAD", "show_in": "suppress" } } 58 | ,{ "caption": "-" } 59 | ,{ "caption": "Commit", "command": "git_commit" } 60 | ,{ "caption": "Amend Last Commit", "command": "git_commit_amend" } 61 | ,{ "caption": "-" } 62 | ,{ "caption": "Open...", "command": "git_open_file" } 63 | ,{ "caption": "Open Config", "command": "git_open_config_file" } 64 | ,{ "caption": "Open Url", "command": "git_open_config_url", "args": { "url_param": "remote.origin.url" } } 65 | ] 66 | } 67 | ,{ 68 | "caption": "Stash", 69 | "children": 70 | [ 71 | { "caption": "Save", "command": "git_raw", "args": { "command": "git stash", "may_change_files": true } } 72 | ,{ "caption": "Pop", "command": "git_raw", "args": { "command": "git stash pop", "may_change_files": true } } 73 | ,{ "caption": "Apply", "command": "git_stash_apply" } 74 | ,{ "caption": "Drop", "command": "git_stash_drop" } 75 | ,{ "caption": "List", "command": "git_stash_list" } 76 | ] 77 | } 78 | ,{ "caption": "-" } 79 | ,{ 80 | "caption": "Flow", 81 | "children": 82 | [ 83 | { "caption": "Feature Start", "command": "git_flow_feature_start"} 84 | ,{ "caption": "Feature Finish", "command": "git_flow_feature_finish"} 85 | ,{ "caption": "-"} 86 | ,{ "caption": "Release Start", "command": "git_flow_release_start"} 87 | ,{ "caption": "Release Finish", "command": "git_flow_release_finish"} 88 | ,{ "caption": "-"} 89 | ,{ "caption": "Hotfix Start", "command": "git_flow_hotfix_start"} 90 | ,{ "caption": "Hotfix Finish", "command": "git_flow_hotfix_finish"} 91 | ] 92 | } 93 | ,{ "caption": "-" } 94 | ,{ "caption": "Init", "command": "git_init"} 95 | ,{ "caption": "Status...", "command": "git_status" } 96 | ,{ "caption": "Branches...", "command": "git_branch" } 97 | ,{ "caption": "Merge...", "command": "git_merge" } 98 | ,{ "caption": "See commit history...", "command": "git_commit_history"} 99 | ] 100 | } 101 | ] 102 | } 103 | 104 | ,{ 105 | "caption": "Preferences", 106 | "mnemonic": "n", 107 | "id": "preferences", 108 | "children": 109 | [ 110 | { 111 | "caption": "Package Settings", 112 | "mnemonic": "P", 113 | "id": "package-settings", 114 | "children": 115 | [ 116 | { 117 | "caption": "Git", 118 | "children": 119 | [ 120 | { 121 | "command": "open_file", 122 | "args": {"file": "${packages}/Git/Git.sublime-settings"}, 123 | "caption": "Settings – Default" 124 | }, 125 | { 126 | "command": "open_file", 127 | "args": {"file": "${packages}/User/Git.sublime-settings"}, 128 | "caption": "Settings – User" 129 | }, 130 | { "caption": "-" } 131 | ] 132 | } 133 | ] 134 | } 135 | ] 136 | } 137 | ] 138 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/kemayo/sublime-text-git.svg?branch=master)](https://travis-ci.org/kemayo/sublime-text-git) 2 | # Sublime Text plugin: git 3 | 4 | Git integration: it's pretty handy. Who knew, right? 5 | 6 | For more information about what's supported, and how to install this, [check the wiki](https://github.com/kemayo/sublime-text-git/wiki). 7 | 8 | ## Install 9 | 10 | ### Package Control 11 | 12 | The easiest way to install this is with [Package Control](http://wbond.net/sublime\_packages/package\_control). 13 | 14 | * If you just went and installed Package Control, you probably need to restart Sublime Text before doing this next bit. 15 | * Bring up the Command Palette (Command+Shift+p on OS X, Control+Shift+p on Linux/Windows). 16 | * Select "Package Control: Install Package" (it'll take a few seconds) 17 | * Select Git when the list appears. 18 | 19 | Package Control will automatically keep Git up to date with the latest version. 20 | 21 | ### Basic Usage 22 | 23 | * Bring up the Command Palette (Command+Shift+p on OS X, Control+Shift+p on Linux/Windows). 24 | * Start typing "Git" and select one of the recommended commands. 25 | 26 | ### The rest 27 | 28 | If you don't want to use Package Control, [check the wiki](https://github.com/kemayo/sublime-text-git/wiki) for other installation methods on various platforms. 29 | 30 | ## Troubleshooting 31 | 32 | This package works by running commands as your system `git`. As such, if you have problems with this package, first make sure that git is installed and configured correctly on your system. 33 | 34 | You may want to make sure that the `git` binary this plugin is using is the correct one, if you have multiple ones installed. Most git installation guides will be happy to walk you through configuring your system `$PATH` appropriately. 35 | 36 | If necessary, set the `git_command` plugin preference to tell us where to look. 37 | 38 | ### `fatal: unable to auto-detect email address` 39 | 40 | Git isn't configured properly. Tell it who you are, by opening a command prompt and doing this: 41 | 42 | git config --global user.email "you@example.com" 43 | git config --global user.name "Your Name" 44 | 45 | If you've done this and it's still complaining, you probably have multiple copies of git on your system which have different configuration locations, and the one which runs on your command line isn't the one which the shell `$PATH` exposes to Sublime Text. 46 | 47 | ### `fatal: could not read Username for 'https://github.com': Device not configured` 48 | 49 | Git isn't configured to use a system-level ssh-agent, and so it's asking you for a username and password when you try to push / pull. The plugin doesn't know how to ask you for this information. 50 | 51 | [Set up a ssh-agent](https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/#adding-your-ssh-key-to-the-ssh-agent) and this will stop happening. 52 | 53 | ## Acknowledgements 54 | 55 | This package contains: 56 | 57 | * [Sublime Text git Commit Message Syntax](https://github.com/adambullmer/sublime_git_commit_syntax) by [Adam Bullmer](https://github.com/adambullmer). 58 | -------------------------------------------------------------------------------- /git/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import os 4 | import re 5 | import sublime 6 | import sublime_plugin 7 | import threading 8 | import subprocess 9 | import functools 10 | import os.path 11 | import time 12 | 13 | 14 | git_root_cache = {} 15 | _has_warned = False 16 | 17 | 18 | # Goal is to get: "Packages/Git", allowing for people who rename things 19 | def find_plugin_directory(): 20 | if ".sublime-package" in __file__: 21 | # zipped package, all we care about is the bit right before .sublime-package 22 | match = re.search(r"([^\\/]+)\.sublime-package", __file__) 23 | if match: 24 | return "Packages/" + match.group(1) 25 | if __file__.startswith('./') or __file__.startswith('.\\'): 26 | # ST2, we get "./git/__init__.py" which is pretty useless since we want the part above that 27 | # However, os.getcwd() is the plugin directory! 28 | full = os.getcwd() 29 | else: 30 | # In a complete inversion, in ST3 when a plugin is loaded we 31 | # actually can trust __file__. It'll be something like: 32 | # /Users/dlynch/Library/Application Support/Sublime Text 3/Packages/Git/git/__init__.py 33 | full = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 34 | dirname = os.path.split(full)[-1] 35 | return "Packages/" + dirname.replace(".sublime-package", "") 36 | 37 | 38 | PLUGIN_DIRECTORY = find_plugin_directory() 39 | 40 | 41 | def main_thread(callback, *args, **kwargs): 42 | # sublime.set_timeout gets used to send things onto the main thread 43 | # most sublime.[something] calls need to be on the main thread 44 | sublime.set_timeout(functools.partial(callback, *args, **kwargs), 0) 45 | 46 | 47 | def open_url(url): 48 | sublime.active_window().run_command('open_url', {"url": url}) 49 | 50 | 51 | def git_root(directory): 52 | global git_root_cache 53 | 54 | retval = False 55 | leaf_dir = directory 56 | 57 | if leaf_dir in git_root_cache and git_root_cache[leaf_dir]['expires'] > time.time(): 58 | return git_root_cache[leaf_dir]['retval'] 59 | 60 | while directory: 61 | if os.path.exists(os.path.join(directory, '.git')): 62 | retval = directory 63 | break 64 | parent = os.path.realpath(os.path.join(directory, os.path.pardir)) 65 | if parent == directory: 66 | # /.. == / 67 | retval = False 68 | break 69 | directory = parent 70 | 71 | git_root_cache[leaf_dir] = { 72 | 'retval': retval, 73 | 'expires': time.time() + 5 74 | } 75 | 76 | return retval 77 | 78 | 79 | # for readability code 80 | def git_root_exist(directory): 81 | return git_root(directory) 82 | 83 | 84 | # try to get an open folder from the window 85 | def get_open_folder_from_window(window): 86 | try: # handle case with no open folder 87 | return window.folders()[0] 88 | except IndexError: 89 | return '' 90 | 91 | 92 | def view_contents(view): 93 | region = sublime.Region(0, view.size()) 94 | return view.substr(region) 95 | 96 | 97 | def plugin_file(name): 98 | return PLUGIN_DIRECTORY + '/' + name 99 | 100 | 101 | def do_when(conditional, command, *args, **kwargs): 102 | if conditional(): 103 | return command(*args, **kwargs) 104 | sublime.set_timeout(functools.partial(do_when, conditional, command, *args, **kwargs), 50) 105 | 106 | 107 | def goto_xy(view, line, col): 108 | view.run_command("goto_line", {"line": line}) 109 | for i in range(col): 110 | view.run_command("move", {"by": "characters", "forward": True}) 111 | 112 | 113 | def _make_text_safeish(text, fallback_encoding, method='decode'): 114 | # The unicode decode here is because sublime converts to unicode inside 115 | # insert in such a way that unknown characters will cause errors, which is 116 | # distinctly non-ideal... and there's no way to tell what's coming out of 117 | # git in output. So... 118 | try: 119 | unitext = getattr(text, method)('utf-8') 120 | except (UnicodeEncodeError, UnicodeDecodeError): 121 | unitext = getattr(text, method)(fallback_encoding) 122 | except AttributeError: 123 | # strongly implies we're already unicode, but just in case let's cast 124 | # to string 125 | unitext = str(text) 126 | return unitext 127 | 128 | 129 | def _test_paths_for_executable(paths, test_file): 130 | for directory in paths: 131 | file_path = os.path.join(directory, test_file) 132 | if os.path.exists(file_path) and os.access(file_path, os.X_OK): 133 | return file_path 134 | 135 | 136 | def find_binary(cmd): 137 | # It turns out to be difficult to reliably run git, with varying paths 138 | # and subprocess environments across different platforms. So. Let's hack 139 | # this a bit. 140 | # (Yes, I could fall back on a hardline "set your system path properly" 141 | # attitude. But that involves a lot more arguing with people.) 142 | path = os.environ.get('PATH', '').split(os.pathsep) 143 | if os.name == 'nt': 144 | cmd = cmd + '.exe' 145 | 146 | path = _test_paths_for_executable(path, cmd) 147 | 148 | if not path: 149 | # /usr/local/bin:/usr/local/git/bin 150 | if os.name == 'nt': 151 | extra_paths = ( 152 | os.path.join(os.environ.get("ProgramFiles", ""), "Git", "bin"), 153 | os.path.join(os.environ.get("ProgramFiles(x86)", ""), "Git", "bin"), 154 | ) 155 | else: 156 | extra_paths = ( 157 | '/usr/local/bin', 158 | '/usr/local/git/bin', 159 | ) 160 | path = _test_paths_for_executable(extra_paths, cmd) 161 | return path 162 | 163 | 164 | GIT = find_binary('git') 165 | GITK = find_binary('gitk') 166 | 167 | 168 | def output_error_message(output, *args, **kwargs): 169 | # print('error', output, args, kwargs) 170 | sublime.error_message(output) 171 | 172 | 173 | class CommandThread(threading.Thread): 174 | command_lock = threading.Lock() 175 | 176 | def __init__(self, command, on_done, working_dir="", fallback_encoding="", error_suppresses_output=False, **kwargs): 177 | threading.Thread.__init__(self) 178 | self.command = command 179 | self.on_done = on_done 180 | self.working_dir = working_dir 181 | if "stdin" in kwargs: 182 | self.stdin = kwargs["stdin"].encode() 183 | else: 184 | self.stdin = None 185 | if "stdout" in kwargs: 186 | self.stdout = kwargs["stdout"] 187 | else: 188 | self.stdout = subprocess.PIPE 189 | self.fallback_encoding = fallback_encoding 190 | self.error_suppresses_output = error_suppresses_output 191 | self.kwargs = kwargs 192 | 193 | def run(self): 194 | # Ignore directories that no longer exist 195 | if not os.path.isdir(self.working_dir): 196 | return 197 | 198 | self.command_lock.acquire() 199 | output = '' 200 | callback = self.on_done 201 | try: 202 | cwd = None 203 | if self.working_dir != "": 204 | cwd = self.working_dir 205 | # Windows needs startupinfo in order to start process in background 206 | startupinfo = None 207 | if os.name == 'nt': 208 | startupinfo = subprocess.STARTUPINFO() 209 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 210 | 211 | env = os.environ.copy() 212 | 213 | shell = False 214 | if sublime.platform() == 'windows': 215 | shell = True 216 | if 'HOME' not in env: 217 | env[str('HOME')] = str(env['HOMEDRIVE']) + str(env['HOMEPATH']) 218 | 219 | # universal_newlines seems to break `log` in python3 220 | proc = subprocess.Popen( 221 | self.command, 222 | stdout=self.stdout, stderr=subprocess.STDOUT, 223 | stdin=subprocess.PIPE, startupinfo=startupinfo, 224 | shell=shell, universal_newlines=False, 225 | env=env, cwd=cwd 226 | ) 227 | output = proc.communicate(self.stdin)[0] 228 | if self.error_suppresses_output and proc.returncode is not None and proc.returncode > 0: 229 | output = False 230 | if not output: 231 | output = '' 232 | output = _make_text_safeish(output, self.fallback_encoding) 233 | except subprocess.CalledProcessError as e: 234 | print("CalledProcessError", e) 235 | if self.error_suppresses_output: 236 | output = '' 237 | else: 238 | output = e.returncode 239 | except OSError as e: 240 | print("OSError", e) 241 | callback = output_error_message 242 | if e.errno == 2: 243 | global _has_warned 244 | if not _has_warned: 245 | _has_warned = True 246 | output = "{cmd} binary could not be found in PATH\n\nConsider using the {cmd_setting}_command setting for the Git plugin\n\nPATH is: {path}".format(cmd=self.command[0], cmd_setting=self.command[0].replace('-', '_'), path=os.environ['PATH']) 247 | else: 248 | output = e.strerror 249 | finally: 250 | self.command_lock.release() 251 | main_thread(callback, output, **self.kwargs) 252 | 253 | 254 | # A base for all commands 255 | class GitCommand(object): 256 | may_change_files = False 257 | 258 | def run_command(self, command, callback=None, show_status=True, filter_empty_args=True, no_save=False, **kwargs): 259 | if filter_empty_args: 260 | command = [arg for arg in command if arg] 261 | if 'working_dir' not in kwargs: 262 | kwargs[str('working_dir')] = str(self.get_working_dir()) 263 | if 'fallback_encoding' not in kwargs and self.active_view() and self.active_view().settings().get('fallback_encoding'): 264 | kwargs[str('fallback_encoding')] = str(self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0]) 265 | 266 | s = sublime.load_settings("Git.sublime-settings") 267 | if ( 268 | s.get('save_first') 269 | and self.active_view() 270 | and self.active_view().file_name() 271 | and self.active_view().is_dirty() 272 | and not no_save 273 | ): 274 | self.active_view().run_command('save') 275 | if command[0] == 'git': 276 | if command[1] == 'flow' and s.get('git_flow_command'): 277 | command[0] = s.get('git_flow_command') 278 | del(command[1]) 279 | else: 280 | us = sublime.load_settings('Preferences.sublime-settings') 281 | if s.get('git_command') or us.get('git_binary'): 282 | command[0] = s.get('git_command') or us.get('git_binary') 283 | elif GIT: 284 | command[0] = GIT 285 | if command[0] == 'gitk' and s.get('gitk_command'): 286 | if s.get('gitk_command'): 287 | command[0] = s.get('gitk_command') 288 | elif GITK: 289 | command[0] = GITK 290 | if not callback: 291 | callback = self.generic_done 292 | 293 | thread = CommandThread(command, callback, **kwargs) 294 | thread.start() 295 | 296 | if show_status: 297 | message = kwargs.get('status_message', False) or ' '.join(command) 298 | sublime.status_message(message) 299 | 300 | def generic_done(self, result, **kw): 301 | if self.may_change_files and self.active_view() and self.active_view().file_name(): 302 | if self.active_view().is_dirty(): 303 | result = "WARNING: Current view is dirty.\n\n" 304 | else: 305 | # just asking the current file to be re-opened doesn't do anything 306 | print("reverting") 307 | position = self.active_view().viewport_position() 308 | self.active_view().run_command('revert') 309 | do_when(lambda: not self.active_view().is_loading(), lambda: self.active_view().set_viewport_position(position, False)) 310 | # self.active_view().show(position) 311 | 312 | view = self.active_view() 313 | if view and view.settings().get('live_git_annotations'): 314 | view.run_command('git_annotate') 315 | 316 | if not result.strip(): 317 | return 318 | self.panel(result) 319 | 320 | def _output_to_view(self, output_file, output, clear=False, syntax="Packages/Diff/Diff.tmLanguage", **kwargs): 321 | output_file.set_syntax_file(syntax) 322 | args = { 323 | 'output': output, 324 | 'clear': clear 325 | } 326 | output_file.run_command('git_scratch_output', args) 327 | 328 | def scratch(self, output, title=False, focused_line=1, **kwargs): 329 | scratch_file = self.get_window().new_file() 330 | if title: 331 | scratch_file.set_name(title) 332 | scratch_file.set_scratch(True) 333 | self._output_to_view(scratch_file, output, **kwargs) 334 | scratch_file.set_read_only(True) 335 | self.record_git_root_to_view(scratch_file) 336 | scratch_file.settings().set('word_wrap', False) 337 | scratch_file.run_command('goto_line', {'line': focused_line}) 338 | return scratch_file 339 | 340 | def panel(self, output, **kwargs): 341 | if not hasattr(self, 'output_view'): 342 | self.output_view = self.get_window().get_output_panel("git") 343 | self.output_view.set_read_only(False) 344 | self._output_to_view(self.output_view, output, clear=True, **kwargs) 345 | self.output_view.set_read_only(True) 346 | self.record_git_root_to_view(self.output_view) 347 | self.get_window().run_command("show_panel", {"panel": "output.git"}) 348 | 349 | def quick_panel(self, *args, **kwargs): 350 | self.get_window().show_quick_panel(*args, **kwargs) 351 | 352 | def record_git_root_to_view(self, view): 353 | # Store the git root directory in the view so we can resolve relative paths 354 | # when the user wants to navigate to the source file. 355 | if self.get_working_dir(): 356 | root = git_root(self.get_working_dir()) 357 | else: 358 | root = self.active_view().settings().get("git_root_dir") 359 | view.settings().set("git_root_dir", root) 360 | 361 | def active_file_path(self): 362 | view = self.active_view() 363 | if view and view.file_name() and len(view.file_name()) > 0: 364 | return view.file_name() 365 | 366 | def active_file_name(self): 367 | path = self.active_file_path() 368 | if path: 369 | return os.path.basename(path) 370 | 371 | def relative_active_file_path(self): 372 | working_dir = self.get_working_dir() 373 | file_path = working_dir.replace(git_root(working_dir), '')[1:] 374 | file_name = os.path.join(file_path, self.active_file_name()) 375 | return file_name.replace('\\', '/') # windows issues 376 | 377 | 378 | # A base for all git commands that work with the entire repository 379 | class GitWindowCommand(GitCommand, sublime_plugin.WindowCommand): 380 | def active_view(self): 381 | return self.window.active_view() 382 | 383 | @property 384 | def fallback_encoding(self): 385 | if self.active_view() and self.active_view().settings().get('fallback_encoding'): 386 | return self.active_view().settings().get('fallback_encoding').rpartition('(')[2].rpartition(')')[0] 387 | 388 | # If there's no active view or the active view is not a file on the 389 | # filesystem (e.g. a search results view), we can infer the folder 390 | # that the user intends Git commands to run against when there's only 391 | # only one. 392 | def is_enabled(self): 393 | if self.active_file_path() or len(self.window.folders()) == 1: 394 | return bool(git_root(self.get_working_dir())) 395 | return False 396 | 397 | def get_file_name(self): 398 | return '' 399 | 400 | def get_relative_file_path(self): 401 | return '' 402 | 403 | # If there is a file in the active view use that file's directory to 404 | # search for the Git root. Otherwise, use the only folder that is 405 | # open. 406 | def get_working_dir(self): 407 | file_name = self.active_file_path() 408 | if file_name: 409 | return os.path.realpath(os.path.dirname(file_name)) 410 | return get_open_folder_from_window(self.window) 411 | 412 | def get_window(self): 413 | return self.window 414 | 415 | 416 | # A base for all git commands that work with the file in the active view 417 | class GitTextCommand(GitCommand, sublime_plugin.TextCommand): 418 | def active_view(self): 419 | return self.view 420 | 421 | def is_enabled(self): 422 | # First, is this actually a file on the file system? 423 | if self.view.file_name() and len(self.view.file_name()) > 0: 424 | return bool(git_root(self.get_working_dir())) 425 | return False 426 | 427 | def get_file_name(self): 428 | return self.active_file_name() 429 | 430 | def get_relative_file_path(self): 431 | return self.relative_active_file_path() 432 | 433 | def get_working_dir(self): 434 | file_name = self.view.file_name() 435 | if file_name: 436 | return os.path.realpath(os.path.dirname(file_name)) 437 | return '' 438 | 439 | def get_window(self): 440 | # Fun discovery: if you switch tabs while a command is working, 441 | # self.view.window() is None. (Admittedly this is a consequence 442 | # of my deciding to do async command processing... but, hey, 443 | # got to live with that now.) 444 | # I did try tracking the window used at the start of the command 445 | # and using it instead of view.window() later, but that results 446 | # panels on a non-visible window, which is especially useless in 447 | # the case of the quick panel. 448 | # So, this is not necessarily ideal, but it does work. 449 | return self.view.window() or sublime.active_window() 450 | -------------------------------------------------------------------------------- /git/add.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import os 4 | import re 5 | 6 | import sublime 7 | from . import GitTextCommand, GitWindowCommand, git_root 8 | from .status import GitStatusCommand 9 | 10 | 11 | class GitAddChoiceCommand(GitStatusCommand): 12 | def status_filter(self, item): 13 | return super(GitAddChoiceCommand, self).status_filter(item) and not item[1].isspace() 14 | 15 | def show_status_list(self): 16 | self.results = [ 17 | [" + All Files", "apart from untracked files"], 18 | [" + All Files", "including untracked files"], 19 | ] + [[a, ''] for a in self.results] 20 | return super(GitAddChoiceCommand, self).show_status_list() 21 | 22 | def panel_followup(self, picked_status, picked_file, picked_index): 23 | working_dir = git_root(self.get_working_dir()) 24 | 25 | if picked_index == 0: 26 | command = ['git', 'add', '--update'] 27 | elif picked_index == 1: 28 | command = ['git', 'add', '--all'] 29 | else: 30 | command = ['git'] 31 | picked_file = picked_file.strip('"') 32 | if os.path.exists(working_dir + "/" + picked_file): 33 | command += ['add'] 34 | else: 35 | command += ['rm'] 36 | command += ['--', picked_file] 37 | 38 | self.run_command( 39 | command, self.rerun, 40 | working_dir=working_dir 41 | ) 42 | 43 | def rerun(self, result): 44 | self.run() 45 | 46 | 47 | class GitAddSelectedHunkCommand(GitTextCommand): 48 | def run(self, edit): 49 | self.run_command(['git', 'diff', '--no-color', '-U1', self.get_file_name()], self.cull_diff) 50 | 51 | def cull_diff(self, result): 52 | selection = [] 53 | for sel in self.view.sel(): 54 | selection.append({ 55 | "start": self.view.rowcol(sel.begin())[0] + 1, 56 | "end": self.view.rowcol(sel.end())[0] + 1, 57 | }) 58 | 59 | hunks = [{"diff": ""}] 60 | i = 0 61 | matcher = re.compile(r'^@@ -([0-9]*)(?:,([0-9]*))? \+([0-9]*)(?:,([0-9]*))? @@') 62 | for line in result.splitlines(): 63 | if line.startswith('@@'): 64 | i += 1 65 | match = matcher.match(line) 66 | start = int(match.group(3)) 67 | end = match.group(4) 68 | if end: 69 | end = start + int(end) 70 | else: 71 | end = start 72 | hunks.append({"diff": "", "start": start, "end": end}) 73 | hunks[i]["diff"] += line + "\n" 74 | 75 | diffs = hunks[0]["diff"] 76 | hunks.pop(0) 77 | selection_is_hunky = False 78 | for hunk in hunks: 79 | for sel in selection: 80 | if sel["end"] < hunk["start"]: 81 | continue 82 | if sel["start"] > hunk["end"]: 83 | continue 84 | diffs += hunk["diff"] # + "\n\nEND OF HUNK\n\n" 85 | selection_is_hunky = True 86 | 87 | if selection_is_hunky: 88 | self.run_command(['git', 'apply', '--cached'], stdin=diffs) 89 | else: 90 | sublime.status_message("No selected hunk") 91 | 92 | 93 | # Also, sometimes we want to undo adds 94 | 95 | 96 | class GitResetHead(object): 97 | def run(self, edit=None): 98 | self.run_command(['git', 'reset', 'HEAD', self.get_file_name()]) 99 | 100 | def generic_done(self, result): 101 | pass 102 | 103 | 104 | class GitResetHeadCommand(GitResetHead, GitTextCommand): 105 | pass 106 | 107 | 108 | class GitResetHeadAllCommand(GitResetHead, GitWindowCommand): 109 | pass 110 | 111 | 112 | class GitResetHardHeadCommand(GitWindowCommand): 113 | may_change_files = True 114 | 115 | def run(self): 116 | if sublime.ok_cancel_dialog("Warning: this will reset your index and revert all files, throwing away all your uncommitted changes with no way to recover. Consider stashing your changes instead if you'd like to set them aside safely.", "Continue"): 117 | self.run_command(['git', 'reset', '--hard', 'HEAD']) 118 | -------------------------------------------------------------------------------- /git/annotate.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import tempfile 4 | import re 5 | import os 6 | import codecs 7 | 8 | import sublime 9 | import sublime_plugin 10 | from . import git_root, GitTextCommand 11 | 12 | 13 | def temp_file(view, key): 14 | if not view.settings().get('git_annotation_temp_%s' % key, False): 15 | fd, filepath = tempfile.mkstemp(prefix='git_annotations_') 16 | os.close(fd) 17 | view.settings().set('git_annotation_temp_%s' % key, filepath) 18 | return view.settings().get('git_annotation_temp_%s' % key) 19 | 20 | 21 | class GitClearAnnotationCommand(GitTextCommand): 22 | def run(self, view): 23 | self.active_view().settings().set('live_git_annotations', False) 24 | self.view.erase_regions('git.changes.x') 25 | self.view.erase_regions('git.changes.+') 26 | self.view.erase_regions('git.changes.-') 27 | 28 | 29 | class GitToggleAnnotationsCommand(GitTextCommand): 30 | def run(self, view): 31 | if self.active_view().settings().get('live_git_annotations'): 32 | self.view.run_command('git_clear_annotation') 33 | else: 34 | self.view.run_command('git_annotate') 35 | 36 | 37 | class GitAnnotationListener(sublime_plugin.EventListener): 38 | def on_modified(self, view): 39 | if not view.settings().get('live_git_annotations'): 40 | return 41 | view.run_command('git_annotate') 42 | 43 | def on_load(self, view): 44 | s = sublime.load_settings("Git.sublime-settings") 45 | if s.get('annotations'): 46 | view.run_command('git_annotate') 47 | 48 | 49 | class GitAnnotateCommand(GitTextCommand): 50 | # Unfortunately, git diff does not support text from stdin, making a *live* 51 | # annotation difficult. Therefore I had to resort to the system diff 52 | # command. 53 | # This works as follows: 54 | # 1. When the command is run for the first time for this file, a temporary 55 | # file with the current state of the HEAD is being pulled from git. 56 | # 2. All consecutive runs will pass the current buffer into diffs stdin. 57 | # The resulting output is then parsed and regions are set accordingly. 58 | may_change_files = False 59 | 60 | def run(self, view): 61 | # If the annotations are already running, we dont have to create a new 62 | # tmpfile 63 | if not hasattr(self, "git_tmp"): 64 | self.git_tmp = temp_file(self.active_view(), 'head') 65 | self.buffer_tmp = temp_file(self.active_view(), 'buffer') 66 | self.active_view().settings().set('live_git_annotations', True) 67 | root = git_root(self.get_working_dir()) 68 | repo_file = os.path.relpath(self.view.file_name(), root).replace('\\', '/') # always unix 69 | self.run_command(['git', 'show', 'HEAD:{0}'.format(repo_file)], show_status=False, no_save=True, callback=self.compare_tmp) 70 | 71 | def compare_tmp(self, result, stdout=None): 72 | with open(self.buffer_tmp, 'wb') as f: 73 | contents = self.get_view_contents() 74 | if self.view.encoding() == "UTF-8 with BOM": 75 | f.write(codecs.BOM_UTF8) 76 | f.write(contents) 77 | with open(self.git_tmp, 'wb') as f: 78 | f.write(result.encode()) 79 | self.run_command(['git', 'diff', '-u', '--', self.git_tmp, self.buffer_tmp], no_save=True, show_status=False, callback=self.parse_diff) 80 | 81 | # This is where the magic happens. At the moment, only one chunk format is supported. While 82 | # the unified diff format theoritaclly supports more, I don't think git diff creates them. 83 | def parse_diff(self, result, stdin=None): 84 | if result.startswith('error:'): 85 | print('Aborted annotations:', result) 86 | return 87 | lines = result.splitlines() 88 | matcher = re.compile(r'^@@ -([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@') 89 | diff = [] 90 | for line_index in range(0, len(lines)): 91 | line = lines[line_index] 92 | if not line.startswith('@'): 93 | continue 94 | match = matcher.match(line) 95 | if not match: 96 | continue 97 | line_before, len_before, line_after, len_after = [int(match.group(x)) for x in [1, 2, 3, 4]] 98 | chunk_index = line_index + 1 99 | tracked_line_index = line_after - 1 100 | deletion = False 101 | insertion = False 102 | while True: 103 | line = lines[chunk_index] 104 | if line.startswith('@'): 105 | break 106 | elif line.startswith('-'): 107 | if not line.strip() == '-': 108 | deletion = True 109 | tracked_line_index -= 1 110 | elif line.startswith('+'): 111 | if deletion and not line.strip() == '+': 112 | diff.append(['x', tracked_line_index]) 113 | insertion = True 114 | elif not deletion: 115 | insertion = True 116 | diff.append(['+', tracked_line_index]) 117 | else: 118 | if not insertion and deletion: 119 | diff.append(['-', tracked_line_index]) 120 | insertion = deletion = False 121 | tracked_line_index += 1 122 | chunk_index += 1 123 | if chunk_index >= len(lines): 124 | break 125 | 126 | self.annotate(diff) 127 | 128 | # Once we got all lines with their specific change types (either x, +, or - for 129 | # modified, added, or removed) we can create our regions and do the actual annotation. 130 | def annotate(self, diff): 131 | self.view.erase_regions('git.changes.x') 132 | self.view.erase_regions('git.changes.+') 133 | self.view.erase_regions('git.changes.-') 134 | typed_diff = {'x': [], '+': [], '-': []} 135 | for change_type, line in diff: 136 | if change_type == '-': 137 | full_region = self.view.full_line(self.view.text_point(line - 1, 0)) 138 | position = full_region.begin() 139 | for i in range(full_region.size()): 140 | typed_diff[change_type].append(sublime.Region(position + i)) 141 | else: 142 | point = self.view.text_point(line, 0) 143 | region = self.view.full_line(point) 144 | if change_type == '-': 145 | region = sublime.Region(point, point + 5) 146 | typed_diff[change_type].append(region) 147 | 148 | for change in ['x', '+']: 149 | self.view.add_regions("git.changes.{0}".format(change), typed_diff[change], 'git.changes.{0}'.format(change), 'dot', sublime.HIDDEN) 150 | 151 | self.view.add_regions("git.changes.-", typed_diff['-'], 'git.changes.-', 'dot', sublime.DRAW_EMPTY_AS_OVERWRITE) 152 | 153 | def get_view_contents(self): 154 | region = sublime.Region(0, self.view.size()) 155 | try: 156 | contents = self.view.substr(region).encode(self._get_view_encoding()) 157 | except UnicodeError: 158 | # Fallback to utf8-encoding 159 | contents = self.view.substr(region).encode('utf-8') 160 | except LookupError: 161 | # May encounter an encoding we don't have a codec for 162 | contents = self.view.substr(region).encode('utf-8') 163 | return contents 164 | 165 | # Copied from GitGutter 166 | def _get_view_encoding(self): 167 | # get encoding and clean it for python ex: "Western (ISO 8859-1)" 168 | # NOTE(maelnor): are we need regex here? 169 | pattern = re.compile(r'.+\((.*)\)') 170 | encoding = self.view.encoding() 171 | if encoding == "Undefined": 172 | encoding = self.view.settings().get('default_encoding') 173 | if pattern.match(encoding): 174 | encoding = pattern.sub(r'\1', encoding) 175 | 176 | encoding = encoding.replace('with BOM', '') 177 | encoding = encoding.replace('Windows', 'cp') 178 | encoding = encoding.replace('-', '_') 179 | encoding = encoding.replace(' ', '') 180 | 181 | # work around with ConvertToUTF8 plugin 182 | origin_encoding = self.view.settings().get('origin_encoding') 183 | return origin_encoding or encoding 184 | -------------------------------------------------------------------------------- /git/commit.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import codecs 4 | import functools 5 | import tempfile 6 | import os 7 | 8 | import sublime 9 | import sublime_plugin 10 | from . import GitTextCommand, GitWindowCommand, plugin_file, view_contents, _make_text_safeish 11 | from .add import GitAddSelectedHunkCommand 12 | 13 | history = [] 14 | 15 | 16 | class GitQuickCommitCommand(GitTextCommand): 17 | def run(self, edit, target=None): 18 | if target is None: 19 | # 'target' might also be False, in which case we just don't provide an add argument 20 | target = self.get_file_name() 21 | self.get_window().show_input_panel( 22 | "Message", "", 23 | functools.partial(self.on_input, target), None, None 24 | ) 25 | 26 | def on_input(self, target, message): 27 | if message.strip() == "": 28 | self.panel("No commit message provided") 29 | return 30 | 31 | if target: 32 | command = ['git', 'add'] 33 | if target == '*': 34 | command.append('--all') 35 | else: 36 | command.extend(('--', target)) 37 | self.run_command(command, functools.partial(self.add_done, message)) 38 | else: 39 | self.add_done(message, "") 40 | 41 | def add_done(self, message, result): 42 | if result.strip(): 43 | sublime.error_message("Error adding file:\n" + result) 44 | return 45 | self.run_command(['git', 'commit', '-m', message]) 46 | 47 | 48 | # Commit is complicated. It'd be easy if I just wanted to let it run 49 | # on OSX, and assume that subl was in the $PATH. However... I can't do 50 | # that. Second choice was to set $GIT_EDITOR to sublime text for the call 51 | # to commit, and let that Just Work. However, on Windows you can't pass 52 | # -w to sublime, which means the editor won't wait, and so the commit will fail 53 | # with an empty message. 54 | # Thus this flow: 55 | # 1. `status --porcelain --untracked-files=no` to know whether files need 56 | # to be committed 57 | # 2. `status` to get a template commit message (not the exact one git uses; I 58 | # can't see a way to ask it to output that, which is not quite ideal) 59 | # 3. Create a scratch buffer containing the template 60 | # 4. When this buffer is closed, get its contents with an event handler and 61 | # pass execution back to the original command. (I feel that the way this 62 | # is done is a total hack. Unfortunately, I cannot see a better way right 63 | # now.) 64 | # 5. Strip lines beginning with # from the message, and save in a temporary 65 | # file 66 | # 6. `commit -F [tempfile]` 67 | class GitCommitCommand(GitWindowCommand): 68 | active_message = False 69 | extra_options = "" 70 | quit_when_nothing_staged = True 71 | 72 | def run(self): 73 | self.lines = [] 74 | self.working_dir = self.get_working_dir() 75 | self.run_command( 76 | ['git', 'status', '--untracked-files=no', '--porcelain'], 77 | self.porcelain_status_done 78 | ) 79 | 80 | def porcelain_status_done(self, result): 81 | # todo: split out these status-parsing things... asdf 82 | has_staged_files = False 83 | result_lines = result.rstrip().split('\n') 84 | for line in result_lines: 85 | if line and not line[0].isspace(): 86 | has_staged_files = True 87 | break 88 | if not has_staged_files and self.quit_when_nothing_staged: 89 | self.panel("Nothing to commit") 90 | return 91 | # Okay, get the template! 92 | s = sublime.load_settings("Git.sublime-settings") 93 | if s.get("verbose_commits"): 94 | self.run_command(['git', 'diff', '--staged', '--no-color'], self.diff_done) 95 | else: 96 | self.run_command(['git', 'status'], self.diff_done) 97 | 98 | def diff_done(self, result): 99 | settings = sublime.load_settings("Git.sublime-settings") 100 | historySize = settings.get('history_size') 101 | rulers = settings.get('commit_rulers') 102 | 103 | def format(line): 104 | return '# ' + line.replace("\n", " ") 105 | 106 | if not len(self.lines): 107 | self.lines = ["", ""] 108 | 109 | self.lines.extend(map(format, history[:historySize])) 110 | self.lines.extend([ 111 | "# --------------", 112 | "# Please enter the commit message for your changes. Everything below", 113 | "# this paragraph is ignored, and an empty message aborts the commit.", 114 | "# Just close the window to accept your message.", 115 | result.strip() 116 | ]) 117 | template = "\n".join(self.lines) 118 | msg = self.window.new_file() 119 | msg.set_scratch(True) 120 | msg.set_name("COMMIT_EDITMSG") 121 | 122 | if rulers: 123 | msg.settings().set('rulers', rulers) 124 | 125 | self._output_to_view(msg, template, syntax=plugin_file("syntax/Git Commit Message.tmLanguage")) 126 | msg.sel().clear() 127 | msg.sel().add(sublime.Region(0, 0)) 128 | GitCommitCommand.active_message = self 129 | 130 | def message_done(self, message): 131 | # filter out the comments (git commit doesn't do this automatically) 132 | settings = sublime.load_settings("Git.sublime-settings") 133 | historySize = settings.get('history_size') 134 | lines = [ 135 | line for line in message.split("\n# --------------")[0].split("\n") 136 | if not line.lstrip().startswith('#') 137 | ] 138 | message = '\n'.join(lines).strip() 139 | 140 | if len(message) and historySize: 141 | history.insert(0, message) 142 | # write the temp file 143 | message_file = tempfile.NamedTemporaryFile(delete=False) 144 | message_file.write(_make_text_safeish(message, self.fallback_encoding, 'encode')) 145 | message_file.close() 146 | self.message_file = message_file 147 | # and actually commit 148 | with codecs.open(message_file.name, mode='r', encoding='utf-8') as fp: 149 | self.run_command( 150 | ['git', 'commit', '-F', '-', self.extra_options], 151 | self.commit_done, working_dir=self.working_dir, stdin=fp.read() 152 | ) 153 | 154 | def commit_done(self, result, **kwargs): 155 | os.remove(self.message_file.name) 156 | self.panel(result) 157 | 158 | 159 | class GitCommitAmendCommand(GitCommitCommand): 160 | extra_options = "--amend" 161 | quit_when_nothing_staged = False 162 | 163 | def diff_done(self, result): 164 | self.after_show = result 165 | self.run_command(['git', 'log', '-n', '1', '--format=format:%B'], self.amend_diff_done) 166 | 167 | def amend_diff_done(self, result): 168 | self.lines = result.split("\n") 169 | super(GitCommitAmendCommand, self).diff_done(self.after_show) 170 | 171 | 172 | class GitCommitMessageListener(sublime_plugin.EventListener): 173 | def on_close(self, view): 174 | if view.name() != "COMMIT_EDITMSG": 175 | return 176 | command = GitCommitCommand.active_message 177 | if not command: 178 | return 179 | message = view_contents(view) 180 | command.message_done(message) 181 | 182 | 183 | class GitCommitHistoryCommand(sublime_plugin.TextCommand): 184 | def run(self, edit): 185 | self.edit = edit 186 | if history: 187 | self.view.window().show_quick_panel(history, self.panel_done, sublime.MONOSPACE_FONT) 188 | else: 189 | sublime.message_dialog("You have no commit history.\n\nCommit history is just a quick list of messages you've used in this session.") 190 | 191 | def panel_done(self, index): 192 | if index > -1: 193 | self.view.replace(self.edit, self.view.sel()[0], history[index] + '\n') 194 | 195 | 196 | class GitCommitSelectedHunk(GitAddSelectedHunkCommand): 197 | def cull_diff(self, result): 198 | super(GitCommitSelectedHunk, self).cull_diff(result) 199 | self.get_window().run_command('git_commit') 200 | -------------------------------------------------------------------------------- /git/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import os 4 | 5 | import sublime 6 | from . import GitWindowCommand, git_root 7 | 8 | 9 | class GitOpenConfigFileCommand(GitWindowCommand): 10 | def run(self): 11 | working_dir = git_root(self.get_working_dir()) 12 | config_file = os.path.join(working_dir, '.git/config') 13 | if os.path.exists(config_file): 14 | self.window.open_file(config_file) 15 | else: 16 | sublime.status_message("No config found") 17 | 18 | 19 | class GitOpenConfigUrlCommand(GitWindowCommand): 20 | def run(self, url_param): 21 | self.run_command(['git', 'config', url_param], self.url_done) 22 | 23 | def url_done(self, result): 24 | results = [r for r in result.rstrip().split('\n') if r.startswith("http")] 25 | if len(results): 26 | url = results[0] 27 | user_end = url.index('@') 28 | if user_end > -1: 29 | # Remove user and pass from url 30 | user_start = url.index('//') + 1 31 | user = url[user_start + 1:user_end + 1] 32 | url = url.replace(user, '') 33 | self.window.run_command('open_url', {"url": url}) 34 | else: 35 | sublime.status_message("No url to open") 36 | -------------------------------------------------------------------------------- /git/core.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import sublime 4 | import sublime_plugin 5 | 6 | from . import GitWindowCommand, GitTextCommand 7 | 8 | 9 | class GitCustomCommand(GitWindowCommand): 10 | may_change_files = True 11 | 12 | def run(self): 13 | self.get_window().show_input_panel( 14 | "Git command", "", 15 | self.on_input, None, None 16 | ) 17 | 18 | def on_input(self, command): 19 | command = str(command) # avoiding unicode 20 | if command.strip() == "": 21 | self.panel("No git command provided") 22 | return 23 | import shlex 24 | command_splitted = ['git'] + shlex.split(command) 25 | print(command_splitted) 26 | self.run_command(command_splitted) 27 | 28 | 29 | class GitRawCommand(GitWindowCommand): 30 | may_change_files = True 31 | 32 | def run(self, **args): 33 | self.command = str(args.get('command', '')) 34 | show_in = str(args.get('show_in', 'pane_below')) 35 | 36 | if self.command.strip() == "": 37 | self.panel("No git command provided") 38 | return 39 | import shlex 40 | command_split = shlex.split(self.command) 41 | 42 | if args.get('append_current_file', False) and self.active_file_name(): 43 | command_split.extend(('--', self.active_file_name())) 44 | 45 | print(command_split) 46 | 47 | self.may_change_files = bool(args.get('may_change_files', True)) 48 | 49 | if show_in == 'pane_below': 50 | self.run_command(command_split) 51 | elif show_in == 'quick_panel': 52 | self.run_command(command_split, self.show_in_quick_panel) 53 | elif show_in == 'new_tab': 54 | self.run_command(command_split, self.show_in_new_tab) 55 | elif show_in == 'suppress': 56 | self.run_command(command_split, self.do_nothing) 57 | 58 | view = self.active_view() 59 | view.run_command('git_branch_status') 60 | 61 | def show_in_quick_panel(self, result): 62 | self.results = list(result.rstrip().split('\n')) 63 | if len(self.results): 64 | self.quick_panel( 65 | self.results, 66 | self.do_nothing, sublime.MONOSPACE_FONT 67 | ) 68 | else: 69 | sublime.status_message("Nothing to show") 70 | 71 | def do_nothing(self, picked): 72 | return 73 | 74 | def show_in_new_tab(self, result): 75 | msg = self.window.new_file() 76 | msg.set_scratch(True) 77 | msg.set_name(self.command) 78 | self._output_to_view(msg, result) 79 | msg.sel().clear() 80 | msg.sel().add(sublime.Region(0, 0)) 81 | 82 | 83 | class GitGuiCommand(GitTextCommand): 84 | def run(self, edit): 85 | command = ['git', 'gui'] 86 | self.run_command(command) 87 | 88 | 89 | class GitGitkCommand(GitTextCommand): 90 | def run(self, edit): 91 | command = ['gitk'] 92 | self.run_command(command) 93 | 94 | 95 | # called by GitWindowCommand 96 | class GitScratchOutputCommand(sublime_plugin.TextCommand): 97 | def run(self, edit, output='', output_file=None, clear=False): 98 | if clear: 99 | region = sublime.Region(0, self.view.size()) 100 | self.view.erase(edit, region) 101 | self.view.insert(edit, 0, output) 102 | -------------------------------------------------------------------------------- /git/diff.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import sublime 4 | import sublime_plugin 5 | import os 6 | import re 7 | from . import GitTextCommand, GitWindowCommand, do_when, goto_xy, git_root, get_open_folder_from_window 8 | 9 | 10 | class GitDiff (object): 11 | def run(self, edit=None, ignore_whitespace=False, word_diff=False): 12 | command = ['git', 'diff', '--no-color'] 13 | if ignore_whitespace: 14 | command.extend(('--ignore-all-space', '--ignore-blank-lines')) 15 | command.extend(('--', self.get_file_name())) 16 | self.run_command(command, self.diff_done) 17 | if word_diff: 18 | command.append('--word-diff') 19 | 20 | def diff_done(self, result): 21 | if not result.strip(): 22 | self.panel("No output") 23 | return 24 | s = sublime.load_settings("Git.sublime-settings") 25 | syntax = s.get("diff_syntax", "Packages/Git/syntax/Git Diff.sublime-syntax") 26 | if s.get('diff_panel'): 27 | self.panel(result, syntax=syntax) 28 | else: 29 | self.scratch(result, title="Git Diff", syntax=syntax) 30 | 31 | 32 | class GitDiffCommit (object): 33 | def run(self, edit=None, ignore_whitespace=False, word_diff=False): 34 | command = ['git', 'diff', '--cached', '--no-color'] 35 | if ignore_whitespace: 36 | command.extend(('--ignore-all-space', '--ignore-blank-lines')) 37 | if word_diff: 38 | command.extend('--word-diff') 39 | self.run_command(command, self.diff_done) 40 | 41 | def diff_done(self, result): 42 | if not result.strip(): 43 | self.panel("No output") 44 | return 45 | s = sublime.load_settings("Git.sublime-settings") 46 | syntax = s.get("diff_syntax", "Packages/Git/syntax/Git Diff.sublime-syntax") 47 | self.scratch(result, title="Git Diff", syntax=syntax) 48 | 49 | 50 | class GitDiffCommand(GitDiff, GitTextCommand): 51 | pass 52 | 53 | 54 | class GitDiffAllCommand(GitDiff, GitWindowCommand): 55 | pass 56 | 57 | 58 | class GitDiffCommitCommand(GitDiffCommit, GitWindowCommand): 59 | pass 60 | 61 | 62 | class GitGotoDiff(sublime_plugin.TextCommand): 63 | def __init__(self, view): 64 | self.view = view 65 | 66 | def run(self, edit): 67 | v = self.view 68 | view_scope_name = v.scope_name(v.sel()[0].a) 69 | scope_markup_inserted = ("markup.inserted.diff" in view_scope_name) 70 | scope_markup_deleted = ("markup.deleted.diff" in view_scope_name) 71 | 72 | if not scope_markup_inserted and not scope_markup_deleted: 73 | return 74 | 75 | beg = v.sel()[0].a # Current position in selection 76 | pt = v.line(beg).a # First position in the current diff line 77 | self.column = beg - pt - 1 # The current column (-1 because the first char in diff file) 78 | 79 | self.file_name = None 80 | hunk_line = None 81 | line_offset = 0 82 | 83 | while pt > 0: 84 | line = v.line(pt) 85 | lineContent = v.substr(line) 86 | if lineContent.startswith("@@"): 87 | if not hunk_line: 88 | hunk_line = lineContent 89 | elif lineContent.startswith("+++ b/"): 90 | self.file_name = v.substr(sublime.Region(line.a + 6, line.b)).strip() 91 | break 92 | elif not hunk_line and not lineContent.startswith("-"): 93 | line_offset = line_offset + 1 94 | 95 | pt = v.line(pt - 1).a 96 | 97 | hunk = re.match(r"^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*", hunk_line) 98 | if not hunk: 99 | sublime.status_message("No hunk info") 100 | return 101 | 102 | hunk_start_line = hunk.group(3) 103 | self.goto_line = int(hunk_start_line) + line_offset - 1 104 | 105 | git_root_dir = v.settings().get("git_root_dir") 106 | # See if we can get the git root directory if we haven't saved it yet 107 | if not git_root_dir: 108 | working_dir = get_open_folder_from_window(v.window()) 109 | git_root_dir = git_root(working_dir) if working_dir else None 110 | 111 | # Sanity check and see if the file we're going to try to open even 112 | # exists. If it does not, prompt the user for the correct base directory 113 | # to use for their diff. 114 | full_path_file_name = self.file_name 115 | if git_root_dir: 116 | full_path_file_name = os.path.join(git_root_dir, self.file_name) 117 | else: 118 | git_root_dir = "" 119 | 120 | if not os.path.isfile(full_path_file_name): 121 | caption = "Enter base directory for file '%s':" % self.file_name 122 | v.window().show_input_panel(caption, 123 | git_root_dir, 124 | self.on_path_confirmed, 125 | None, 126 | None) 127 | else: 128 | self.on_path_confirmed(git_root_dir) 129 | 130 | def on_path_confirmed(self, git_root_dir): 131 | v = self.view 132 | old_git_root_dir = v.settings().get("git_root_dir") 133 | 134 | # If the user provided a new git_root_dir, save it in the view settings 135 | # so they only have to fix it once 136 | if old_git_root_dir != git_root_dir: 137 | v.settings().set("git_root_dir", git_root_dir) 138 | 139 | full_path_file_name = os.path.join(git_root_dir, self.file_name) 140 | 141 | new_view = v.window().open_file(full_path_file_name) 142 | do_when(lambda: not new_view.is_loading(), 143 | lambda: goto_xy(new_view, self.goto_line, self.column)) 144 | -------------------------------------------------------------------------------- /git/file.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import sublime 4 | import functools 5 | from . import GitWindowCommand, git_root 6 | 7 | 8 | class GitFileMove(GitWindowCommand): 9 | def run(self, **args): 10 | filename = self.relative_active_file_path() 11 | branch, leaf = os.path.split(filename) 12 | 13 | if not os.access(self.active_file_path(), os.W_OK): 14 | sublime.error_message(leaf + " is read-only") 15 | 16 | panel = self.get_window().show_input_panel( 17 | "New path / name", filename, 18 | self.on_input, None, None 19 | ) 20 | 21 | if branch: 22 | # We want a trailing slash for selection purposes 23 | branch = branch + os.path.sep 24 | 25 | # Now, select just the base part of the filename 26 | name, ext = os.path.splitext(leaf) 27 | panel.sel().clear() 28 | panel.sel().add(sublime.Region(len(branch), len(branch) + len(name))) 29 | 30 | def on_input(self, newpath): 31 | newpath = str(newpath) # avoiding unicode 32 | 33 | if not newpath.strip(): 34 | return self.panel("No input received") 35 | 36 | working_dir = git_root(self.get_working_dir()) 37 | newpath = os.path.join(working_dir, newpath) 38 | 39 | command = ['git', 'mv', '--', self.active_file_path(), newpath] 40 | self.run_command(command, functools.partial(self.on_done, newpath), working_dir=working_dir) 41 | 42 | def on_done(self, newpath, result): 43 | if result.strip(): 44 | return self.panel(result) 45 | 46 | self.active_view().retarget(newpath) 47 | -------------------------------------------------------------------------------- /git/flow.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import sublime 4 | from . import GitWindowCommand 5 | 6 | 7 | class GitFlowCommand(GitWindowCommand): 8 | def is_visible(self): 9 | s = sublime.load_settings("Git.sublime-settings") 10 | if s.get('flow'): 11 | return True 12 | return False 13 | 14 | def is_notag(self): 15 | s = sublime.load_settings("Git.sublime-settings") 16 | if s.get('flow-notag'): 17 | return True 18 | return False 19 | 20 | 21 | class GitFlowFeatureStartCommand(GitFlowCommand): 22 | def run(self): 23 | self.get_window().show_input_panel('Enter Feature Name:', '', self.on_done, None, None) 24 | 25 | def on_done(self, feature_name): 26 | self.run_command(['git', 'flow', 'feature', 'start', feature_name]) 27 | 28 | 29 | class GitFlowFeatureFinishCommand(GitFlowCommand): 30 | def run(self): 31 | self.run_command(['git', 'flow', 'feature'], self.feature_done) 32 | 33 | def feature_done(self, result): 34 | self.results = result.rstrip().split('\n') 35 | self.quick_panel( 36 | self.results, self.panel_done, 37 | sublime.MONOSPACE_FONT 38 | ) 39 | 40 | def panel_done(self, picked): 41 | if 0 > picked < len(self.results): 42 | return 43 | picked_feature = self.results[picked] 44 | if picked_feature.startswith("*"): 45 | picked_feature = picked_feature.strip("*") 46 | picked_feature = picked_feature.strip() 47 | self.run_command(['git', 'flow', 'feature', 'finish', picked_feature]) 48 | 49 | 50 | class GitFlowReleaseStartCommand(GitFlowCommand): 51 | def run(self): 52 | self.get_window().show_input_panel('Enter Version Number:', '', self.on_done, None, None) 53 | 54 | def on_done(self, release_name): 55 | self.run_command(['git', 'flow', 'release', 'start', release_name]) 56 | 57 | 58 | class GitFlowReleaseFinishCommand(GitFlowCommand): 59 | def run(self): 60 | self.run_command(['git', 'flow', 'release'], self.release_done) 61 | 62 | def release_done(self, result): 63 | self.results = result.rstrip().split('\n') 64 | self.quick_panel( 65 | self.results, self.panel_done, 66 | sublime.MONOSPACE_FONT 67 | ) 68 | 69 | def panel_done(self, picked): 70 | if 0 > picked < len(self.results): 71 | return 72 | picked_release = self.results[picked] 73 | if picked_release.startswith("*"): 74 | picked_release = picked_release.strip("*") 75 | picked_release = picked_release.strip() 76 | if self.is_notag(): 77 | self.run_command(['git', 'flow', 'release', 'finish', '-n', picked_release]) 78 | else: 79 | self.picked_release = picked_release 80 | self.get_window().show_input_panel('Enter Tag message:', '', self.tag_message_done, None, None) 81 | 82 | def tag_message_done(self, tag_message): 83 | self.run_command(['git', 'flow', 'release', 'finish', '-m', tag_message, self.picked_release]) 84 | 85 | 86 | class GitFlowHotfixStartCommand(GitFlowCommand): 87 | def run(self): 88 | self.get_window().show_input_panel('Enter hotfix name:', '', self.on_done, None, None) 89 | 90 | def on_done(self, hotfix_name): 91 | self.run_command(['git', 'flow', 'hotfix', 'start', hotfix_name]) 92 | 93 | 94 | class GitFlowHotfixFinishCommand(GitFlowCommand): 95 | def run(self): 96 | self.run_command(['git', 'flow', 'hotfix'], self.hotfix_done) 97 | 98 | def hotfix_done(self, result): 99 | self.results = result.rstrip().split('\n') 100 | self.quick_panel( 101 | self.results, self.panel_done, 102 | sublime.MONOSPACE_FONT 103 | ) 104 | 105 | def panel_done(self, picked): 106 | if 0 > picked < len(self.results): 107 | return 108 | picked_hotfix = self.results[picked] 109 | if picked_hotfix.startswith("*"): 110 | picked_hotfix = picked_hotfix.strip("*") 111 | picked_hotfix = picked_hotfix.strip() 112 | if self.is_notag(): 113 | self.run_command(['git', 'flow', 'hotfix', 'finish', '-n', picked_hotfix]) 114 | else: 115 | self.picked_hotfix = picked_hotfix 116 | self.get_window().show_input_panel('Enter Tag message:', '', self.tag_message_done, None, None) 117 | 118 | def tag_message_done(self, tag_message): 119 | self.run_command(['git', 'flow', 'hotfix', 'finish', '-m', tag_message, self.picked_hotfix]) 120 | -------------------------------------------------------------------------------- /git/history.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import functools 4 | import re 5 | 6 | import sublime 7 | from . import GitTextCommand, GitWindowCommand, plugin_file 8 | 9 | 10 | class GitBlameCommand(GitTextCommand): 11 | def run(self, edit): 12 | # somewhat custom blame command: 13 | # -w: ignore whitespace changes 14 | # -M: retain blame when moving lines 15 | # -C: retain blame when copying lines between files 16 | command = ['git', 'blame', '-w', '-M', '-C'] 17 | line_ranges = [self.get_lines(selection) for selection in self.view.sel() if not selection.empty()] 18 | 19 | if line_ranges: 20 | for (range_start, range_end, *line_range_len) in line_ranges: 21 | command.extend(('-L', str(range_start) + ',' + str(range_end))) 22 | 23 | callback = self.blame_done 24 | else: 25 | callback = functools.partial(self.blame_done, 26 | focused_line=self.get_current_line()) 27 | 28 | command.append(self.get_file_name()) 29 | self.run_command(command, callback) 30 | 31 | def get_current_line(self): 32 | (current_line, column) = self.view.rowcol(self.view.sel()[0].a) 33 | # line is 1 based 34 | return current_line + 1 35 | 36 | def get_lines(self, selection): 37 | if selection.empty(): 38 | return False 39 | # just the lines we have a selection on 40 | begin_line, begin_column = self.view.rowcol(selection.begin()) 41 | end_line, end_column = self.view.rowcol(selection.end()) 42 | # blame will fail if last line is empty and is included in the selection 43 | if end_line > begin_line and end_column == 0: 44 | end_line -= 1 45 | # add one to each, to line up sublime's index with git's 46 | return begin_line + 1, end_line + 1 47 | 48 | def blame_done(self, result, focused_line=1): 49 | self.scratch( 50 | result, title="Git Blame", focused_line=focused_line, 51 | syntax=plugin_file("syntax/Git Blame.tmLanguage") 52 | ) 53 | 54 | 55 | class GitLog(object): 56 | def run(self, edit=None): 57 | fn = self.get_file_name() 58 | return self.run_log(fn != '', '--', fn) 59 | 60 | def run_log(self, follow, *args): 61 | # the ASCII bell (\a) is just a convenient character I'm pretty sure 62 | # won't ever come up in the subject of the commit (and if it does then 63 | # you positively deserve broken output...) 64 | # 9000 is a pretty arbitrarily chosen limit; picked entirely because 65 | # it's about the size of the largest repo I've tested this on... and 66 | # there's a definite hiccup when it's loading that 67 | command = [ 68 | 'git', 'log', '--no-color', '--pretty=%s (%h)\a%an <%aE>\a%ad (%ar)', 69 | '--date=local', '--max-count=9000', 70 | '--follow' if follow else None 71 | ] 72 | command.extend(args) 73 | self.run_command( 74 | command, 75 | self.log_done) 76 | 77 | def log_done(self, result): 78 | self.results = [r.split('\a', 2) for r in result.strip().split('\n')] 79 | self.quick_panel(self.results, self.log_panel_done) 80 | 81 | def log_panel_done(self, picked): 82 | if 0 > picked < len(self.results): 83 | return 84 | item = self.results[picked] 85 | # the commit hash is the last thing on the first line, in brackets 86 | ref = item[0].split(' ')[-1].strip('()') 87 | self.log_result(ref) 88 | 89 | def log_result(self, ref): 90 | # I'm not certain I should have the file name here; it restricts the 91 | # details to just the current file. Depends on what the user expects... 92 | # which I'm not sure of. 93 | self.run_command( 94 | ['git', 'log', '--no-color', '-p', '-1', ref, '--', self.get_file_name()], 95 | self.details_done) 96 | 97 | def details_done(self, result): 98 | self.scratch(result, title="Git Commit Details", 99 | syntax=plugin_file("syntax/Git Commit View.tmLanguage")) 100 | 101 | 102 | class GitLogCommand(GitLog, GitTextCommand): 103 | pass 104 | 105 | 106 | class GitLogAllCommand(GitLog, GitWindowCommand): 107 | pass 108 | 109 | 110 | class GitShow(object): 111 | def run(self, edit=None): 112 | # GitLog Copy-Past 113 | self.run_command( 114 | ['git', 'log', '--no-color', '--pretty=%s (%h)\a%an <%aE>\a%ad (%ar)', 115 | '--date=local', '--max-count=9000', '--', self.get_file_name()], 116 | self.show_done) 117 | 118 | def show_done(self, result): 119 | # GitLog Copy-Past 120 | self.results = [r.split('\a', 2) for r in result.strip().split('\n')] 121 | self.quick_panel(self.results, self.panel_done) 122 | 123 | def panel_done(self, picked): 124 | if 0 > picked < len(self.results): 125 | return 126 | item = self.results[picked] 127 | # the commit hash is the last thing on the first line, in brackets 128 | ref = item[0].split(' ')[-1].strip('()') 129 | self.run_command( 130 | ['git', 'show', '%s:%s' % (ref, self.get_relative_file_path())], 131 | self.details_done, 132 | ref=ref) 133 | 134 | def details_done(self, result, ref): 135 | syntax = self.view.settings().get('syntax') 136 | self.scratch(result, title="%s:%s" % (ref, self.get_file_name()), syntax=syntax) 137 | 138 | 139 | class GitShowCommand(GitShow, GitTextCommand): 140 | pass 141 | 142 | 143 | class GitShowAllCommand(GitShow, GitWindowCommand): 144 | pass 145 | 146 | 147 | class GitShowCommitCommand(GitWindowCommand): 148 | def run(self, edit=None): 149 | self.window.show_input_panel("Commit to show:", "", self.input_done, None, None) 150 | 151 | def input_done(self, commit): 152 | commit = commit.strip() 153 | 154 | self.run_command(['git', 'show', commit, '--'], self.show_done, commit=commit) 155 | 156 | def show_done(self, result, commit): 157 | if result.startswith('fatal:'): 158 | self.panel(result) 159 | return 160 | self.scratch(result, title="Git Commit: %s" % commit, 161 | syntax=plugin_file("syntax/Git Commit View.tmLanguage")) 162 | 163 | 164 | class GitGraph(object): 165 | def run(self, edit=None): 166 | filename = self.get_file_name() 167 | self.run_command( 168 | ['git', 'log', '--graph', '--pretty=%h -%d (%cr) (%ci) <%an> %s', '--abbrev-commit', '--no-color', '--decorate', '--date=relative', '--follow' if filename else None, '--', filename], 169 | self.log_done 170 | ) 171 | 172 | def log_done(self, result): 173 | self.scratch(result, title="Git Log Graph", syntax=plugin_file("syntax/Git Graph.tmLanguage")) 174 | 175 | 176 | class GitGraphCommand(GitGraph, GitTextCommand): 177 | pass 178 | 179 | 180 | class GitGraphAllCommand(GitGraph, GitWindowCommand): 181 | pass 182 | 183 | 184 | class GitOpenFileCommand(GitLog, GitWindowCommand): 185 | def run(self): 186 | self.run_command(['git', 'branch', '-a', '--no-color'], self.branch_done) 187 | 188 | def branch_done(self, result): 189 | self.results = result.rstrip().split('\n') 190 | self.quick_panel( 191 | self.results, self.branch_panel_done, 192 | sublime.MONOSPACE_FONT 193 | ) 194 | 195 | def branch_panel_done(self, picked): 196 | if 0 > picked < len(self.results): 197 | return 198 | self.branch = self.results[picked].split(' ')[-1] 199 | self.run_log(False, self.branch) 200 | 201 | def log_result(self, result_hash): 202 | self.ref = result_hash 203 | self.run_command( 204 | ['git', 'ls-tree', '-r', '--full-tree', self.ref], 205 | self.ls_done) 206 | 207 | def ls_done(self, result): 208 | # Last two items are the ref and the file name 209 | # p.s. has to be a list of lists; tuples cause errors later 210 | self.results = [[match.group(2), match.group(1)] for match in re.finditer(r"\S+\s(\S+)\t(.+)", result)] 211 | 212 | self.quick_panel(self.results, self.ls_panel_done) 213 | 214 | def ls_panel_done(self, picked): 215 | if 0 > picked < len(self.results): 216 | return 217 | item = self.results[picked] 218 | 219 | self.filename = item[0] 220 | self.fileRef = item[1] 221 | 222 | self.run_command( 223 | ['git', 'show', self.fileRef], 224 | self.show_done) 225 | 226 | def show_done(self, result): 227 | self.scratch(result, title="%s:%s" % (self.fileRef, self.filename)) 228 | 229 | 230 | class GitDocumentCommand(GitBlameCommand): 231 | def blame_done(self, result, focused_line=1): 232 | shas = set((sha for sha in re.findall(r'^[0-9a-f]+', result, re.MULTILINE) if not re.match(r'^0+$', sha))) 233 | command = ['git', 'show', '-s', '-z', '--no-color', '--date=iso'] 234 | command.extend(shas) 235 | 236 | self.run_command(command, self.show_done) 237 | 238 | def show_done(self, result): 239 | commits = [] 240 | for commit in result.split('\0'): 241 | match = re.search(r'^Date:\s+(.+)$', commit, re.MULTILINE) 242 | if match: 243 | commits.append((match.group(1), commit)) 244 | commits.sort(reverse=True) 245 | commits = [commit for d, commit in commits] 246 | 247 | self.scratch('\n\n'.join(commits), title="Git Commit Documentation", 248 | syntax=plugin_file("syntax/Git Commit View.tmLanguage")) 249 | 250 | 251 | class GitGotoCommit(GitTextCommand): 252 | def run(self, edit): 253 | view = self.view 254 | 255 | # Sublime is missing a "find scope in region" API, so we piece one together here: 256 | lines = [view.line(sel.a) for sel in view.sel()] 257 | hashes = self.view.find_by_selector("string.sha") 258 | commits = [] 259 | for region in hashes: 260 | for line in lines: 261 | if line.contains(region): 262 | commit = view.substr(region) 263 | if commit.strip("0"): 264 | commits.append(commit) 265 | break 266 | 267 | working_dir = view.settings().get("git_root_dir") 268 | for commit in commits: 269 | self.run_command(['git', 'show', commit], self.show_done, working_dir=working_dir) 270 | 271 | def show_done(self, result): 272 | self.scratch(result, title="Git Commit View", 273 | syntax=plugin_file("syntax/Git Commit View.tmLanguage")) 274 | 275 | def is_enabled(self): 276 | selection = self.view.sel()[0] 277 | return ( 278 | self.view.match_selector(selection.a, "text.git-blame") 279 | or self.view.match_selector(selection.a, "text.git-graph") 280 | ) 281 | -------------------------------------------------------------------------------- /git/ignore.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import functools 4 | import os 5 | 6 | import sublime 7 | import sublime_plugin 8 | from . import GitTextCommand 9 | 10 | 11 | class GitIgnoreEventListener(sublime_plugin.EventListener): 12 | def is_enabled(self): 13 | # So, is_enabled isn't actually part of the API for event listeners. But... 14 | s = sublime.load_settings("Git.sublime-settings") 15 | return s.get("gitignore_sync") 16 | 17 | def on_activated(self, view): 18 | if self.is_enabled(): 19 | view.run_command("git_update_ignore") 20 | 21 | def on_post_save(self, view): 22 | if self.is_enabled(): 23 | view.run_command("git_update_ignore") 24 | 25 | 26 | class GitUpdateIgnoreCommand(GitTextCommand): 27 | def path(self, folderpath): 28 | project_file_name = self.view.window().project_file_name() 29 | if project_file_name: 30 | return os.path.join(os.path.dirname(project_file_name), folderpath) 31 | return folderpath 32 | 33 | def run(self, edit): 34 | self.count = 0 35 | self.excludes = {} 36 | 37 | data = self.view.window().project_data() 38 | for index, folder in enumerate(data['folders']): 39 | self.count += 2 40 | self.excludes[index] = { 41 | 'files': set(), 42 | 'folders': set(), 43 | } 44 | 45 | path = self.path(folder['path']) 46 | callback = functools.partial(self.ignored_files_found, folder_index=index) 47 | self.run_command( 48 | ['git', 'status', '--ignored', '--porcelain'], 49 | callback=callback, 50 | working_dir=path, 51 | error_suppresses_output=True, 52 | show_status=False 53 | ) 54 | self.run_command( 55 | ['git', 'submodule', 'foreach', 'git status --ignored --porcelain'], 56 | callback=callback, 57 | working_dir=path, 58 | error_suppresses_output=True, 59 | show_status=False 60 | ) 61 | 62 | def ignored_files_found(self, result, folder_index): 63 | self.count -= 1 64 | 65 | self.process_ignored_files(result, folder_index) 66 | 67 | if self.count == 0: 68 | self.all_ignored_files_found() 69 | 70 | def process_ignored_files(self, result, folder_index): 71 | data = self.view.window().project_data() 72 | folder = data['folders'][folder_index] 73 | 74 | if not folder: 75 | return 76 | if not result or result.isspace(): 77 | return 78 | 79 | root = self.path(folder['path']) 80 | exclude_folders = self.excludes[folder_index]['folders'] 81 | exclude_files = self.excludes[folder_index]['files'] 82 | 83 | subroot = '' 84 | for line in result.strip().split('\n'): 85 | if line.startswith('Entering'): 86 | subroot = line.replace('Entering ', '').replace('\'', '') 87 | if not line.startswith('!!'): 88 | continue 89 | path = os.path.join(subroot, line.replace('!! ', '')) 90 | 91 | if os.path.isdir(os.path.join(root, path)): 92 | exclude_folders.add(path.rstrip('\\/')) 93 | else: 94 | exclude_files.add(path) 95 | 96 | return exclude_files, exclude_folders 97 | 98 | def all_ignored_files_found(self): 99 | data = self.view.window().project_data() 100 | changed = False 101 | for index, folder in enumerate(data['folders']): 102 | exclude_folders = self.excludes[index]['folders'] 103 | exclude_files = self.excludes[index]['files'] 104 | 105 | old_exclude_folders = set(folder.get('folder_exclude_patterns', [])) 106 | old_exclude_files = set(folder.get('file_exclude_patterns', [])) 107 | 108 | if exclude_folders != old_exclude_folders or exclude_files != old_exclude_files: 109 | print('Git: updating project exclusions', folder['path'], exclude_folders, exclude_files) 110 | folder['folder_exclude_patterns'] = list(exclude_folders) 111 | folder['file_exclude_patterns'] = list(exclude_files) 112 | changed = True 113 | if changed: 114 | self.view.window().set_project_data(data) 115 | -------------------------------------------------------------------------------- /git/index.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import os 4 | import re 5 | 6 | import sublime 7 | from . import GitWindowCommand, git_root 8 | from .status import GitStatusCommand 9 | 10 | 11 | class GitUpdateIndexAssumeUnchangedCommand(GitStatusCommand): 12 | def status_filter(self, item): 13 | return super(GitUpdateIndexAssumeUnchangedCommand, self).status_filter(item) and not item[1].isspace() 14 | 15 | def show_status_list(self): 16 | self.results = [] + [[a, ''] for a in self.results] 17 | return super(GitUpdateIndexAssumeUnchangedCommand, self).show_status_list() 18 | 19 | def panel_followup(self, picked_status, picked_file, picked_index): 20 | working_dir = git_root(self.get_working_dir()) 21 | 22 | command = ['git'] 23 | picked_file = picked_file.strip('"') 24 | if os.path.exists(working_dir + "/" + picked_file): 25 | command += ['update-index', '--assume-unchanged'] 26 | command += ['--', picked_file] 27 | 28 | self.run_command( 29 | command, self.rerun, 30 | working_dir=working_dir 31 | ) 32 | 33 | def rerun(self, result): 34 | self.run() 35 | 36 | 37 | class GitUpdateIndexNoAssumeUnchangedCommand(GitWindowCommand): 38 | force_open = False 39 | 40 | def run(self): 41 | root = git_root(self.get_working_dir()) 42 | self.run_command(['git', 'ls-files', '-v'], self.status_done, working_dir=root) 43 | 44 | def status_done(self, result): 45 | self.results = list(filter(self.status_filter, result.rstrip().split('\n'))) 46 | if len(self.results): 47 | self.show_status_list() 48 | else: 49 | sublime.status_message("Nothing to show") 50 | 51 | def show_status_list(self): 52 | self.quick_panel( 53 | self.results, self.panel_done, 54 | sublime.MONOSPACE_FONT 55 | ) 56 | 57 | def status_filter(self, item): 58 | # for this class we don't actually care 59 | if not re.match(r'^h\s+.*', item): 60 | return False 61 | return len(item) > 0 62 | 63 | def panel_done(self, picked): 64 | if 0 > picked < len(self.results): 65 | return 66 | root = git_root(self.get_working_dir()) 67 | picked_file = self.results[picked] 68 | if isinstance(picked_file, (list, tuple)): 69 | picked_file = picked_file[0] 70 | # first 1 character is a status code, the second is a space 71 | picked_file = picked_file[2:] 72 | self.run_command( 73 | ['git', 'update-index', '--no-assume-unchanged', picked_file.strip('"')], 74 | self.rerun, working_dir=root 75 | ) 76 | 77 | def rerun(self, result): 78 | self.run() 79 | -------------------------------------------------------------------------------- /git/repo.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import os 4 | 5 | import sublime 6 | from . import GitWindowCommand, git_root_exist 7 | 8 | 9 | class GitInit(object): 10 | def git_init(self, directory): 11 | if os.path.exists(directory): 12 | self.run_command(['git', 'init'], self.git_inited, working_dir=directory) 13 | else: 14 | sublime.status_message("Directory does not exist.") 15 | 16 | def git_inited(self, result): 17 | sublime.status_message(result) 18 | 19 | 20 | class GitInitCommand(GitInit, GitWindowCommand): 21 | def run(self): 22 | self.get_window().show_input_panel("Git directory", self.get_working_dir(), self.git_init, None, None) 23 | 24 | def is_enabled(self): 25 | if not git_root_exist(self.get_working_dir()): 26 | return True 27 | else: 28 | return False 29 | 30 | 31 | class GitBranchCommand(GitWindowCommand): 32 | may_change_files = True 33 | command_to_run_after_branch = ['checkout'] 34 | extra_flags = [] 35 | 36 | def run(self): 37 | self.run_command(['git', 'branch', '--no-color'] + self.extra_flags, self.branch_done) 38 | 39 | def branch_done(self, result): 40 | self.results = result.rstrip().split('\n') 41 | self.quick_panel( 42 | self.results, self.panel_done, 43 | sublime.MONOSPACE_FONT 44 | ) 45 | 46 | def panel_done(self, picked): 47 | if 0 > picked < len(self.results): 48 | return 49 | picked_branch = self.results[picked] 50 | if picked_branch.startswith("*"): 51 | return 52 | picked_branch = picked_branch.strip() 53 | self.run_command(['git'] + self.command_to_run_after_branch + [picked_branch], self.update_status) 54 | 55 | def update_status(self, result): 56 | self.panel(result) 57 | global branch 58 | branch = "" 59 | for view in self.window.views(): 60 | view.run_command("git_branch_status") 61 | 62 | 63 | class GitMergeCommand(GitBranchCommand): 64 | command_to_run_after_branch = ['merge'] 65 | extra_flags = ['--no-merge'] 66 | 67 | 68 | class GitDeleteBranchCommand(GitBranchCommand): 69 | command_to_run_after_branch = ['branch', '-d'] 70 | 71 | 72 | class GitForceDeleteBranchCommand(GitDeleteBranchCommand): 73 | command_to_run_after_branch = ['branch', '-D'] 74 | 75 | 76 | class GitNewBranchCommand(GitWindowCommand): 77 | def run(self): 78 | self.get_window().show_input_panel( 79 | "Branch name", "", 80 | self.on_input, None, None 81 | ) 82 | 83 | def on_input(self, branchname): 84 | if branchname.strip() == "": 85 | self.panel("No branch name provided") 86 | return 87 | self.run_command(['git', 'checkout', '-b', branchname], self.branch_done) 88 | 89 | def branch_done(self, result): 90 | self.panel(result) 91 | for view in self.window.views(): 92 | view.run_command("git_branch_status") 93 | 94 | 95 | class GitTrackRemoteBranchCommand(GitBranchCommand): 96 | command_to_run_after_branch = ['checkout', '--track'] 97 | extra_flags = ['--remote'] 98 | 99 | 100 | class GitSetUpstreamBranchCommand(GitBranchCommand): 101 | command_to_run_after_branch = ['branch', '--set-upstream-to'] 102 | extra_flags = ['--remote'] 103 | 104 | 105 | class GitNewTagCommand(GitWindowCommand): 106 | def run(self): 107 | self.get_window().show_input_panel("Tag name", "", self.on_input, None, None) 108 | 109 | def on_input(self, tagname): 110 | if not tagname.strip(): 111 | self.panel("No branch name provided") 112 | return 113 | self.run_command(['git', 'tag', tagname]) 114 | 115 | 116 | class GitDeleteTagCommand(GitWindowCommand): 117 | def run(self): 118 | self.run_command(['git', 'tag'], self.fetch_tag) 119 | 120 | def fetch_tag(self, result): 121 | if result.strip() == "": 122 | sublime.status_message("No Tags provided.") 123 | return 124 | self.results = result.rstrip().split('\n') 125 | self.quick_panel(self.results, self.panel_done) 126 | 127 | def panel_done(self, picked): 128 | if 0 > picked < len(self.results): 129 | return 130 | picked_tag = self.results[picked] 131 | picked_tag = picked_tag.strip() 132 | if sublime.ok_cancel_dialog("Delete \"%s\" Tag?" % picked_tag, "Delete"): 133 | self.run_command(['git', 'tag', '-d', picked_tag]) 134 | 135 | 136 | class GitShowTagsCommand(GitWindowCommand): 137 | def run(self): 138 | self.run_command(['git', 'tag'], self.fetch_tag) 139 | 140 | def fetch_tag(self, result): 141 | self.results = result.rstrip().split('\n') 142 | self.quick_panel(self.results, self.panel_done) 143 | 144 | def panel_done(self, picked): 145 | if 0 > picked < len(self.results): 146 | return 147 | picked_tag = self.results[picked] 148 | picked_tag = picked_tag.strip() 149 | self.run_command(['git', 'show', picked_tag]) 150 | 151 | 152 | class GitCheckoutTagCommand(GitWindowCommand): 153 | def run(self): 154 | self.run_command(['git', 'tag'], self.fetch_tag) 155 | 156 | def fetch_tag(self, result): 157 | if result.strip() == "": 158 | sublime.status_message("No Tags provided.") 159 | return 160 | self.results = result.rstrip().split('\n') 161 | self.quick_panel(self.results, self.panel_done) 162 | 163 | def panel_done(self, picked): 164 | if 0 > picked < len(self.results): 165 | return 166 | picked_tag = self.results[picked] 167 | picked_tag = picked_tag.strip() 168 | self.run_command(['git', 'checkout', "tags/%s" % picked_tag]) 169 | 170 | 171 | class GitPullCurrentBranchCommand(GitWindowCommand): 172 | command_to_run_after_describe = 'pull' 173 | 174 | def run(self): 175 | self.run_command(['git', 'describe', '--contains', '--all', 'HEAD'], callback=self.describe_done) 176 | 177 | def describe_done(self, result): 178 | self.current_branch = result.strip() 179 | self.run_command(['git', 'remote'], callback=self.remote_done) 180 | 181 | def remote_done(self, result): 182 | self.remotes = result.rstrip().split('\n') 183 | if len(self.remotes) == 1: 184 | self.panel_done() 185 | else: 186 | self.quick_panel(self.remotes, self.panel_done, sublime.MONOSPACE_FONT) 187 | 188 | def panel_done(self, picked=0): 189 | if picked < 0 or picked >= len(self.remotes): 190 | return 191 | self.picked_remote = self.remotes[picked] 192 | self.picked_remote = self.picked_remote.strip() 193 | self.run_command(['git', self.command_to_run_after_describe, self.picked_remote, self.current_branch]) 194 | 195 | 196 | class GitPushCurrentBranchCommand(GitPullCurrentBranchCommand): 197 | command_to_run_after_describe = 'push' 198 | -------------------------------------------------------------------------------- /git/stash.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | from . import GitWindowCommand 4 | 5 | 6 | class GitStashCommand(GitWindowCommand): 7 | may_change_files = True 8 | command_to_run_after_list = False 9 | 10 | def run(self): 11 | self.run_command(['git', 'stash', 'list'], self.stash_list_done) 12 | 13 | def stash_list_done(self, result): 14 | # No stash list at all 15 | if not result: 16 | self.panel('No stash found') 17 | return 18 | 19 | self.results = result.rstrip().split('\n') 20 | 21 | # If there is only one, apply it 22 | if len(self.results) == 1: 23 | self.stash_list_panel_done() 24 | else: 25 | self.quick_panel(self.results, self.stash_list_panel_done) 26 | 27 | def stash_list_panel_done(self, picked=0): 28 | if 0 > picked < len(self.results): 29 | return 30 | 31 | # get the stash ref (e.g. stash@{3}) 32 | stash = self.results[picked].split(':')[0] 33 | self.run_command(['git', 'stash'] + self.command_to_run_after_list + [stash], self.handle_command or self.generic_done, stash=stash) 34 | 35 | def handle_command(self, result, stash, **kw): 36 | return self.generic_done(result, **kw) 37 | 38 | 39 | class GitStashListCommand(GitStashCommand): 40 | may_change_files = False 41 | command_to_run_after_list = ['show', '-p'] 42 | 43 | def handle_command(self, result, stash, **kw): 44 | self.scratch(result, title=stash, syntax="Packages/Diff/Diff.tmLanguage") 45 | 46 | 47 | class GitStashApplyCommand(GitStashCommand): 48 | command_to_run_after_list = ['apply'] 49 | 50 | 51 | class GitStashDropCommand(GitStashCommand): 52 | command_to_run_after_list = ['drop'] 53 | -------------------------------------------------------------------------------- /git/status.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import os 4 | import re 5 | 6 | import sublime 7 | from . import GitWindowCommand, git_root 8 | 9 | 10 | class GitStatusCommand(GitWindowCommand): 11 | force_open = False 12 | 13 | def run(self): 14 | self.run_command(['git', 'status', '--porcelain'], self.status_done) 15 | 16 | def status_done(self, result): 17 | self.results = list(filter(self.status_filter, result.rstrip().split('\n'))) 18 | if len(self.results): 19 | self.show_status_list() 20 | else: 21 | sublime.status_message("Nothing to show") 22 | 23 | def show_status_list(self): 24 | self.quick_panel( 25 | self.results, self.panel_done, 26 | sublime.MONOSPACE_FONT 27 | ) 28 | 29 | def status_filter(self, item): 30 | # for this class we don't actually care 31 | if not re.match(r'^[ MADRCU?!]{1,2}\s+.*', item): 32 | return False 33 | return len(item) > 0 34 | 35 | def panel_done(self, picked): 36 | if 0 > picked < len(self.results): 37 | return 38 | picked_file = self.results[picked] 39 | if isinstance(picked_file, (list, tuple)): 40 | picked_file = picked_file[0] 41 | # first 2 characters are status codes, the third is a space 42 | picked_status = picked_file[:2] 43 | picked_file = picked_file[3:] 44 | self.panel_followup(picked_status, picked_file, picked) 45 | 46 | def panel_followup(self, picked_status, picked_file, picked_index): 47 | # split out solely so I can override it for laughs 48 | 49 | s = sublime.load_settings("Git.sublime-settings") 50 | root = git_root(self.get_working_dir()) 51 | if picked_status == '??' or s.get('status_opens_file') or self.force_open: 52 | file_name = os.path.join(root, picked_file) 53 | if(os.path.isfile(file_name)): 54 | # Sublime Text 3 has a bug wherein calling open_file from within a panel 55 | # callback causes the new view to not have focus. Make a deferred call via 56 | # set_timeout to workaround this issue. 57 | sublime.set_timeout(lambda: self.window.open_file(file_name), 0) 58 | else: 59 | if s.get('diff_tool'): 60 | self.run_command( 61 | ['git', 'difftool', '--', picked_file.strip('"')], 62 | working_dir=root 63 | ) 64 | else: 65 | self.run_command( 66 | ['git', 'diff', '--no-color', '--', picked_file.strip('"')], 67 | self.diff_done, working_dir=root 68 | ) 69 | 70 | def diff_done(self, result): 71 | if not result.strip(): 72 | return 73 | self.scratch(result, title="Git Diff") 74 | 75 | 76 | class GitOpenModifiedFilesCommand(GitStatusCommand): 77 | force_open = True 78 | 79 | def show_status_list(self): 80 | for line_index in range(0, len(self.results)): 81 | self.panel_done(line_index) 82 | -------------------------------------------------------------------------------- /git/statusbar.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import re 4 | 5 | import sublime 6 | import sublime_plugin 7 | from . import GitTextCommand 8 | 9 | 10 | class GitBranchStatusListener(sublime_plugin.EventListener): 11 | def on_activated(self, view): 12 | view.run_command("git_branch_status") 13 | 14 | def on_post_save(self, view): 15 | view.run_command("git_branch_status") 16 | 17 | 18 | class GitBranchStatusCommand(GitTextCommand): 19 | def run(self, view): 20 | s = sublime.load_settings("Git.sublime-settings") 21 | if s.get("statusbar_branch"): 22 | self.run_command(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], self.branch_done, show_status=False, no_save=True, error_suppresses_output=True) 23 | else: 24 | self.branch_done(False) 25 | if (s.get("statusbar_status")): 26 | self.run_command(['git', 'status', '--porcelain'], self.status_done, show_status=False, no_save=True, error_suppresses_output=True) 27 | else: 28 | self.status_done(False) 29 | 30 | def branch_done(self, result): 31 | if result is False: 32 | self.view.set_status("git-branch", "") 33 | else: 34 | self.view.set_status("git-branch", "Git branch: " + result.strip()) 35 | 36 | def status_done(self, result): 37 | if result is False: 38 | self.view.set_status("git-status-index", "") 39 | self.view.set_status("git-status-working", "") 40 | else: 41 | lines = [line for line in result.splitlines() if re.match(r'^[ MADRCU?!]{1,2}\s+.*', line)] 42 | index = [line[0] for line in lines if not line[0].isspace()] 43 | working = [line[1] for line in lines if not line[1].isspace()] 44 | self.view.set_status("git-status-index", "index: " + self.status_string(index)) 45 | self.view.set_status("git-status-working", "working: " + self.status_string(working)) 46 | 47 | def status_string(self, statuses): 48 | s = sublime.load_settings("Git.sublime-settings") 49 | symbols = s.get("statusbar_status_symbols") 50 | if not statuses: 51 | return symbols['clean'] 52 | status = [] 53 | if statuses.count('M'): 54 | status.append("%d%s" % (statuses.count('M'), symbols['modified'])) 55 | if statuses.count('A'): 56 | status.append("%d%s" % (statuses.count('A'), symbols['added'])) 57 | if statuses.count('D'): 58 | status.append("%d%s" % (statuses.count('D'), symbols['deleted'])) 59 | if statuses.count('?'): 60 | status.append("%d%s" % (statuses.count('?'), symbols['untracked'])) 61 | if statuses.count('U'): 62 | status.append("%d%s" % (statuses.count('U'), symbols['conflicts'])) 63 | if statuses.count('R'): 64 | status.append("%d%s" % (statuses.count('R'), symbols['renamed'])) 65 | if statuses.count('C'): 66 | status.append("%d%s" % (statuses.count('C'), symbols['copied'])) 67 | return symbols['separator'].join(status) 68 | -------------------------------------------------------------------------------- /git_commands.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, division 2 | 3 | import sys 4 | 5 | """This module collates the Git commands from the submodule 6 | 7 | ...it's a python 2 / 3 compatibility workaround, mostly. 8 | """ 9 | 10 | # Sublime doesn't reload submodules. This code is based on 1_reloader.py 11 | # in Package Control, and handles that. 12 | 13 | mod_prefix = 'git' 14 | 15 | # ST3 loads each package as a module, so it needs an extra prefix 16 | if sys.version_info >= (3,): 17 | bare_mod_prefix = mod_prefix 18 | mod_prefix = 'Git.' + mod_prefix 19 | from imp import reload 20 | 21 | # Modules have to be reloaded in dependency order. So list 'em here: 22 | mods_load_order = [ 23 | '', 24 | 25 | '.status', 26 | '.add', # imports status 27 | '.index', # imports status 28 | '.commit', # imports add 29 | 30 | # no interdependencies below 31 | '.core', 32 | '.annotate', 33 | '.config', 34 | '.diff', 35 | '.history', 36 | '.ignore', 37 | '.repo', 38 | '.stash', 39 | '.statusbar', 40 | '.flow', 41 | '.file', 42 | ] 43 | 44 | reload_mods = [mod for mod in sys.modules if mod[0:3] in ('git', 'Git') and sys.modules[mod] is not None] 45 | 46 | reloaded = [] 47 | for suffix in mods_load_order: 48 | mod = mod_prefix + suffix 49 | if mod in reload_mods: 50 | reload(sys.modules[mod]) 51 | reloaded.append(mod) 52 | 53 | if reloaded: 54 | print("Git: reloaded submodules", reloaded) 55 | 56 | # Now actually import all the commands so they'll be visible to Sublime 57 | try: 58 | # Python 3 59 | from .git.core import * # noqa 60 | 61 | from .git.add import * # noqa 62 | from .git.index import * # noqa 63 | from .git.annotate import * # noqa 64 | from .git.config import * # noqa 65 | from .git.commit import * # noqa 66 | from .git.diff import * # noqa 67 | from .git.flow import * # noqa 68 | from .git.history import * # noqa 69 | from .git.file import * # noqa 70 | from .git.ignore import * # noqa 71 | from .git.repo import * # noqa 72 | from .git.stash import * # noqa 73 | from .git.status import * # noqa 74 | from .git.statusbar import * # noqa 75 | except (ImportError, ValueError): 76 | # Python 2 77 | from git.core import * # noqa 78 | 79 | from git.add import * # noqa 80 | from git.index import * # noqa 81 | from git.annotate import * # noqa 82 | from git.config import * # noqa 83 | from git.commit import * # noqa 84 | from git.diff import * # noqa 85 | from git.flow import * # noqa 86 | from git.history import * # noqa 87 | from git.file import * # noqa 88 | from git.ignore import * # noqa 89 | from git.repo import * # noqa 90 | from git.stash import * # noqa 91 | from git.status import * # noqa 92 | from git.statusbar import * # noqa 93 | -------------------------------------------------------------------------------- /syntax/Git Blame.JSON-tmLanguage: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [ 3 | "git-blame" 4 | ], 5 | "name": "Git Blame", 6 | "scopeName": "text.git-blame", 7 | "patterns": [ 8 | { 9 | "name": "line.comment.git-blame", 10 | "match": "^(\\^?[a-f0-9]+)\\s+([\\w\\-\\d\\.\\/\\s]*?)\\s*\\((.*?)\\s+(\\d{4}-\\d\\d-\\d\\d( \\d\\d:\\d\\d:\\d\\d [+-]\\d{4})?)\\s+(\\d+)\\)", 11 | "captures": { 12 | "5": { 13 | "name": "variable.parameter.line-number.git-blame" 14 | }, 15 | "4": { 16 | "name": "constant.numeric.date.git-blame" 17 | }, 18 | "1": { 19 | "name": "string.sha.git-blame" 20 | }, 21 | "3": { 22 | "name": "support.function.author.git-blame" 23 | }, 24 | "2": { 25 | "name": "string.path.git-blame" 26 | } 27 | } 28 | } 29 | ], 30 | "uuid": "5d37add9-889e-4174-b232-4bd423b84c0a" 31 | } 32 | -------------------------------------------------------------------------------- /syntax/Git Blame.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | git-blame 8 | 9 | name 10 | Git Blame 11 | patterns 12 | 13 | 14 | captures 15 | 16 | 1 17 | 18 | name 19 | string.sha.git-blame 20 | 21 | 2 22 | 23 | name 24 | string.path.git-blame 25 | 26 | 3 27 | 28 | name 29 | support.function.author.git-blame 30 | 31 | 4 32 | 33 | name 34 | constant.numeric.date.git-blame 35 | 36 | 5 37 | 38 | name 39 | variable.parameter.line-number.git-blame 40 | 41 | 42 | match 43 | ^(\^?[a-f0-9]+)\s+([\w\-\d\.\/\s]*?)\s*\((.*?)\s+(\d{4}-\d\d-\d\d( \d\d:\d\d:\d\d [+-]\d{4})?)\s+(\d+)\) 44 | name 45 | line.comment.git-blame 46 | 47 | 48 | scopeName 49 | text.git-blame 50 | uuid 51 | 5d37add9-889e-4174-b232-4bd423b84c0a 52 | 53 | 54 | -------------------------------------------------------------------------------- /syntax/Git Commit Message.JSON-tmLanguage: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Git Commit Message", 3 | "scopeName": "text.git-commit", 4 | "fileTypes": [ 5 | "COMMIT_EDITMSG" 6 | ], 7 | "patterns": [ 8 | { 9 | "name": "comment.line.number-sign.git-commit-message", 10 | "begin": "^#", 11 | "beginCaptures": { 12 | "0": { 13 | "name": "punctuation.definition.comment.git-commit-message" 14 | } 15 | }, 16 | "end": "$", 17 | "patterns": [ 18 | { 19 | "name": "comment.line.on-branch.git-commit-message", 20 | "match": "(?:On branch )([^ ]+)", 21 | "captures": { 22 | "1": { 23 | "name": "support.function.branch.git-commit-message" 24 | } 25 | } 26 | }, 27 | { 28 | "name": "comment.line.on-branch.git-commit-message", 29 | "match": "Your branch .* '([^ ']+)'", 30 | "captures": { 31 | "1": { 32 | "name": "support.function.branch.git-commit-message" 33 | } 34 | } 35 | }, 36 | { 37 | "name": "comment.line.untracked.git-commit-message", 38 | "begin": " Untracked files:", 39 | "beginCaptures": { 40 | "0": { 41 | "name": "entity.definition.untracked.git-commit-message" 42 | } 43 | }, 44 | "end": "^#$", 45 | "patterns": [ 46 | { 47 | "name": "comment.line.untracked-file.git-commit-message", 48 | "match": "\t(.*)$", 49 | "captures": { 50 | "1": { 51 | "name": "support.function.file-status.git-commit-message" 52 | }, 53 | "2": { 54 | "name": "constant.character.branch.git-commit-message" 55 | } 56 | } 57 | } 58 | ] 59 | }, 60 | { 61 | "name": "comment.line.discarded.git-commit-message", 62 | "begin": " Change(?:s not staged for commit|d but not updated):", 63 | "beginCaptures": { 64 | "0": { 65 | "name": "entity.definition.discarded.git-commit-message" 66 | } 67 | }, 68 | "end": "^#$", 69 | "patterns": [ 70 | { 71 | "name": "comment.line.discarded.git-commit-message", 72 | "match": "\t([^:]+):(.*)$", 73 | "captures": { 74 | "1": { 75 | "name": "support.function.file-status.git-commit-message" 76 | }, 77 | "2": { 78 | "name": "constant.character.branch.git-commit-message" 79 | } 80 | } 81 | } 82 | ] 83 | }, 84 | { 85 | "name": "comment.line.selected.git-commit-message", 86 | "begin": " Changes to be committed:", 87 | "beginCaptures": { 88 | "0": { 89 | "name": "entity.definition.selected.git-commit-message" 90 | } 91 | }, 92 | "end": "^#$", 93 | "patterns": [ 94 | { 95 | "name": "comment.line.selected.git-commit-message", 96 | "match": "\t([^:]+):(.*)$", 97 | "captures": { 98 | "1": { 99 | "name": "support.function.file-status.git-commit-message" 100 | }, 101 | "2": { 102 | "name": "constant.character.branch.git-commit-message" 103 | } 104 | } 105 | } 106 | ] 107 | } 108 | ] 109 | }, 110 | { 111 | "name": "meta.diff.git-commit", 112 | "comment": "diff at the end of the commit message when using commit -v, or viewing a log. End pattern is just something to be never matched so that the meta continues untill the end of the file.", 113 | "begin": "diff\\ \\-\\-git", 114 | "end": "(?=xxxxxx)123457", 115 | "patterns": [ 116 | { 117 | "include": "source.diff" 118 | } 119 | ] 120 | } 121 | ], 122 | "uuid": "de3fb2fc-e564-4a31-9813-5ee26967c5c8" 123 | } 124 | -------------------------------------------------------------------------------- /syntax/Git Commit Message.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | COMMIT_EDITMSG 8 | 9 | name 10 | Git Commit Message 11 | patterns 12 | 13 | 14 | begin 15 | ^# 16 | beginCaptures 17 | 18 | 0 19 | 20 | name 21 | punctuation.definition.comment.git-commit-message 22 | 23 | 24 | end 25 | $ 26 | name 27 | comment.line.number-sign.git-commit-message 28 | patterns 29 | 30 | 31 | captures 32 | 33 | 1 34 | 35 | name 36 | support.function.branch.git-commit-message 37 | 38 | 39 | match 40 | (?:On branch )([^ ]+) 41 | name 42 | comment.line.on-branch.git-commit-message 43 | 44 | 45 | captures 46 | 47 | 1 48 | 49 | name 50 | support.function.branch.git-commit-message 51 | 52 | 53 | match 54 | Your branch .* '([^ ']+)' 55 | name 56 | comment.line.on-branch.git-commit-message 57 | 58 | 59 | begin 60 | Untracked files: 61 | beginCaptures 62 | 63 | 0 64 | 65 | name 66 | entity.definition.untracked.git-commit-message 67 | 68 | 69 | end 70 | ^#$ 71 | name 72 | comment.line.untracked.git-commit-message 73 | patterns 74 | 75 | 76 | captures 77 | 78 | 1 79 | 80 | name 81 | support.function.file-status.git-commit-message 82 | 83 | 2 84 | 85 | name 86 | constant.character.branch.git-commit-message 87 | 88 | 89 | match 90 | (.*)$ 91 | name 92 | comment.line.untracked-file.git-commit-message 93 | 94 | 95 | 96 | 97 | begin 98 | Change(?:s not staged for commit|d but not updated): 99 | beginCaptures 100 | 101 | 0 102 | 103 | name 104 | entity.definition.discarded.git-commit-message 105 | 106 | 107 | end 108 | ^#$ 109 | name 110 | comment.line.discarded.git-commit-message 111 | patterns 112 | 113 | 114 | captures 115 | 116 | 1 117 | 118 | name 119 | support.function.file-status.git-commit-message 120 | 121 | 2 122 | 123 | name 124 | constant.character.branch.git-commit-message 125 | 126 | 127 | match 128 | ([^:]+):(.*)$ 129 | name 130 | comment.line.discarded.git-commit-message 131 | 132 | 133 | 134 | 135 | begin 136 | Changes to be committed: 137 | beginCaptures 138 | 139 | 0 140 | 141 | name 142 | entity.definition.selected.git-commit-message 143 | 144 | 145 | end 146 | ^#$ 147 | name 148 | comment.line.selected.git-commit-message 149 | patterns 150 | 151 | 152 | captures 153 | 154 | 1 155 | 156 | name 157 | support.function.file-status.git-commit-message 158 | 159 | 2 160 | 161 | name 162 | constant.character.branch.git-commit-message 163 | 164 | 165 | match 166 | ([^:]+):(.*)$ 167 | name 168 | comment.line.selected.git-commit-message 169 | 170 | 171 | 172 | 173 | 174 | 175 | begin 176 | diff\ \-\-git 177 | comment 178 | diff at the end of the commit message when using commit -v, or viewing a log. End pattern is just something to be never matched so that the meta continues untill the end of the file. 179 | end 180 | (?=xxxxxx)123457 181 | name 182 | meta.diff.git-commit 183 | patterns 184 | 185 | 186 | include 187 | source.diff 188 | 189 | 190 | 191 | 192 | scopeName 193 | text.git-commit 194 | uuid 195 | de3fb2fc-e564-4a31-9813-5ee26967c5c8 196 | 197 | 198 | -------------------------------------------------------------------------------- /syntax/Git Commit Message.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Git Commit Message 7 | scope 8 | text.git-commit 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START 16 | value 17 | # 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /syntax/Git Commit View.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | git-commit-view 8 | 9 | name 10 | Git Commit View 11 | patterns 12 | 13 | 14 | name 15 | string.sha.git-blame 16 | match 17 | ^commit [a-f0-9]+$ 18 | 19 | 20 | name 21 | support.function.author.git-blame 22 | match 23 | ^Author: .+$ 24 | 25 | 26 | name 27 | constant.numeric.date.git-blame 28 | match 29 | ^Date: .+$ 30 | 31 | 32 | match 33 | (^diff --.+$) 34 | name 35 | string.path.git-diff 36 | 37 | 38 | match 39 | (^(((-{3}) .+)|((\*{3}) .+))$\n?|^(={4}) .+(?= - )) 40 | name 41 | meta.diff.header.from-file 42 | 43 | 44 | match 45 | (^(\+{3}) .+$\n?| (-) .* (={4})$\n?) 46 | name 47 | meta.diff.header.to-file 48 | 49 | 50 | captures 51 | 52 | 1 53 | 54 | name 55 | punctuation.definition.range.diff 56 | 57 | 2 58 | 59 | name 60 | meta.toc-list.line-number.diff 61 | 62 | 3 63 | 64 | name 65 | punctuation.definition.range.diff 66 | 67 | 68 | match 69 | ^(@@)\s*(.+?)\s*(@@)($\n?)? 70 | name 71 | meta.diff.range.unified 72 | 73 | 74 | captures 75 | 76 | 3 77 | 78 | name 79 | punctuation.definition.inserted.diff 80 | 81 | 6 82 | 83 | name 84 | punctuation.definition.inserted.diff 85 | 86 | 87 | match 88 | ^(((>)( .*)?)|((\+).*))$\n? 89 | name 90 | markup.inserted.diff 91 | 92 | 93 | captures 94 | 95 | 1 96 | 97 | name 98 | punctuation.definition.inserted.diff 99 | 100 | 101 | match 102 | ^(!).*$\n? 103 | name 104 | markup.changed.diff 105 | 106 | 107 | captures 108 | 109 | 3 110 | 111 | name 112 | punctuation.definition.inserted.diff 113 | 114 | 6 115 | 116 | name 117 | punctuation.definition.inserted.diff 118 | 119 | 120 | match 121 | ^(((<)( .*)?)|((-).*))$\n? 122 | name 123 | markup.deleted.diff 124 | 125 | 126 | captures 127 | 128 | 1 129 | 130 | name 131 | meta.toc-list.file-name.diff 132 | 133 | 134 | match 135 | ^index (.+)$\n? 136 | name 137 | meta.diff.index 138 | 139 | 140 | scopeName 141 | text.git-commit-view 142 | uuid 143 | 5d37add9-1219-4174-b232-4bd423b84c0a 144 | 145 | 146 | -------------------------------------------------------------------------------- /syntax/Git Diff.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | # http://www.sublimetext.com/docs/3/syntax.html 4 | name: Git Diff 5 | file_extensions: 6 | - diff 7 | - patch 8 | first_line_match: |- 9 | (?x)^ 10 | (===\ modified\ file 11 | |==== \s* // .+ \s - \s .+ \s+ ==== 12 | |Index:[ ] 13 | |---\ [^%] 14 | |\*\*\*.*\d{4}\s*$ 15 | |\d+(,\d+)* (a|d|c) \d+(,\d+)* $ 16 | |diff\ --git[ ] 17 | ) 18 | 19 | scope: source.diff 20 | contexts: 21 | main: 22 | - match: '^((\*{15})|(={67})|(-{3}))$\n?' 23 | scope: meta.separator.diff 24 | captures: 25 | 1: punctuation.definition.separator.diff 26 | - match: ^\d+(,\d+)*(a|d|c)\d+(,\d+)*$\n? 27 | scope: meta.diff.range.normal 28 | - match: ^(@@)\s*(.+?)\s*(@@)($\n?)? 29 | scope: meta.diff.range.unified 30 | captures: 31 | 1: punctuation.definition.range.diff 32 | 2: meta.toc-list.line-number.diff 33 | 3: punctuation.definition.range.diff 34 | - match: '^(((\-{3}) .+ (\-{4}))|((\*{3}) .+ (\*{4})))$\n?' 35 | scope: meta.diff.range.context 36 | captures: 37 | 3: punctuation.definition.range.diff 38 | 4: punctuation.definition.range.diff 39 | 6: punctuation.definition.range.diff 40 | 7: punctuation.definition.range.diff 41 | - match: '(^(((-{3}) .+)|((\*{3}) .+))$\n?|^(={4}) .+(?= - ))' 42 | scope: meta.diff.header.from-file 43 | captures: 44 | 4: punctuation.definition.from-file.diff 45 | 6: punctuation.definition.from-file.diff 46 | 7: punctuation.definition.from-file.diff 47 | - match: '(^(\+{3}) .+$\n?| (-) .* (={4})$\n?)' 48 | scope: meta.diff.header.to-file 49 | captures: 50 | 2: punctuation.definition.to-file.diff 51 | 3: punctuation.definition.to-file.diff 52 | 4: punctuation.definition.to-file.diff 53 | - match: ^(((>)( .*)?)|((\+).*))$\n? 54 | scope: markup.inserted.diff 55 | captures: 56 | 3: punctuation.definition.inserted.diff 57 | 6: punctuation.definition.inserted.diff 58 | - match: ^(((<)( .*)?)|((-).*))$\n? 59 | scope: markup.deleted.diff 60 | captures: 61 | 3: punctuation.definition.inserted.diff 62 | 6: punctuation.definition.inserted.diff 63 | - match: ^Index(:) (.+)$\n? 64 | scope: meta.diff.index 65 | captures: 66 | 1: punctuation.separator.key-value.diff 67 | 2: meta.toc-list.file-name.diff 68 | - match: \{\+.+?\+\} 69 | scope: markup.inserted.diff 70 | - match: \[\-.+?\-\] 71 | scope: markup.deleted.diff 72 | -------------------------------------------------------------------------------- /syntax/Git Graph.JSON-tmLanguage: -------------------------------------------------------------------------------- 1 | { "name": "Git Graph", 2 | "scopeName": "text.git-graph", 3 | "fileTypes": ["git-graph"], 4 | "patterns": [ 5 | { "match": "^([| *\\\\]+)([0-9a-f]{4,40}) (.*?) (\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d [+-]\\d{4}) (?:\\(((?:[a-zA-Z0-9._\\-\\/]+(?:, )?)+)\\) )?", 6 | "name": "log-entry.git-graph", 7 | "captures": { 8 | "1": {"name": "comment.git-graph" }, 9 | "2": {"name": "string.sha.git-graph" }, 10 | "3": {"name": "support.function.git-graph" }, 11 | "4": {"name": "constant.numeric.git-graph" }, 12 | "5": {"name": "variable.parameter.git-graph" } 13 | } 14 | }, 15 | { "match": "^\\|[\\|_\\/\\\\ ]+\n?$", 16 | "name": "comment.git-graph", 17 | "comment": "lines with no commit details" 18 | }, 19 | { "match": "(?:[Ff]ix(?:e[ds])?|[Rr]esolve[ds]?|[Cc]lose[ds]?)?\\s*(?:#\\d+|\\[.*?\\])", 20 | "name": "keyword.git-graph", 21 | "comment": "issue numbers" 22 | }, 23 | { "match": "Merge branch '(.*?)' of .*?\n?$", 24 | "name": "comment.git-graph", 25 | "captures": { 26 | "1": {"name": "variable.parameter.git-graph"} 27 | } 28 | } 29 | ], 30 | "uuid": "b900521e-af64-471b-aec8-1ecf88aab595" 31 | } 32 | -------------------------------------------------------------------------------- /syntax/Git Graph.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | git-graph 8 | 9 | name 10 | Git Graph 11 | patterns 12 | 13 | 14 | captures 15 | 16 | 1 17 | 18 | name 19 | comment.git-graph 20 | 21 | 2 22 | 23 | name 24 | string.sha.git-graph 25 | 26 | 3 27 | 28 | name 29 | support.function.git-graph 30 | 31 | 4 32 | 33 | name 34 | constant.numeric.git-graph 35 | 36 | 5 37 | 38 | name 39 | variable.parameter.git-graph 40 | 41 | 6 42 | 43 | name 44 | keyword.git-graph 45 | 46 | 47 | match 48 | ^([| *\\]+)([0-9a-f]{4,40}) -( \(.*?\))? (.*) (\(.*\)) (<.*?>) .* 49 | name 50 | log-entry.git-graph 51 | 52 | 53 | comment 54 | lines with no commit details 55 | match 56 | ^\|[\|_\/\\ ]+ 57 | ?$ 58 | name 59 | comment.git-graph 60 | 61 | 62 | comment 63 | issue numbers 64 | match 65 | (?:[Ff]ix(?:e[ds])?|[Rr]esolve[ds]?|[Cc]lose[ds]?)?\s*(?:#\d+|\[.*?\]) 66 | name 67 | keyword.git-graph 68 | 69 | 70 | captures 71 | 72 | 1 73 | 74 | name 75 | variable.parameter.git-graph 76 | 77 | 78 | match 79 | Merge branch '(.*?)' of .*? 80 | ?$ 81 | name 82 | comment.git-graph 83 | 84 | 85 | scopeName 86 | text.git-graph 87 | uuid 88 | b900521e-af64-471b-aec8-1ecf88aab595 89 | 90 | 91 | --------------------------------------------------------------------------------