├── .gitignore ├── .hgignore ├── .hgtags ├── CHANGES.txt ├── Default.sublime-keymap ├── LICENSE.txt ├── MANIFEST.in ├── PackageTesting.json ├── README.rst ├── Support ├── Mercurial Annotate.JSON-tmLanguage ├── Mercurial Annotate.hidden-tmLanguage ├── Mercurial Annotate.tmLanguage ├── Mercurial Log.JSON-tmLanguage ├── Mercurial Log.hidden-tmLanguage ├── Mercurial Log.tmLanguage ├── Mercurial Status Report.JSON-tmLanguage ├── Mercurial Status Report.hidden-tmLanguage ├── Mercurial Status Report.tmLanguage ├── Preferences.sublime-settings ├── Sublime Hg CLI.JSON-tmLanguage ├── Sublime Hg CLI.hidden-tmLanguage ├── Sublime Hg CLI.tmLanguage ├── SublimeHG.sublime-commands ├── SublimeHg Command Line.JSON-tmLanguage ├── SublimeHg Command Line.hidden-tmLanguage ├── SublimeHg Command Line.sublime-settings └── SublimeHg Command Line.tmLanguage ├── bin ├── CleanUp.ps1 └── MakeRelease.ps1 ├── hg_actions.py ├── setup.py ├── shglib ├── __init__.py ├── client.py ├── commands.py ├── parsing.py └── utils.py ├── sublime_hg.py ├── sublime_hg_cli.py └── tests ├── __init__.py ├── sublime.py ├── sublime_plugin.py ├── test_sublime_hg.py └── test_utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.cache 3 | *.sublime-project -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | tags 4 | tags_sorted_by_file 5 | 6 | *.hgtags 7 | 8 | *.pyc 9 | 10 | *.cache 11 | *.sublime-workspace 12 | *.sublime-project 13 | 14 | _*.txt 15 | history.txt 16 | 17 | MANIFEST 18 | 19 | # hglib/ 20 | dist/ 21 | build/ 22 | data/ 23 | Doc/ 24 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 4539ce1d7f61dddc74e534da8ba7d62c770be438 threaded 2 | 8bb0007e079168f34fba51b2710ab36931207479 pre-cli 3 | c331141f3fe20ed1473e02700e3d329f5266db00 pre-simple-client 4 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 12.9.27 2 | - enable use of double quotes in user-supplied input 3 | 12.6.8 4 | - add command to kill servers 5 | - improve error handling when current view does not belong to any repo 6 | 11.9.8 7 | - tab completion for top level commands 8 | - command history 9 | 10 | 11.9.3 11 | - use hglib (by Mercurial team) as client for the command server 12 | - shell is restored when originating view is re-focused 13 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["enter"], "command": "sublime_hg_send_line", "context": [{"key": "selector", "operand": "source.sublime_hg_cli"}]}, 3 | {"keys": ["ctrl+enter"], "command": "sublime_hg_diff_selected", "context": [{ "key": "selector", "operand": "text.mercurial-log" }]}, 4 | {"keys": ["ctrl+shift+enter"], "command": "sublime_hg_update_to_revision", "context": [{ "key": "selector", "operand": "text.mercurial-log" }]} 5 | ] 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Guillermo López-Anglada 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-include *.py *.txt 2 | global-include *.sublime-* *.hidden-tmLanguage 3 | 4 | exclude setup.py 5 | exclude *.sublime-project 6 | exclude *.sublime-workspace 7 | exclude _*.txt 8 | exclude *.cache 9 | 10 | graft Doc 11 | prune dist 12 | prune data 13 | prune tests 14 | -------------------------------------------------------------------------------- /PackageTesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "working_dir": "SublimeHg", 3 | "data": { 4 | "main": "tests/data/data.txt" 5 | }, 6 | 7 | "test_suites": { 8 | "utils": ["package_testing_run_data_file_based_tests", "tests.test_utils"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | SublimeHg 3 | ========= 4 | 5 | Issue commands to Mercurial from Sublime Text. 6 | 7 | 8 | Requirements 9 | ============ 10 | 11 | * Mercurial command server (Mercurial 1.9 or above) 12 | 13 | 14 | Getting Started 15 | =============== 16 | 17 | - `Download`_ and install SublimeHg. (See the `installation instructions`_ for *.sublime-package* files.) 18 | 19 | .. _Download: https://bitbucket.org/guillermooo/sublimehg/downloads/SublimeHg.sublime-package 20 | .. _installation instructions: http://docs.sublimetext.info/en/latest/extensibility/packages.html#installation-of-sublime-package-files 21 | 22 | 23 | Configuration 24 | ============= 25 | 26 | These options can be set in **Preferences | Settings - User**. 27 | 28 | ``packages.sublime_hg.hg_exe`` 29 | 30 | By default, the executable name for Mercurial is ``hg``. If you need to 31 | use a different one, such as ``hg.bat``, change this option. 32 | 33 | Example:: 34 | 35 | { 36 | "packages.sublime_hg.hg_exe": "hg.bat" 37 | } 38 | 39 | ``packages.sublime_hg.terminal`` 40 | 41 | Determines the terminal emulator to be used in Linux. Some commands, such 42 | as *serve*, need this information to work. 43 | 44 | ``packages.sublime_hg.extensions`` 45 | 46 | A list of Mercurial extension names. Commands belonging to these extensions 47 | will show up in the SublimeHg quick panel along with built-in Mercurial 48 | commands. 49 | 50 | 51 | How to Use 52 | ========== 53 | 54 | SublimeHg can be used in two ways: 55 | 56 | - Through a *menu* (``show_sublime_hg_menu`` command). 57 | - Through a *command-line* interface (``show_sublime_hg_cli`` command). 58 | 59 | Regardless of the method used, SublimeHg ultimately talks to the Mercurial 60 | command server. The command-line interface is the more flexible option, but 61 | some operations might be quicker through the menu. 62 | 63 | By default, you have to follow these steps to use SublimeHg: 64 | 65 | #. Open the Command Palette (``Ctrl+Shift+P``) and look for ``SublimeHg``. 66 | #. Select option 67 | #. Select Mercurial command (or type in command line) 68 | 69 | It is however **recommended to assign** ``show_sublime_hg_cli`` and 70 | ``show_sublime_hg_menu`` their own **key bindings**. 71 | 72 | For example:: 73 | 74 | { "keys": ["ctrl+k", "ctrl+k"], "command": "show_sublime_hg_menu" }, 75 | { "keys": ["ctrl+shift+k"], "command": "show_sublime_hg_cli" }, 76 | 77 | 78 | Restarting the Current Server 79 | ============================= 80 | 81 | The Mercurial command server will not detect changes to the repository made 82 | from the outside (perhaps from a command line) while it is running. To restart 83 | the current server so that external changes are picked up, select 84 | *Kill Current Server* from the command palette. 85 | 86 | Tab Completion 87 | ============== 88 | 89 | While in the command-line, top level commands will be autocompleted when you 90 | press ``Tab``. 91 | 92 | 93 | Quick Actions 94 | ============= 95 | 96 | In some situations, you can perform quick actions. 97 | 98 | Log 99 | *** 100 | 101 | In a log report (``text.mercurial-log``), select two commit numbers (``keyword.other.changeset-ref.short.mercurial-log``) 102 | and press *CTRL+ENTER* to **diff the two revisions** (``diff -rSMALLER_REV_NR:LARGER_REV_NR``). 103 | 104 | If you want to **update to a revision number**, select a commit number and press *CTRL+SHIFT+ENTER* (``update REV_NR``). 105 | 106 | 107 | Donations 108 | ========= 109 | 110 | You can tip me through www.gittip.com: guillermooo_. 111 | 112 | .. _guillermooo: http://www.gittip.com/guillermooo/ 113 | -------------------------------------------------------------------------------- /Support/Mercurial Annotate.JSON-tmLanguage: -------------------------------------------------------------------------------- 1 | { "name": "Mercurial Annotate", 2 | "scopeName": "text.mercurial-annotate", 3 | "patterns": [ 4 | { "match": "^\\s*(\\d+)(:)", 5 | "captures": { 6 | "1": { "name": "constant.numeric.line-number.mercurial-annotate" }, 7 | "2": { "name": "keyword.other.mercurial-annotate" } 8 | } 9 | } 10 | ], 11 | "uuid": "c6d14ed9-34bb-4416-b891-7f557f3515df" 12 | } -------------------------------------------------------------------------------- /Support/Mercurial Annotate.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Mercurial Annotate 7 | patterns 8 | 9 | 10 | captures 11 | 12 | 1 13 | 14 | name 15 | constant.numeric.line-number.mercurial-annotate 16 | 17 | 2 18 | 19 | name 20 | keyword.other.mercurial-annotate 21 | 22 | 23 | match 24 | ^\s*(\d+)(:) 25 | 26 | 27 | scopeName 28 | text.mercurial-annotate 29 | uuid 30 | c6d14ed9-34bb-4416-b891-7f557f3515df 31 | 32 | 33 | -------------------------------------------------------------------------------- /Support/Mercurial Annotate.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Mercurial Annotate 7 | patterns 8 | 9 | 10 | captures 11 | 12 | 1 13 | 14 | name 15 | constant.numeric.line-number.mercurial-annotate 16 | 17 | 2 18 | 19 | name 20 | keyword.other.mercurial-annotate 21 | 22 | 23 | match 24 | ^\s*(\d+)(:) 25 | 26 | 27 | scopeName 28 | text.mercurial-annotate 29 | uuid 30 | c6d14ed9-34bb-4416-b891-7f557f3515df 31 | 32 | 33 | -------------------------------------------------------------------------------- /Support/Mercurial Log.JSON-tmLanguage: -------------------------------------------------------------------------------- 1 | { "name": "Mercurial Log", 2 | "scopeName": "text.mercurial-log", 3 | "patterns": [ 4 | 5 | { "match": "^(changeset):\\s+(\\d+):([0-9a-z]+)$", 6 | "captures": { 7 | "1": { "name": "support.function.mercurial-log" }, 8 | "2": { "name": "keyword.other.changeset-ref.short.mercurial-log" }, 9 | "3": { "name": "entity.other.attribute-name.changeset-ref.long.mercurial-log" } 10 | } 11 | }, 12 | 13 | { "match": "^(user):\\s+(.+?) (<)(.+)(>)$", 14 | "captures": { 15 | "1": { "name": "support.function.mercurial-log" }, 16 | "2": { "name": "string.user.name.mercurial-log" }, 17 | "3": { "name": "keyword.other.mercurial-log" }, 18 | "4": { "name": "constant.numeric.user.email.mercurial-log" }, 19 | "5": { "name": "keyword.other.mercurial-log" } 20 | } 21 | }, 22 | 23 | { "match": "^(\\w+):\\s+(.+)$", 24 | "captures": { 25 | "1": { "name": "support.function.mercurial-log" }, 26 | "2": { "name": "string.info.mercurial-log" } 27 | } 28 | } 29 | ], 30 | "uuid": "25e7259d-96d0-4ac0-83c7-f04acd0c8b84" 31 | } -------------------------------------------------------------------------------- /Support/Mercurial Log.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Mercurial Log 7 | patterns 8 | 9 | 10 | captures 11 | 12 | 1 13 | 14 | name 15 | support.function.mercurial-log 16 | 17 | 2 18 | 19 | name 20 | keyword.other.changeset-ref.short.mercurial-log 21 | 22 | 3 23 | 24 | name 25 | entity.other.attribute-name.changeset-ref.long.mercurial-log 26 | 27 | 28 | match 29 | ^(changeset):\s+(\d+):([0-9a-z]+)$ 30 | 31 | 32 | captures 33 | 34 | 1 35 | 36 | name 37 | support.function.mercurial-log 38 | 39 | 2 40 | 41 | name 42 | string.user.name.mercurial-log 43 | 44 | 3 45 | 46 | name 47 | keyword.other.mercurial-log 48 | 49 | 4 50 | 51 | name 52 | constant.numeric.user.email.mercurial-log 53 | 54 | 5 55 | 56 | name 57 | keyword.other.mercurial-log 58 | 59 | 60 | match 61 | ^(user):\s+(.+?) (<)(.+)(>)$ 62 | 63 | 64 | captures 65 | 66 | 1 67 | 68 | name 69 | support.function.mercurial-log 70 | 71 | 2 72 | 73 | name 74 | string.info.mercurial-log 75 | 76 | 77 | match 78 | ^(\w+):\s+(.+)$ 79 | 80 | 81 | scopeName 82 | text.mercurial-log 83 | uuid 84 | 25e7259d-96d0-4ac0-83c7-f04acd0c8b84 85 | 86 | 87 | -------------------------------------------------------------------------------- /Support/Mercurial Log.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Mercurial Log 7 | patterns 8 | 9 | 10 | captures 11 | 12 | 1 13 | 14 | name 15 | support.function.mercurial-log 16 | 17 | 2 18 | 19 | name 20 | keyword.other.changeset-ref.short.mercurial-log 21 | 22 | 3 23 | 24 | name 25 | entity.other.attribute-name.changeset-ref.long.mercurial-log 26 | 27 | 28 | match 29 | ^(changeset):\s+(\d+):([0-9a-z]+)$ 30 | 31 | 32 | captures 33 | 34 | 1 35 | 36 | name 37 | support.function.mercurial-log 38 | 39 | 2 40 | 41 | name 42 | string.user.name.mercurial-log 43 | 44 | 3 45 | 46 | name 47 | keyword.other.mercurial-log 48 | 49 | 4 50 | 51 | name 52 | constant.numeric.user.email.mercurial-log 53 | 54 | 5 55 | 56 | name 57 | keyword.other.mercurial-log 58 | 59 | 60 | match 61 | ^(user):\s+(.+?) (<)(.+)(>)$ 62 | 63 | 64 | captures 65 | 66 | 1 67 | 68 | name 69 | support.function.mercurial-log 70 | 71 | 2 72 | 73 | name 74 | string.info.mercurial-log 75 | 76 | 77 | match 78 | ^(\w+):\s+(.+)$ 79 | 80 | 81 | scopeName 82 | text.mercurial-log 83 | uuid 84 | 25e7259d-96d0-4ac0-83c7-f04acd0c8b84 85 | 86 | 87 | -------------------------------------------------------------------------------- /Support/Mercurial Status Report.JSON-tmLanguage: -------------------------------------------------------------------------------- 1 | { "name": "Mercurial Status Report", 2 | "scopeName": "text.mercurial-status-report", 3 | "patterns": [ 4 | { "match": "^([AM!?])\\s+(.+)$", 5 | "captures": { 6 | "1": { "name": "support.function.status.mercurial-status-report" }, 7 | "2": { "name": "string.file-name.mercurial-status-report" } 8 | } 9 | }, 10 | 11 | { "name": "string.mercurial-status-report", 12 | "match": ".+" 13 | } 14 | 15 | ], 16 | "uuid": "6086c539-0f15-4f54-a43b-3db7bb6ff106" 17 | } -------------------------------------------------------------------------------- /Support/Mercurial Status Report.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Mercurial Status Report 7 | patterns 8 | 9 | 10 | captures 11 | 12 | 1 13 | 14 | name 15 | support.function.status.mercurial-status-report 16 | 17 | 2 18 | 19 | name 20 | string.file-name.mercurial-status-report 21 | 22 | 23 | match 24 | ^([AM!?])\s+(.+)$ 25 | 26 | 27 | match 28 | .+ 29 | name 30 | string.mercurial-status-report 31 | 32 | 33 | scopeName 34 | text.mercurial-status-report 35 | uuid 36 | 6086c539-0f15-4f54-a43b-3db7bb6ff106 37 | 38 | 39 | -------------------------------------------------------------------------------- /Support/Mercurial Status Report.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Mercurial Status Report 7 | patterns 8 | 9 | 10 | captures 11 | 12 | 1 13 | 14 | name 15 | support.function.status.mercurial-status-report 16 | 17 | 2 18 | 19 | name 20 | string.file-name.mercurial-status-report 21 | 22 | 23 | match 24 | ^([AM!?])\s+(.+)$ 25 | 26 | 27 | match 28 | .+ 29 | name 30 | string.mercurial-status-report 31 | 32 | 33 | scopeName 34 | text.mercurial-status-report 35 | uuid 36 | 6086c539-0f15-4f54-a43b-3db7bb6ff106 37 | 38 | 39 | -------------------------------------------------------------------------------- /Support/Preferences.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // List of extensions whose commands will be listed in the command quick 3 | // panel (for example, "mq"). 4 | "packages.sublime_hg.extensions": [], 5 | 6 | // Terminal emulator to be used on Linux. 7 | "packages.sublime_hg.terminal": "lxterminal" 8 | } 9 | -------------------------------------------------------------------------------- /Support/Sublime Hg CLI.JSON-tmLanguage: -------------------------------------------------------------------------------- 1 | { "name": "Sublime Hg CLI (New)", 2 | "scopeName": "source.sublime_hg_cli", 3 | "patterns": [ 4 | ], 5 | "uuid": "d9e5ff0a-4258-4475-be50-2d7554d407f8" 6 | } 7 | -------------------------------------------------------------------------------- /Support/Sublime Hg CLI.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Sublime Hg CLI (New) 7 | patterns 8 | 9 | 10 | scopeName 11 | source.sublime_hg_cli 12 | uuid 13 | d9e5ff0a-4258-4475-be50-2d7554d407f8 14 | 15 | 16 | -------------------------------------------------------------------------------- /Support/Sublime Hg CLI.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Sublime Hg CLI (New) 7 | patterns 8 | 9 | 10 | scopeName 11 | source.sublime_hg_cli 12 | uuid 13 | d9e5ff0a-4258-4475-be50-2d7554d407f8 14 | 15 | 16 | -------------------------------------------------------------------------------- /Support/SublimeHG.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "SublimeHg: Open Menu", "command": "show_sublime_hg_menu" }, 3 | { "caption": "SublimeHg: Open CLI", "command": "show_sublime_hg_cli" }, 4 | { "caption": "SublimeHg: Kill Current Server", "command": "kill_hg_server" } 5 | ] 6 | -------------------------------------------------------------------------------- /Support/SublimeHg Command Line.JSON-tmLanguage: -------------------------------------------------------------------------------- 1 | { "name": "SublimeHg Command Line", 2 | "scopeName": "text.sublimehgcmdline", 3 | "uuid": "9880a5b1-a545-4113-bc56-1466904e5ba5" 4 | } 5 | -------------------------------------------------------------------------------- /Support/SublimeHg Command Line.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | SublimeHg Command Line 7 | scopeName 8 | text.sublimehgcmdline 9 | uuid 10 | 9880a5b1-a545-4113-bc56-1466904e5ba5 11 | 12 | 13 | -------------------------------------------------------------------------------- /Support/SublimeHg Command Line.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "word_separators": "./\\()\"'-:,.;<>~@#$%^&*|+=[]{}`~?", 3 | "gutter": false, 4 | "vintage_start_in_command_mode": false 5 | } 6 | -------------------------------------------------------------------------------- /Support/SublimeHg Command Line.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | SublimeHg Command Line 7 | scopeName 8 | text.sublimehgcmdline 9 | uuid 10 | 9880a5b1-a545-4113-bc56-1466904e5ba5 11 | 12 | 13 | -------------------------------------------------------------------------------- /bin/CleanUp.ps1: -------------------------------------------------------------------------------- 1 | $here = $MyInvocation.MyCommand.Definition 2 | $here = split-path $here -parent 3 | $root = resolve-path (join-path $here "..") 4 | 5 | push-location $root 6 | # remove-item cmdlet doesn't work well! 7 | get-childitem "." -recurse -filter "*.pyc" | remove-item 8 | remove-item "dist" -recurse -force 9 | remove-item "Doc" -recurse 10 | remove-item "MANIFEST" 11 | pop-location 12 | -------------------------------------------------------------------------------- /bin/MakeRelease.ps1: -------------------------------------------------------------------------------- 1 | param([switch]$DontUpload=$False) 2 | 3 | $here = $MyInvocation.MyCommand.Definition 4 | $here = split-path $here -parent 5 | $root = resolve-path (join-path $here "..") 6 | 7 | push-location $root 8 | # rename all .tmLanguage so they don't show up in syntax menu 9 | get-childitem ".\Support\*.tmLanguage" | ` 10 | foreach-object { copy-item $_ ($_ -replace '.tmLanguage','.hidden-tmLanguage') } 11 | if (-not (test-path (join-path $root "Doc"))) { 12 | new-item -itemtype "d" -name "Doc" > $null 13 | copy-item ".\Data\main.css" ".\Doc" 14 | } 15 | 16 | # Generate docs in html from rst. 17 | push-location ".\Doc" 18 | get-childitem "..\*.rst" | foreach-object { 19 | & "rst2html.py" ` 20 | "--template" "..\data\html_template.txt" ` 21 | "--stylesheet-path" "main.css" ` 22 | "--link-stylesheet" ` 23 | $_.fullname "$($_.basename).html" 24 | } 25 | pop-location 26 | 27 | # Ensure MANIFEST reflects all changes to file system. 28 | remove-item ".\MANIFEST" -erroraction silentlycontinue 29 | start-process "python" -argumentlist ".\setup.py","spa" -NoNewWindow -Wait 30 | 31 | (get-item ".\dist\SublimeHg.sublime-package").fullname | clip.exe 32 | pop-location 33 | 34 | if (-not $DontUpload) { 35 | start-process "https://bitbucket.org/guillermooo/sublimehg/downloads" 36 | } 37 | -------------------------------------------------------------------------------- /hg_actions.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | from sublime_hg import run_hg_cmd 5 | from sublime_hg import running_servers 6 | 7 | 8 | class SublimeHgDiffSelectedCommand(sublime_plugin.TextCommand): 9 | def is_enabled(self): 10 | return self.view.match_selector(0, "text.mercurial-log") 11 | 12 | def run(self, edit): 13 | if len(self.view.sel()) > 2: 14 | sublime.status_message("SublimeHg: Please select only two commits.") 15 | return 16 | 17 | sels = list(self.view.sel()) 18 | if not (self.view.match_selector(sels[0].begin(), "keyword.other.changeset-ref.short.mercurial-log") and 19 | self.view.match_selector(sels[1].begin(), "keyword.other.changeset-ref.short.mercurial-log")): 20 | sublime.status_message("SublimeHg: SublimeHg: Please select only two commits.") 21 | return 22 | 23 | commit_nrs = [int(self.view.substr(x)) for x in self.view.sel()] 24 | older, newer = min(commit_nrs), max(commit_nrs) 25 | 26 | w = self.view.window() 27 | w.run_command("close") 28 | # FIXME: We're assuming this is the correct view, and it might not be. 29 | v = sublime.active_window().active_view() 30 | path = v.file_name() 31 | v.run_command("hg_command_runner", {"cmd": "diff -r%d:%d" % (older, newer), 32 | "display_name": "diff", 33 | "cwd": path}) 34 | 35 | 36 | class SublimeHgUpdateToRevisionCommand(sublime_plugin.TextCommand): 37 | def is_enabled(self): 38 | return self.view.match_selector(0, "text.mercurial-log") 39 | 40 | def run(self, edit): 41 | if len(self.view.sel()) > 1: 42 | sublime.status_message("SublimeHg: Please select only one commit.") 43 | return 44 | 45 | sels = list(self.view.sel()) 46 | if not (self.view.match_selector(sels[0].begin(), "keyword.other.changeset-ref.short.mercurial-log")): 47 | sublime.status_message("SublimeHg: SublimeHg: Please select only one commit.") 48 | return 49 | 50 | w = self.view.window() 51 | w.run_command("close") 52 | # FIXME: We're assuming this is the correct view, and it might not be. 53 | v = sublime.active_window().active_view() 54 | path = v.file_name() 55 | 56 | text, exit_code = run_hg_cmd(running_servers[path], "status") 57 | if text: 58 | msg = "SublimeHg: Don't update to a different revision with uncommited changes. Aborting." 59 | print msg 60 | sublime.status_message(msg) 61 | return 62 | 63 | v.run_command("hg_command_runner", {"cmd": "update %d" % int(self.view.substr(self.view.sel()[0])), 64 | "display_name": "update", 65 | "cwd": path}) 66 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | PACKAGE_VERSION = "12.9.27" 4 | 5 | """Commands to build and manage .sublime-package archives with distutils.""" 6 | 7 | import sys 8 | import os, string 9 | from types import * 10 | from glob import glob 11 | from distutils import log, dir_util, dep_util, file_util, archive_util 12 | from distutils.core import Command 13 | from distutils.core import setup 14 | from distutils.text_file import TextFile 15 | from distutils.filelist import FileList 16 | from distutils.errors import * 17 | from distutils.spawn import spawn 18 | from distutils.dir_util import mkpath 19 | import subprocess 20 | 21 | 22 | def make_zipfile (base_name, base_dir, verbose=0, dry_run=0): 23 | """Create a zip file from all the files under 'base_dir'. The output 24 | zip file will be named 'base_dir' + ".zip". Uses either the "zipfile" 25 | Python module (if available) or the InfoZIP "zip" utility (if installed 26 | and found on the default search path). If neither tool is available, 27 | raises DistutilsExecError. Returns the name of the output zip file. 28 | """ 29 | try: 30 | import zipfile 31 | except ImportError: 32 | zipfile = None 33 | 34 | zip_filename = base_name + ".sublime-package" 35 | mkpath(os.path.dirname(zip_filename), dry_run=dry_run) 36 | 37 | # If zipfile module is not available, try spawning an external 38 | # 'zip' command. 39 | if zipfile is None: 40 | if verbose: 41 | zipoptions = "-r" 42 | else: 43 | zipoptions = "-rq" 44 | 45 | try: 46 | spawn(["zip", zipoptions, zip_filename, base_dir], 47 | dry_run=dry_run) 48 | except DistutilsExecError: 49 | # XXX really should distinguish between "couldn't find 50 | # external 'zip' command" and "zip failed". 51 | raise DistutilsExecError, \ 52 | ("unable to create zip file '%s': " 53 | "could neither import the 'zipfile' module nor " 54 | "find a standalone zip utility") % zip_filename 55 | 56 | else: 57 | log.info("creating '%s' and adding '%s' to it", 58 | zip_filename, base_dir) 59 | 60 | if not dry_run: 61 | z = zipfile.ZipFile(zip_filename, "w", 62 | compression=zipfile.ZIP_DEFLATED) 63 | 64 | for dirpath, dirnames, filenames in os.walk(base_dir): 65 | for name in filenames: 66 | path = os.path.normpath(os.path.join(dirpath, name)) 67 | arcname = path[len(base_dir):] 68 | # if dirpath == base_dir: 69 | # arcname = name 70 | # else: 71 | # arcname = path[len(base_dir):] 72 | # print arcname 73 | if os.path.isfile(path): 74 | z.write(path, arcname) 75 | log.info("adding '%s'" % path) 76 | z.close() 77 | 78 | return zip_filename 79 | 80 | 81 | def show_formats (): 82 | """Print all possible values for the 'formats' option (used by 83 | the "--help-formats" command-line option). 84 | """ 85 | from distutils.fancy_getopt import FancyGetopt 86 | from distutils.archive_util import ARCHIVE_FORMATS 87 | formats=[] 88 | for format in ARCHIVE_FORMATS.keys(): 89 | formats.append(("formats=" + format, None, 90 | ARCHIVE_FORMATS[format][2])) 91 | formats.sort() 92 | pretty_printer = FancyGetopt(formats) 93 | pretty_printer.print_help( 94 | "List of available source distribution formats:") 95 | 96 | class spa (Command): 97 | 98 | description = "create a source distribution (tarball, zip file, etc.)" 99 | 100 | user_options = [ 101 | ('template=', 't', 102 | "name of manifest template file [default: MANIFEST.in]"), 103 | ('manifest=', 'm', 104 | "name of manifest file [default: MANIFEST]"), 105 | ('use-defaults', None, 106 | "include the default file set in the manifest " 107 | "[default; disable with --no-defaults]"), 108 | ('no-defaults', None, 109 | "don't include the default file set"), 110 | ('prune', None, 111 | "specifically exclude files/directories that should not be " 112 | "distributed (build tree, RCS/CVS dirs, etc.) " 113 | "[default; disable with --no-prune]"), 114 | ('no-prune', None, 115 | "don't automatically exclude anything"), 116 | ('manifest-only', 'o', 117 | "just regenerate the manifest and then stop " 118 | "(implies --force-manifest)"), 119 | ('force-manifest', 'f', 120 | "forcibly regenerate the manifest and carry on as usual"), 121 | ('formats=', None, 122 | "formats for source distribution (comma-separated list)"), 123 | ('keep-temp', 'k', 124 | "keep the distribution tree around after creating " + 125 | "archive file(s)"), 126 | ('dist-dir=', 'd', 127 | "directory to put the source distribution archive(s) in " 128 | "[default: dist]"), 129 | ] 130 | 131 | boolean_options = ['use-defaults', 'prune', 132 | 'manifest-only', 'force-manifest', 133 | 'keep-temp'] 134 | 135 | help_options = [ 136 | ('help-formats', None, 137 | "list available distribution formats", show_formats), 138 | ] 139 | 140 | negative_opt = {'no-defaults': 'use-defaults', 141 | 'no-prune': 'prune' } 142 | 143 | default_format = { 'posix': 'gztar', 144 | 'nt': 'zip' } 145 | 146 | def initialize_options (self): 147 | # 'template' and 'manifest' are, respectively, the names of 148 | # the manifest template and manifest file. 149 | self.template = None 150 | self.manifest = None 151 | 152 | # 'use_defaults': if true, we will include the default file set 153 | # in the manifest 154 | self.use_defaults = 1 155 | self.prune = 1 156 | 157 | self.manifest_only = 0 158 | self.force_manifest = 0 159 | 160 | self.formats = None 161 | self.keep_temp = 0 162 | self.dist_dir = None 163 | 164 | self.archive_files = None 165 | 166 | 167 | def finalize_options (self): 168 | if self.manifest is None: 169 | self.manifest = "MANIFEST" 170 | if self.template is None: 171 | self.template = "MANIFEST.in" 172 | 173 | self.ensure_string_list('formats') 174 | if self.formats is None: 175 | try: 176 | self.formats = [self.default_format[os.name]] 177 | except KeyError: 178 | raise DistutilsPlatformError, \ 179 | "don't know how to create source distributions " + \ 180 | "on platform %s" % os.name 181 | 182 | bad_format = archive_util.check_archive_formats(self.formats) 183 | if bad_format: 184 | raise DistutilsOptionError, \ 185 | "unknown archive format '%s'" % bad_format 186 | 187 | if self.dist_dir is None: 188 | self.dist_dir = "dist" 189 | 190 | 191 | def run (self): 192 | 193 | # 'filelist' contains the list of files that will make up the 194 | # manifest 195 | self.filelist = FileList() 196 | 197 | # Ensure that all required meta-data is given; warn if not (but 198 | # don't die, it's not *that* serious!) 199 | self.check_metadata() 200 | 201 | # Do whatever it takes to get the list of files to process 202 | # (process the manifest template, read an existing manifest, 203 | # whatever). File list is accumulated in 'self.filelist'. 204 | self.get_file_list() 205 | 206 | # If user just wanted us to regenerate the manifest, stop now. 207 | if self.manifest_only: 208 | return 209 | 210 | # Otherwise, go ahead and create the source distribution tarball, 211 | # or zipfile, or whatever. 212 | self.make_distribution() 213 | 214 | 215 | def check_metadata (self): 216 | """Ensure that all required elements of meta-data (name, version, 217 | URL, (author and author_email) or (maintainer and 218 | maintainer_email)) are supplied by the Distribution object; warn if 219 | any are missing. 220 | """ 221 | metadata = self.distribution.metadata 222 | 223 | missing = [] 224 | for attr in ('name', 'version', 'url'): 225 | if not (hasattr(metadata, attr) and getattr(metadata, attr)): 226 | missing.append(attr) 227 | 228 | if missing: 229 | self.warn("missing required meta-data: " + 230 | string.join(missing, ", ")) 231 | 232 | if metadata.author: 233 | if not metadata.author_email: 234 | self.warn("missing meta-data: if 'author' supplied, " + 235 | "'author_email' must be supplied too") 236 | elif metadata.maintainer: 237 | if not metadata.maintainer_email: 238 | self.warn("missing meta-data: if 'maintainer' supplied, " + 239 | "'maintainer_email' must be supplied too") 240 | else: 241 | self.warn("missing meta-data: either (author and author_email) " + 242 | "or (maintainer and maintainer_email) " + 243 | "must be supplied") 244 | 245 | # check_metadata () 246 | 247 | 248 | def get_file_list (self): 249 | """Figure out the list of files to include in the source 250 | distribution, and put it in 'self.filelist'. This might involve 251 | reading the manifest template (and writing the manifest), or just 252 | reading the manifest, or just using the default file set -- it all 253 | depends on the user's options and the state of the filesystem. 254 | """ 255 | 256 | # If we have a manifest template, see if it's newer than the 257 | # manifest; if so, we'll regenerate the manifest. 258 | template_exists = os.path.isfile(self.template) 259 | if template_exists: 260 | template_newer = dep_util.newer(self.template, self.manifest) 261 | 262 | # The contents of the manifest file almost certainly depend on the 263 | # setup script as well as the manifest template -- so if the setup 264 | # script is newer than the manifest, we'll regenerate the manifest 265 | # from the template. (Well, not quite: if we already have a 266 | # manifest, but there's no template -- which will happen if the 267 | # developer elects to generate a manifest some other way -- then we 268 | # can't regenerate the manifest, so we don't.) 269 | self.debug_print("checking if %s newer than %s" % 270 | (self.distribution.script_name, self.manifest)) 271 | setup_newer = dep_util.newer(self.distribution.script_name, 272 | self.manifest) 273 | 274 | # cases: 275 | # 1) no manifest, template exists: generate manifest 276 | # (covered by 2a: no manifest == template newer) 277 | # 2) manifest & template exist: 278 | # 2a) template or setup script newer than manifest: 279 | # regenerate manifest 280 | # 2b) manifest newer than both: 281 | # do nothing (unless --force or --manifest-only) 282 | # 3) manifest exists, no template: 283 | # do nothing (unless --force or --manifest-only) 284 | # 4) no manifest, no template: generate w/ warning ("defaults only") 285 | 286 | manifest_outofdate = (template_exists and 287 | (template_newer or setup_newer)) 288 | force_regen = self.force_manifest or self.manifest_only 289 | manifest_exists = os.path.isfile(self.manifest) 290 | neither_exists = (not template_exists and not manifest_exists) 291 | 292 | # Regenerate the manifest if necessary (or if explicitly told to) 293 | if manifest_outofdate or neither_exists or force_regen: 294 | if not template_exists: 295 | self.warn(("manifest template '%s' does not exist " + 296 | "(using default file list)") % 297 | self.template) 298 | self.filelist.findall() 299 | 300 | if self.use_defaults: 301 | self.add_defaults() 302 | if template_exists: 303 | self.read_template() 304 | if self.prune: 305 | self.prune_file_list() 306 | 307 | self.filelist.sort() 308 | self.filelist.remove_duplicates() 309 | self.write_manifest() 310 | 311 | # Don't regenerate the manifest, just read it in. 312 | else: 313 | self.read_manifest() 314 | 315 | # get_file_list () 316 | 317 | 318 | def add_defaults (self): 319 | """Add all the default files to self.filelist: 320 | - README or README.txt 321 | - setup.py 322 | - test/test*.py 323 | - all pure Python modules mentioned in setup script 324 | - all C sources listed as part of extensions or C libraries 325 | in the setup script (doesn't catch C headers!) 326 | Warns if (README or README.txt) or setup.py are missing; everything 327 | else is optional. 328 | """ 329 | 330 | standards = [('README', 'README.txt'), self.distribution.script_name] 331 | for fn in standards: 332 | # XXX 333 | if fn == 'setup.py': continue # We don't want setup.py 334 | if type(fn) is TupleType: 335 | alts = fn 336 | got_it = 0 337 | for fn in alts: 338 | if os.path.exists(fn): 339 | got_it = 1 340 | self.filelist.append(fn) 341 | break 342 | 343 | if not got_it: 344 | self.warn("standard file not found: should have one of " + 345 | string.join(alts, ', ')) 346 | else: 347 | if os.path.exists(fn): 348 | self.filelist.append(fn) 349 | else: 350 | self.warn("standard file '%s' not found" % fn) 351 | 352 | optional = ['test/test*.py', 'setup.cfg'] 353 | for pattern in optional: 354 | files = filter(os.path.isfile, glob(pattern)) 355 | if files: 356 | self.filelist.extend(files) 357 | 358 | if self.distribution.has_pure_modules(): 359 | build_py = self.get_finalized_command('build_py') 360 | self.filelist.extend(build_py.get_source_files()) 361 | 362 | if self.distribution.has_ext_modules(): 363 | build_ext = self.get_finalized_command('build_ext') 364 | self.filelist.extend(build_ext.get_source_files()) 365 | 366 | if self.distribution.has_c_libraries(): 367 | build_clib = self.get_finalized_command('build_clib') 368 | self.filelist.extend(build_clib.get_source_files()) 369 | 370 | if self.distribution.has_scripts(): 371 | build_scripts = self.get_finalized_command('build_scripts') 372 | self.filelist.extend(build_scripts.get_source_files()) 373 | 374 | # add_defaults () 375 | 376 | 377 | def read_template (self): 378 | """Read and parse manifest template file named by self.template. 379 | 380 | (usually "MANIFEST.in") The parsing and processing is done by 381 | 'self.filelist', which updates itself accordingly. 382 | """ 383 | log.info("reading manifest template '%s'", self.template) 384 | template = TextFile(self.template, 385 | strip_comments=1, 386 | skip_blanks=1, 387 | join_lines=1, 388 | lstrip_ws=1, 389 | rstrip_ws=1, 390 | collapse_join=1) 391 | 392 | while 1: 393 | line = template.readline() 394 | if line is None: # end of file 395 | break 396 | 397 | try: 398 | self.filelist.process_template_line(line) 399 | except DistutilsTemplateError, msg: 400 | self.warn("%s, line %d: %s" % (template.filename, 401 | template.current_line, 402 | msg)) 403 | 404 | # read_template () 405 | 406 | 407 | def prune_file_list (self): 408 | """Prune off branches that might slip into the file list as created 409 | by 'read_template()', but really don't belong there: 410 | * the build tree (typically "build") 411 | * the release tree itself (only an issue if we ran "spa" 412 | previously with --keep-temp, or it aborted) 413 | * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories 414 | """ 415 | build = self.get_finalized_command('build') 416 | base_dir = self.distribution.get_fullname() 417 | base_dir = self.distribution.get_name() 418 | 419 | self.filelist.exclude_pattern(None, prefix=build.build_base) 420 | self.filelist.exclude_pattern(None, prefix=base_dir) 421 | 422 | # pruning out vcs directories 423 | # both separators are used under win32 424 | if sys.platform == 'win32': 425 | seps = r'/|\\' 426 | else: 427 | seps = '/' 428 | 429 | vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr', 430 | '_darcs'] 431 | vcs_ptrn = r'(^|%s)(%s)(%s).*' % (seps, '|'.join(vcs_dirs), seps) 432 | self.filelist.exclude_pattern(vcs_ptrn, is_regex=1) 433 | 434 | def write_manifest (self): 435 | """Write the file list in 'self.filelist' (presumably as filled in 436 | by 'add_defaults()' and 'read_template()') to the manifest file 437 | named by 'self.manifest'. 438 | """ 439 | self.execute(file_util.write_file, 440 | (self.manifest, self.filelist.files), 441 | "writing manifest file '%s'" % self.manifest) 442 | 443 | # write_manifest () 444 | 445 | 446 | def read_manifest (self): 447 | """Read the manifest file (named by 'self.manifest') and use it to 448 | fill in 'self.filelist', the list of files to include in the source 449 | distribution. 450 | """ 451 | log.info("reading manifest file '%s'", self.manifest) 452 | manifest = open(self.manifest) 453 | while 1: 454 | line = manifest.readline() 455 | if line == '': # end of file 456 | break 457 | if line[-1] == '\n': 458 | line = line[0:-1] 459 | self.filelist.append(line) 460 | manifest.close() 461 | 462 | # read_manifest () 463 | 464 | 465 | def make_release_tree (self, base_dir, files): 466 | """Create the directory tree that will become the source 467 | distribution archive. All directories implied by the filenames in 468 | 'files' are created under 'base_dir', and then we hard link or copy 469 | (if hard linking is unavailable) those files into place. 470 | Essentially, this duplicates the developer's source tree, but in a 471 | directory named after the distribution, containing only the files 472 | to be distributed. 473 | """ 474 | # Create all the directories under 'base_dir' necessary to 475 | # put 'files' there; the 'mkpath()' is just so we don't die 476 | # if the manifest happens to be empty. 477 | self.mkpath(base_dir) 478 | dir_util.create_tree(base_dir, files, dry_run=self.dry_run) 479 | 480 | # And walk over the list of files, either making a hard link (if 481 | # os.link exists) to each one that doesn't already exist in its 482 | # corresponding location under 'base_dir', or copying each file 483 | # that's out-of-date in 'base_dir'. (Usually, all files will be 484 | # out-of-date, because by default we blow away 'base_dir' when 485 | # we're done making the distribution archives.) 486 | 487 | if hasattr(os, 'link'): # can make hard links on this system 488 | link = 'hard' 489 | msg = "making hard links in %s..." % base_dir 490 | else: # nope, have to copy 491 | link = None 492 | msg = "copying files to %s..." % base_dir 493 | 494 | if not files: 495 | log.warn("no files to distribute -- empty manifest?") 496 | else: 497 | log.info(msg) 498 | for file in files: 499 | if not os.path.isfile(file): 500 | log.warn("'%s' not a regular file -- skipping" % file) 501 | else: 502 | dest = os.path.join(base_dir, file) 503 | self.copy_file(file, dest, link=link) 504 | 505 | self.distribution.metadata.write_pkg_info(base_dir) 506 | 507 | # make_release_tree () 508 | 509 | def make_distribution (self): 510 | """Create the source distribution(s). First, we create the release 511 | tree with 'make_release_tree()'; then, we create all required 512 | archive files (according to 'self.formats') from the release tree. 513 | Finally, we clean up by blowing away the release tree (unless 514 | 'self.keep_temp' is true). The list of archive files created is 515 | stored so it can be retrieved later by 'get_archive_files()'. 516 | """ 517 | # Don't warn about missing meta-data here -- should be (and is!) 518 | # done elsewhere. 519 | base_dir = self.distribution.get_fullname() 520 | base_dir = self.distribution.get_name() 521 | # XXX 522 | # base_dir = "TEST" 523 | base_name = os.path.join(self.dist_dir, base_dir) 524 | 525 | 526 | self.make_release_tree(base_dir, self.filelist.files) 527 | archive_files = [] # remember names of files we create 528 | # tar archive must be created last to avoid overwrite and remove 529 | if 'tar' in self.formats: 530 | self.formats.append(self.formats.pop(self.formats.index('tar'))) 531 | 532 | for fmt in self.formats: 533 | # file = self.make_archive(base_name, fmt, base_dir=base_dir) 534 | file = make_zipfile(base_name, base_dir=base_dir) 535 | archive_files.append(file) 536 | self.distribution.dist_files.append(('spa', '', file)) 537 | 538 | self.archive_files = archive_files 539 | 540 | if not self.keep_temp: 541 | dir_util.remove_tree(base_dir, dry_run=self.dry_run) 542 | 543 | def get_archive_files (self): 544 | """Return the list of archive files created when the command 545 | was run, or None if the command hasn't run yet. 546 | """ 547 | return self.archive_files 548 | 549 | # class spa 550 | 551 | 552 | class install(Command): 553 | """Does it make sense?""" 554 | 555 | user_options = [('aa', 'a', 'aa')] 556 | 557 | def initialize_options(self): 558 | pass 559 | 560 | def finalize_options(self): 561 | pass 562 | 563 | def run(self): 564 | print NotImplementedError("Command not implemented yet.") 565 | 566 | 567 | class test(Command): 568 | """Does it make sense?""" 569 | 570 | user_options = [('aa', 'a', 'aa')] 571 | 572 | def initialize_options(self): 573 | pass 574 | 575 | def finalize_options(self): 576 | pass 577 | 578 | def run(self): 579 | if os.name == 'nt': 580 | subprocess.call(["py.test.exe"]) 581 | 582 | 583 | 584 | setup(cmdclass={'spa': spa, 'install': install, 'test': test}, 585 | name='SublimeHg', 586 | version=PACKAGE_VERSION, 587 | description='An interface for Mercurial\'s command server for Sublime Text 2.', 588 | author='Guillermo López', 589 | author_email='guilan70@hotmail.com', 590 | url='sublimetext.info', 591 | ) 592 | -------------------------------------------------------------------------------- /shglib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SublimeText/SublimeHg/9983cd427fef0ff0751b2b6d923510b9a2cf696d/shglib/__init__.py -------------------------------------------------------------------------------- /shglib/client.py: -------------------------------------------------------------------------------- 1 | """simple client for the mercurial command server""" 2 | 3 | import subprocess 4 | import struct 5 | import os 6 | 7 | import parsing 8 | 9 | CH_DEBUG = 'd' 10 | CH_ERROR = 'e' 11 | CH_INPUT = 'I' 12 | CH_LINE_INPUT = 'L' 13 | CH_OUTPUT = 'o' 14 | CH_RETVAL = 'r' 15 | 16 | 17 | def start_server(hg_bin, repo_root, **kwargs): 18 | """Returns a command server ready to be used.""" 19 | startup_info = None 20 | if os.name == 'nt': 21 | startup_info = subprocess.STARTUPINFO() 22 | startup_info.dwFlags = subprocess.STARTF_USESHOWWINDOW 23 | 24 | return subprocess.Popen([hg_bin, "serve", "--cmdserver", "pipe", 25 | "--repository", repo_root, 26 | "--config", "ui.interactive=False"], 27 | stdin=subprocess.PIPE, 28 | stdout=subprocess.PIPE, 29 | # If we don't redirect stderr and the server does 30 | # not support an enabled extension, we won't be 31 | # able to read stdout. 32 | stderr=subprocess.PIPE, 33 | startupinfo=startup_info) 34 | 35 | 36 | def init_repo(root): 37 | subprocess.Popen("hg", "init", "--repository", root) 38 | 39 | 40 | class CmdServerClient(object): 41 | def __init__(self, repo_root, hg_bin='hg'): 42 | self.hg_bin = hg_bin 43 | self.server = start_server(hg_bin, repo_root) 44 | self.read_greeting() 45 | 46 | def shut_down(self): 47 | self.server.stdin.close() 48 | 49 | def read_channel(self): 50 | # read channel name (1 byte) plus data length (4 bytes, BE) 51 | fmt = '>cI' 52 | ch, length = struct.unpack(fmt, 53 | self.server.stdout.read(struct.calcsize(fmt))) 54 | assert len(ch) == 1, "Expected channel name of length 1." 55 | if ch in 'LI': 56 | raise NotImplementedError("Can't provide more data to server.") 57 | 58 | return ch, self.server.stdout.read(length) 59 | 60 | def read_greeting(self): 61 | _, ascii_txt = self.read_channel() 62 | assert ascii_txt, "Expected hello message from server." 63 | 64 | # Parse hello message. 65 | capabilities, encoding = ascii_txt.split('\n') 66 | self.encoding = encoding.split(':')[1].strip().lower() 67 | self.capabilities = capabilities.split(':')[1].strip().split() 68 | 69 | if not 'runcommand' in self.capabilities: 70 | raise EnvironmentError("Server doesn't support basic features.") 71 | 72 | def _write_block(self, data): 73 | # Encoding won't work well on Windows: 74 | # http://mercurial.selenic.com/wiki/CharacterEncodingOnWindows 75 | encoded_data = [x.encode(self.encoding) for x in data] 76 | encoded_data = '\0'.join(encoded_data) 77 | preamble = struct.pack(">I", len(encoded_data)) 78 | self.server.stdin.write(preamble + encoded_data) 79 | self.server.stdin.flush() 80 | 81 | def run_command(self, cmd): 82 | args = list(parsing.CommandLexer(cmd)) 83 | if args[0] == 'hg': 84 | print "SublimeHg:inf: Stripped superfluous 'hg' from command." 85 | args = args[1:] 86 | 87 | print "SublimeHg:inf: Sending command '%s' as %s" % (args, args) 88 | self.server.stdin.write('runcommand\n') 89 | self._write_block(args) 90 | 91 | def receive_data(self): 92 | lines = [] 93 | while True: 94 | channel, data = self.read_channel() 95 | if channel == CH_OUTPUT: 96 | lines.append(data.decode(self.encoding)) 97 | elif channel == CH_RETVAL: 98 | return (''.join(lines)[:-1], struct.unpack(">l", data)[0]) 99 | elif channel == CH_DEBUG: 100 | print "debug:", data 101 | elif channel == CH_ERROR: 102 | lines.append(data.decode(self.encoding)) 103 | print "error:", data 104 | elif channel in (CH_INPUT, CH_LINE_INPUT): 105 | print "More data requested, can't satisfy." 106 | self.shut_down() 107 | return 108 | else: 109 | self.shut_down() 110 | print "Didn't expect such channel." 111 | return 112 | -------------------------------------------------------------------------------- /shglib/commands.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | class CommandNotFoundError(Exception): 5 | pass 6 | 7 | 8 | class AmbiguousCommandError(Exception): 9 | pass 10 | 11 | 12 | cmd_data = namedtuple('cmd_data','invocations prompt enabled syntax_file help flags') 13 | 14 | # Flags 15 | RUN_IN_OWN_CONSOLE = 0x02 16 | 17 | HG_COMMANDS = {} 18 | 19 | 20 | HG_COMMANDS['default'] = { 21 | 'commit': cmd_data( 22 | invocations={'commit...': 'commit -m "%(input)s"', 23 | 'commit... (this file)': 'commit "%(file_name)s" -m "%(input)s"', 24 | }, 25 | prompt='Commit message:', 26 | enabled=True, 27 | syntax_file='', 28 | help='commit the specified files or all outstanding changes', 29 | flags=0, 30 | ), 31 | 'init': cmd_data( 32 | invocations={'init': 'init', 33 | }, 34 | prompt='', 35 | enabled=True, 36 | syntax_file='', 37 | help='create a new repository in the given directory', 38 | flags=RUN_IN_OWN_CONSOLE, 39 | ), 40 | 'add': cmd_data( 41 | invocations={'add (this file)': 'add "%(file_name)s"', 42 | 'add': 'add', 43 | }, 44 | prompt='', 45 | enabled=True, 46 | syntax_file='', 47 | help='add the specified files on the next commit', 48 | flags=0, 49 | ), 50 | 'addremove': cmd_data( 51 | invocations={}, 52 | prompt='', 53 | enabled=True, 54 | syntax_file='', 55 | help='add all new files, delete all missing files', 56 | flags=0, 57 | ), 58 | 'annotate': cmd_data( 59 | invocations={'annotate (this file)': 'annotate "%(file_name)s"', 60 | 'blame (this file)': 'annotate "%(file_name)s"', 61 | }, 62 | prompt='', 63 | enabled=True, 64 | syntax_file='Packages/SublimeHg/Support/Mercurial Annotate.hidden-tmLanguage', 65 | help='show changeset information by line for each file', 66 | flags=0, 67 | ), 68 | 'bookmark': cmd_data( 69 | invocations={'bookmark... (parent revision)': 'bookmark "%(input)s"', 70 | }, 71 | prompt='Bookmark name:', 72 | enabled=True, 73 | syntax_file='', 74 | help='track a line of development with movable markers', 75 | flags=0, 76 | ), 77 | 'bookmarks': cmd_data( 78 | invocations={}, 79 | prompt='', 80 | enabled=True, 81 | syntax_file='', 82 | help='track a line of development with movable markers', 83 | flags=0, 84 | ), 85 | 'branch': cmd_data( 86 | invocations={}, 87 | prompt='', 88 | enabled=True, 89 | syntax_file='', 90 | help='set or show the current branch name', 91 | flags=0, 92 | ), 93 | 'branches': cmd_data( 94 | invocations={}, 95 | prompt='', 96 | enabled=True, 97 | syntax_file='', 98 | help='list repository named branches', 99 | flags=0, 100 | ), 101 | 'diff': cmd_data( 102 | invocations={'diff (this file)': 'diff "%(file_name)s"', 103 | 'diff': 'diff', 104 | }, 105 | prompt='', 106 | enabled=True, 107 | syntax_file='Packages/Diff/Diff.tmLanguage', 108 | help='diff repository (or selected files)', 109 | flags=0, 110 | ), 111 | 'forget': cmd_data( 112 | invocations={'forget (this file)': 'forget "%(file_name)s"', 113 | }, 114 | prompt='', 115 | enabled=True, 116 | syntax_file='', 117 | help='forget the specified files on the next commit', 118 | flags=0, 119 | ), 120 | 'grep': cmd_data( 121 | invocations={'grep...': 'grep "%(input)s"', 122 | }, 123 | prompt='Pattern (grep):', 124 | enabled=True, 125 | syntax_file='', 126 | help='search for a pattern in specified files and revisions', 127 | flags=0, 128 | ), 129 | 'heads': cmd_data( 130 | invocations={}, 131 | prompt='', 132 | enabled=True, 133 | syntax_file='', 134 | help='show current repository heads or show branch heads', 135 | flags=0, 136 | ), 137 | 'help': cmd_data( 138 | invocations={'help...': 'help "%(input)s"', 139 | 'help': 'help', 140 | }, 141 | prompt='Help topic:', 142 | enabled=True, 143 | syntax_file='', 144 | help='show help for a given topic or a help overview', 145 | flags=0, 146 | ), 147 | 'identify': cmd_data( 148 | invocations={'identify': 'identify -nibtB'}, 149 | prompt='', 150 | enabled=True, 151 | syntax_file='', 152 | help='identify the working copy or specified revision', 153 | flags=0, 154 | ), 155 | 'incoming': cmd_data( 156 | invocations={'incoming...': 'incoming %(input)s', 157 | 'incoming': 'incoming', 158 | }, 159 | prompt='Incoming source:', 160 | enabled=True, 161 | syntax_file='', 162 | help='show new changesets found in source', 163 | flags=RUN_IN_OWN_CONSOLE, 164 | ), 165 | 'locate': cmd_data( 166 | invocations={'locate...': 'locate "%(input)s"' 167 | }, 168 | prompt='Pattern:', 169 | enabled=True, 170 | syntax_file='', 171 | help='locate files matching specific patterns', 172 | flags=0, 173 | ), 174 | 'log': cmd_data( 175 | invocations={'log (this file)': 'log "%(file_name)s"', 176 | 'log': 'log', 177 | }, 178 | prompt='', 179 | enabled=True, 180 | syntax_file='Packages/SublimeHg/Support/Mercurial Log.hidden-tmLanguage', 181 | help='show revision history of entire repository or files', 182 | flags=0, 183 | ), 184 | 'manifest': cmd_data( 185 | invocations={}, 186 | prompt='', 187 | enabled=True, 188 | syntax_file='', 189 | help='output the current or given revision of the project manifest', 190 | flags=0, 191 | ), 192 | 'merge': cmd_data( 193 | invocations={'merge...': 'merge "%(input)s"', 194 | 'merge': 'merge', 195 | }, 196 | prompt='', 197 | enabled=True, 198 | syntax_file='', 199 | help='merge working directory with another revision', 200 | flags=0, 201 | ), 202 | 'outgoing': cmd_data( 203 | invocations={'outgoing...': 'outgoing %(input)s', 204 | 'outgoing': 'outgoing', 205 | }, 206 | prompt='Outgoing target:', 207 | enabled=True, 208 | syntax_file='Packages/SublimeHg/Support/Mercurial Log.hidden-tmLanguage', 209 | help='show changesets not found in the destination', 210 | flags=RUN_IN_OWN_CONSOLE, 211 | ), 212 | 'parents': cmd_data( 213 | invocations={}, 214 | prompt='', 215 | enabled=True, 216 | syntax_file='', 217 | help='show the parents of the working directory or revision', 218 | flags=0, 219 | ), 220 | 'paths': cmd_data( 221 | invocations={}, 222 | prompt='', 223 | enabled=True, 224 | syntax_file='', 225 | help='show aliases for remote repositories', 226 | flags=0, 227 | ), 228 | 'pull': cmd_data( 229 | invocations={'pull...': 'pull %(input)s', 230 | 'pull': 'pull', 231 | }, 232 | prompt='Pull source:', 233 | enabled=True, 234 | syntax_file='', 235 | help='pull changes from the specified source', 236 | flags=RUN_IN_OWN_CONSOLE, 237 | ), 238 | "push": cmd_data( 239 | invocations={'push...': 'push %(input)s', 240 | 'push': 'push', 241 | }, 242 | prompt="Push target:", 243 | enabled=True, 244 | syntax_file='', 245 | help='push changes to the specified destination', 246 | flags=RUN_IN_OWN_CONSOLE, 247 | ), 248 | "recover": cmd_data( 249 | invocations={}, 250 | prompt='', 251 | enabled=True, 252 | syntax_file='', 253 | help='roll back an interrupted transaction', 254 | flags=0, 255 | ), 256 | "remove": cmd_data( 257 | invocations={'remove (this file)': 'remove "%(file_name)s"', 258 | }, 259 | prompt='', 260 | enabled=True, 261 | syntax_file='', 262 | help='remove the specified files on the next commit', 263 | flags=0, 264 | ), 265 | "rename": cmd_data( 266 | invocations={'rename... (this file)': 'rename "%(file_name)s" "%(input)s"', 267 | }, 268 | prompt="New name:", 269 | enabled=True, 270 | syntax_file='', 271 | help='rename files; equivalent of copy + remove', 272 | flags=0, 273 | ), 274 | "resolve": cmd_data( 275 | invocations={'resolve (this file)': 'resolve "%(file_name)s"', 276 | }, 277 | prompt='', 278 | enabled=True, 279 | syntax_file='', 280 | help='redo merges or set/view the merge status of files', 281 | flags=0, 282 | ), 283 | "revert": cmd_data( 284 | invocations={'revert (this file)': 'revert "%(file_name)s"', 285 | }, 286 | prompt='', 287 | enabled=True, 288 | syntax_file='', 289 | help='restore files to their checkout state', 290 | flags=0, 291 | ), 292 | "rollback": cmd_data( 293 | invocations={}, 294 | prompt='', 295 | enabled=True, 296 | syntax_file='', 297 | help='roll back the last transaction (dangerous)', 298 | flags=0, 299 | ), 300 | "root": cmd_data( 301 | invocations={}, 302 | prompt='', 303 | enabled=True, 304 | syntax_file='', 305 | help='print the root (top) of the current working directory', 306 | flags=0, 307 | ), 308 | "showconfig": cmd_data( 309 | invocations={}, 310 | prompt='', 311 | enabled=True, 312 | syntax_file='', 313 | help='show combined config settings from all hgrc files', 314 | flags=0, 315 | ), 316 | "status": cmd_data( 317 | invocations={'status (this file)': 'status "%(file_name)s"', 318 | 'status': 'status', 319 | }, 320 | prompt='', 321 | enabled=True, 322 | syntax_file='Packages/SublimeHg/Support/Mercurial Status Report.hidden-tmLanguage', 323 | help='show changed files in the working directory', 324 | flags=0, 325 | ), 326 | "summary": cmd_data( 327 | invocations={}, 328 | prompt='', 329 | enabled=True, 330 | syntax_file='', 331 | help='summarize working directory state', 332 | flags=0, 333 | ), 334 | "tag": cmd_data( 335 | invocations={'tag...': 'tag "%(input)s"', 336 | }, 337 | prompt="Tag name:", 338 | enabled=True, 339 | syntax_file='', 340 | help='add one or more tags for the current or given revision', 341 | flags=0, 342 | ), 343 | "tags": cmd_data( 344 | invocations={}, 345 | prompt='', 346 | enabled=True, 347 | syntax_file='', 348 | help='list repository tags', 349 | flags=0, 350 | ), 351 | "tip": cmd_data( 352 | invocations={}, 353 | prompt='', 354 | enabled=True, 355 | syntax_file='', 356 | help='show the tip revision', 357 | flags=0, 358 | ), 359 | "update": cmd_data( 360 | invocations={'update...': 'update "%(input)s"', 361 | 'update': 'update', 362 | }, 363 | prompt="Branch:", 364 | enabled=True, 365 | syntax_file='', 366 | help='update working directory (or switch revisions)', 367 | flags=0, 368 | ), 369 | "verify": cmd_data( 370 | invocations={}, 371 | prompt='', 372 | enabled=True, 373 | syntax_file='', 374 | help='verify the integrity of the repository', 375 | flags=0, 376 | ), 377 | "version": cmd_data( 378 | invocations={}, 379 | prompt='', 380 | enabled=True, 381 | syntax_file='', 382 | help='output version and copyright information', 383 | flags=0, 384 | ), 385 | "serve": cmd_data( 386 | invocations={"serve": "serve"}, 387 | prompt='', 388 | enabled=True, 389 | syntax_file='', 390 | help='start stand-alone webserver', 391 | flags=RUN_IN_OWN_CONSOLE, 392 | ), 393 | "init": cmd_data( 394 | invocations={"init (this file's directory)": "init"}, 395 | prompt='', 396 | enabled=True, 397 | syntax_file='', 398 | help='create a new repository in the given directory', 399 | flags=RUN_IN_OWN_CONSOLE, 400 | ), 401 | } 402 | 403 | # At some point we'll let the user choose whether to load extensions. 404 | HG_COMMANDS['mq'] = { 405 | "qapplied": cmd_data( 406 | invocations={'qapplied': 'qapplied -s', 407 | }, 408 | prompt='', 409 | enabled=True, 410 | syntax_file='', 411 | help='print the patches already applied', 412 | flags=0, 413 | ), 414 | "qdiff": cmd_data( 415 | invocations={}, 416 | prompt='', 417 | enabled=True, 418 | syntax_file='Packages/Diff/Diff.tmLanguage', 419 | help='diff of the current patch and subsequent modifications', 420 | flags=0, 421 | ), 422 | "qgoto": cmd_data( 423 | invocations={'qgoto...':'qgoto "%(input)s"', 424 | }, 425 | prompt="Patch name:", 426 | enabled=True, 427 | syntax_file='', 428 | help='push or pop patches until named patch is at top of stack', 429 | flags=0, 430 | ), 431 | "qheader": cmd_data( 432 | invocations={'qheader...': 'qheader "%(input)s"', 433 | 'qheader': 'qheader', 434 | }, 435 | prompt="Patch name:", 436 | enabled=True, 437 | syntax_file='', 438 | help='print the header of the topmost or specified patch', 439 | flags=0, 440 | ), 441 | "qnext": cmd_data( 442 | invocations={'qnext': 'qnext -s', 443 | }, 444 | prompt='', 445 | enabled=True, 446 | syntax_file='', 447 | help='print the name of the next pushable patch', 448 | flags=0, 449 | ), 450 | "qpop": cmd_data( 451 | invocations={}, 452 | prompt='', 453 | enabled=True, 454 | syntax_file='', 455 | help='pop the current patch off the stack', 456 | flags=0, 457 | ), 458 | "qprev": cmd_data( 459 | invocations={'qprev': 'qprev -s', 460 | }, 461 | prompt='', 462 | enabled=True, 463 | syntax_file='', 464 | help='print the name of the preceding applied patch', 465 | flags=0, 466 | ), 467 | "qpush": cmd_data( 468 | invocations={}, 469 | prompt='', 470 | enabled=True, 471 | syntax_file='', 472 | help='push the next patch onto the stack', 473 | flags=0, 474 | ), 475 | "qrefresh": cmd_data( 476 | invocations={'qrefresh... (EDIT commit message': 'qrefresh -e', 477 | 'qrefresh... (NEW commit message)': 'qrefresh -m "%(input)s"', 478 | 'qrefresh': 'qrefresh', 479 | }, 480 | prompt='Commit message:', 481 | enabled=True, 482 | syntax_file='', 483 | help='update the current patch', 484 | flags=0, 485 | ), 486 | "qfold": cmd_data( 487 | invocations={'qfold...': 'qfold "%(input)s"'}, 488 | prompt='Patch name:', 489 | enabled=True, 490 | syntax_file='', 491 | help='fold the named patches into the current patch', 492 | flags=0, 493 | ), 494 | "qseries": cmd_data( 495 | invocations={'qseries': 'qseries -s', 496 | }, 497 | prompt='', 498 | enabled=True, 499 | syntax_file='', 500 | help='print the entire series file', 501 | flags=0, 502 | ), 503 | "qfinish": cmd_data( 504 | invocations={'qfinish...': 'qfinish "%(input)s"', 505 | }, 506 | prompt='Patch name:', 507 | enabled=True, 508 | syntax_file='', 509 | help='move applied patches into repository history', 510 | flags=0, 511 | ), 512 | "qnew": cmd_data( 513 | invocations={'qnew...': 'qnew "%(input)s"', 514 | }, 515 | prompt='Patch name:', 516 | enabled=True, 517 | syntax_file='', 518 | help='create a new patch', 519 | flags=0, 520 | ), 521 | "qdelete": cmd_data( 522 | invocations={'qdelete...': 'qdelete "%(input)s"', 523 | }, 524 | prompt='Patch name:', 525 | enabled=True, 526 | syntax_file='', 527 | help='remove patches from queue', 528 | flags=0, 529 | ), 530 | "qtop": cmd_data( 531 | invocations={'qtop': 'qtop -s', 532 | }, 533 | prompt='', 534 | enabled=True, 535 | syntax_file='', 536 | help='print the name of the current patch', 537 | flags=0, 538 | ), 539 | "qunapplied": cmd_data( 540 | invocations={}, 541 | prompt='', 542 | enabled=True, 543 | syntax_file='', 544 | help='print the patches not yet applied', 545 | flags=0, 546 | ), 547 | } 548 | 549 | 550 | def format_for_display(extension): 551 | all_cmds = [] 552 | for name, cmd_data in HG_COMMANDS[extension].iteritems(): 553 | if cmd_data.invocations: 554 | for display_name, invocation in cmd_data.invocations.iteritems(): 555 | all_cmds.append([display_name, cmd_data.help]) 556 | else: 557 | all_cmds.append([name, cmd_data.help]) 558 | 559 | return sorted(all_cmds, key=lambda x: x[0]) 560 | 561 | 562 | def find_cmd(extensions, search_term): 563 | candidates = [] 564 | extensions.insert(0, 'default') 565 | for ext in extensions: 566 | cmds = HG_COMMANDS[ext] 567 | for name, cmd_data in cmds.iteritems(): 568 | if search_term in cmd_data.invocations: 569 | return cmd_data.invocations[search_term], cmd_data 570 | break 571 | elif search_term == name: 572 | return name, cmd_data 573 | break 574 | elif name.startswith(search_term): 575 | candidates.append((name, cmd_data)) 576 | 577 | if len(candidates) == 1: 578 | return candidates[0] 579 | 580 | if len(candidates) > 1: 581 | raise AmbiguousCommandError 582 | else: 583 | raise CommandNotFoundError 584 | 585 | def get_commands_by_ext(extensions): 586 | cmds = [] 587 | for ext in extensions: 588 | if not ext.lower() == 'default': 589 | cmds.extend(format_for_display(ext)) 590 | # Make sure we return at least 'default' commands. 591 | cmds = format_for_display('default') + cmds 592 | return cmds 593 | 594 | 595 | HG_COMMANDS_AND_SHORT_HELP = format_for_display("default") 596 | HG_COMMANDS_LIST = [x.replace('.', '') for x in HG_COMMANDS if ' ' not in x] 597 | HG_COMMANDS_LIST = list(sorted(set(HG_COMMANDS_LIST))) 598 | -------------------------------------------------------------------------------- /shglib/parsing.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | 3 | EOF = -1 4 | 5 | class Lexer(object): 6 | def __init__(self, in_unicode): 7 | self.input = in_unicode 8 | self.index = 0 9 | self.c = self.input[self.index] 10 | 11 | def consume(self): 12 | self.index += 1 13 | if self.index >= len(self.input): 14 | self.c = EOF 15 | else: 16 | self.c = self.input[self.index] 17 | 18 | 19 | class CommandLexer(Lexer): 20 | # todo: describe grammar more formally 21 | """ 22 | cmd : NAME (option string?)* 23 | option: -NAME | [^"']+ | NUM 24 | string : (["']) .* \1 25 | NAME : [a-zA-Z]+ 26 | NUM : 0-9+ 27 | """ 28 | 29 | _white_space = ' \t' 30 | 31 | def __init__(self, in_unicode): 32 | Lexer.__init__(self, in_unicode) 33 | 34 | def _WHITE_SPACE(self): 35 | while self.c in self._white_space: 36 | self.consume() 37 | 38 | def _NAME(self): 39 | name_buf = [] 40 | while self.c != EOF and self.c.isalpha(): 41 | name_buf.append(self.c) 42 | self.consume() 43 | return ''.join(name_buf) 44 | 45 | def _OPTION(self): 46 | opt_buf = [] 47 | while self.c != EOF and self.c == '-': 48 | opt_buf.append(self.c) 49 | self.consume() 50 | if self.c == EOF: 51 | SyntaxError("expected option name, got nothing") 52 | opt_buf.append(self._NAME()) 53 | return ''.join(opt_buf) 54 | 55 | def _VALUE(self): 56 | val_buf = [] 57 | while self.c != EOF and self.c not in self._white_space: 58 | val_buf.append(self.c) 59 | self.consume() 60 | return ''.join(val_buf) 61 | 62 | def _STRING(self): 63 | delimiter = self.c 64 | str_buf = [] 65 | self.consume() 66 | while self.c != EOF: 67 | if self.c == '\\': 68 | self.consume() 69 | if self.c == delimiter: 70 | str_buf.append(self.c) 71 | self.consume() 72 | else: 73 | str_buf.append('\\') 74 | str_buf.append(self.c) 75 | self.consume() 76 | else: 77 | str_buf.append(self.c) 78 | self.consume() 79 | if self.c == delimiter: 80 | self.consume() 81 | break 82 | return ''.join(str_buf) 83 | 84 | def __iter__(self): 85 | if self.c in self._white_space: 86 | self._WHITE_SPACE() 87 | if self.c.isalpha(): 88 | yield self._NAME() 89 | else: 90 | SyntaxError("cannot find command name") 91 | 92 | while self.c != EOF: 93 | if self.c in self._white_space: 94 | self._WHITE_SPACE() 95 | elif self.c == '-': 96 | yield self._OPTION() 97 | elif self.c in '\'"': 98 | yield self._STRING() 99 | else: 100 | # For example, eats locate *pat* and log -l5 101 | yield self._VALUE() 102 | raise StopIteration 103 | 104 | 105 | if __name__ == '__main__': 106 | # todo: add tests 107 | values = ( 108 | "foo", 109 | "foo -b", 110 | "foo --bar", 111 | "foo -b -c", 112 | "foo -b 100", 113 | "foo -b200", 114 | "foo -b 'this is a string'", 115 | "foo -b 'this is a string' --cmd \"whatever and ever\"", 116 | "foo -b 'this is \\'a string'", 117 | "foo -b 'mañana será otro día'", 118 | "commit -m 'there are \" some things here'", 119 | "commit -m 'there are \u some things here'", 120 | "locate ut*.py", 121 | ) 122 | for v in values: 123 | lx = CommandLexer(v) 124 | print v 125 | x = list([x for x in lx]) 126 | print x 127 | print ' '.join(x) 128 | -------------------------------------------------------------------------------- /shglib/utils.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import os 3 | import contextlib 4 | import client 5 | 6 | 7 | class NoRepositoryFoundError(Exception): 8 | def __str__(self): 9 | return "No repository found." 10 | 11 | 12 | @contextlib.contextmanager 13 | def pushd(to): 14 | old_cwd = os.getcwdu() 15 | os.chdir(to) 16 | yield 17 | os.chdir(old_cwd) 18 | 19 | 20 | def get_hg_exe_name(): 21 | # fixme(guillermooo): There must be a better way of getting the 22 | # active view. 23 | view = sublime.active_window().active_view() 24 | if view: 25 | # Retrieving the view's settings guarantees that settings 26 | # defined in projects, etc. work as expected. 27 | return view.settings().get('packages.sublime_hg.hg_exe') or 'hg' 28 | else: 29 | return 'hg' 30 | 31 | 32 | def get_preferred_terminal(): 33 | settings = sublime.load_settings('Global.sublime-settings') 34 | return settings.get('packages.sublime_hg.terminal') or '' 35 | 36 | 37 | def find_hg_root(path): 38 | abs_path = os.path.join(path, '.hg') 39 | if os.path.exists(abs_path) and os.path.isdir(abs_path): 40 | return path 41 | elif os.path.dirname(path) == path: 42 | return None 43 | else: 44 | return find_hg_root(os.path.dirname(path)) 45 | 46 | 47 | def is_flag_set(flags, which_one): 48 | return flags & which_one == which_one 49 | 50 | 51 | class HgServers(object): 52 | def __getitem__(self, key): 53 | return self._select_server(key) 54 | 55 | def _select_server(self, current_path=None): 56 | """Finds an existing server for the given path. If none is 57 | found, it creates one for the path. 58 | """ 59 | v = sublime.active_window().active_view() 60 | repo_root = find_hg_root(current_path or v.file_name()) 61 | if not repo_root: 62 | raise NoRepositoryFound() 63 | if not repo_root in self.__dict__: 64 | server = self._start_server(repo_root) 65 | self.__dict__[repo_root] = server 66 | return self.__dict__[repo_root] 67 | 68 | def _start_server(self, repo_root): 69 | """Starts a new Mercurial command server. 70 | """ 71 | # By default, hglib uses 'hg'. User might need to change that on 72 | # Windows, for example. 73 | hg_bin = get_hg_exe_name() 74 | server = client.CmdServerClient(hg_bin=hg_bin, repo_root=repo_root) 75 | return server 76 | 77 | def shut_down(self, repo_root): 78 | self[repo_root].shut_down() 79 | del self.__dict__[repo_root] 80 | -------------------------------------------------------------------------------- /sublime_hg.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | import threading 5 | import functools 6 | import subprocess 7 | import os 8 | import re 9 | 10 | from shglib import commands 11 | from shglib import utils 12 | from shglib.commands import AmbiguousCommandError 13 | from shglib.commands import CommandNotFoundError 14 | from shglib.commands import find_cmd 15 | from shglib.commands import get_commands_by_ext 16 | from shglib.commands import HG_COMMANDS_LIST 17 | from shglib.commands import RUN_IN_OWN_CONSOLE 18 | from shglib.parsing import CommandLexer 19 | 20 | 21 | VERSION = '12.8.12' 22 | 23 | 24 | CMD_LINE_SYNTAX = 'Packages/SublimeHg/Support/SublimeHg Command Line.hidden-tmLanguage' 25 | 26 | ############################################################################### 27 | # Globals 28 | #------------------------------------------------------------------------------ 29 | # Holds the existing server so it doesn't have to be reloaded. 30 | running_servers = utils.HgServers() 31 | # Helps find the file where the cmdline should be restored. 32 | recent_file_name = None 33 | #============================================================================== 34 | 35 | 36 | def run_hg_cmd(server, cmd_string): 37 | """Runs a Mercurial command through the given command server. 38 | """ 39 | server.run_command(cmd_string) 40 | text, exit_code = server.receive_data() 41 | return text, exit_code 42 | 43 | 44 | class KillHgServerCommand(sublime_plugin.TextCommand): 45 | """Shut down the server for the current file if it's running. 46 | 47 | The Mercurial command server does not detect state changes in the 48 | repo originating outside the command server itself (such as from a 49 | separate command line). This command makes it easy to restart the 50 | server so that the newest changes are picked up. 51 | """ 52 | 53 | def run(self, edit): 54 | try: 55 | repo_root = utils.find_hg_root(self.view.file_name()) 56 | # XXX: Will swallow the same error for the utils. call. 57 | except AttributeError: 58 | msg = "SublimeHg: No server found for this file." 59 | sublime.status_message(msg) 60 | return 61 | 62 | running_servers.shut_down(repo_root) 63 | sublime.status_message("SublimeHg: Killed server for '%s'" % 64 | repo_root) 65 | 66 | 67 | def run_in_console(hg_bin, cmd, encoding=None): 68 | if sublime.platform() == 'windows': 69 | cmd_str = ("%s %s && pause" % (hg_bin, cmd)).encode(encoding) 70 | subprocess.Popen(["cmd.exe", "/c", cmd_str,]) 71 | elif sublime.platform() == 'linux': 72 | # Apparently it isn't possible to retrieve the preferred 73 | # terminal in a general way for different distros: 74 | # http://unix.stackexchange.com/questions/32547/how-to-launch-an-application-with-default-terminal-emulator-on-ubuntu 75 | term = utils.get_preferred_terminal() 76 | if term: 77 | cmd_str = "bash -c '%s %s;read'" % (hg_bin, cmd) 78 | subprocess.Popen([term, '-e', cmd_str]) 79 | else: 80 | raise EnvironmentError("No terminal found." 81 | "You might want to add packages.sublime_hg.terminal " 82 | "to your settings.") 83 | elif sublime.platform() == 'osx': 84 | cmd_str = "%s %s" % (hg_bin, cmd) 85 | osa = "tell application \"Terminal\"\ndo script \"cd '%s' && %s\"\nactivate\nend tell" % (os.getcwd(), cmd_str) 86 | 87 | subprocess.Popen(["osascript", "-e", osa]) 88 | else: 89 | raise NotImplementedError("Cannot run consoles on your OS: %s. Not implemented." % sublime.platform()) 90 | 91 | 92 | def escape(s, c, esc='\\\\'): 93 | # FIXME: won't escape \\" and such correctly. 94 | pat = "(? ") 178 | p.end_edit(p_edit) 179 | p.show(self.view.size()) 180 | 181 | 182 | class HgCommandRunnerCommand(sublime_plugin.TextCommand): 183 | def run(self, edit, cmd=None, display_name=None, cwd=None, append=False): 184 | self.display_name = display_name 185 | self.cwd = cwd 186 | self.append = append 187 | try: 188 | self.on_done(cmd) 189 | except CommandNotFoundError: 190 | # This will happen when we cannot find an unambiguous command or 191 | # any command at all. 192 | sublime.status_message("SublimeHg: Command not found.") 193 | except AmbiguousCommandError: 194 | sublime.status_message("SublimeHg: Ambiguous command.") 195 | 196 | def on_done(self, s): 197 | # FIXME: won't work with short aliases like st, etc. 198 | self.display_name = self.display_name or s.split(' ')[0] 199 | 200 | try: 201 | hgs = running_servers[self.cwd] 202 | except utils.NoRepositoryFoundError, e: 203 | msg = "SublimeHg: %s" % e 204 | print msg 205 | sublime.status_message(msg) 206 | return 207 | except EnvironmentError, e: 208 | msg = "SublimeHg: %s (Is the Mercurial binary on your PATH?)" % e 209 | print msg 210 | sublime.status_message(msg) 211 | return 212 | except Exception, e: 213 | msg = ("SublimeHg: Cannot start server." 214 | "(Your Mercurial version might be too old.)") 215 | print msg 216 | sublime.status_message(msg) 217 | return 218 | 219 | if getattr(self, 'worker', None) and self.worker.is_alive(): 220 | sublime.status_message("SublimeHg: Processing another request. " 221 | "Try again later.") 222 | return 223 | 224 | self.worker = CommandRunnerWorker(hgs, 225 | s, 226 | self.view, 227 | self.cwd or self.view.file_name(), 228 | self.display_name, 229 | append=self.append,) 230 | self.worker.start() 231 | 232 | 233 | class ShowSublimeHgMenuCommand(sublime_plugin.TextCommand): 234 | CMDS_FOR_DISPLAY = None 235 | 236 | def is_enabled(self): 237 | return self.view.file_name() 238 | 239 | def run(self, edit): 240 | if not self.CMDS_FOR_DISPLAY: 241 | extensions = self.view.settings().get('packages.sublime_hg.extensions', []) 242 | self.CMDS_FOR_DISPLAY = get_commands_by_ext(extensions) 243 | 244 | self.view.window().show_quick_panel(self.CMDS_FOR_DISPLAY, 245 | self.on_done) 246 | 247 | def on_done(self, s): 248 | if s == -1: return 249 | 250 | hg_cmd = self.CMDS_FOR_DISPLAY[s][0] 251 | extensions = self.view.settings().get('packages.sublime_hg.extensions', []) 252 | format_str , cmd_data = find_cmd(extensions, hg_cmd) 253 | 254 | fn = self.view.file_name() 255 | env = {'file_name': fn} 256 | 257 | # Handle commands differently whether they require input or not. 258 | # Commands requiring input have a "format_str". 259 | if format_str: 260 | # Collect single-line inputs from an input panel. 261 | if '%(input)s' in format_str: 262 | env['caption'] = cmd_data.prompt 263 | env['fmtstr'] = format_str 264 | self.view.run_command('hg_command_asking', env) 265 | return 266 | 267 | # Command requires additional info, but it's provided automatically. 268 | self.view.run_command('hg_command_runner', { 269 | 'cmd': format_str % env, 270 | 'display_name': hg_cmd}) 271 | else: 272 | # It's a simple command that doesn't require any input, so just 273 | # go ahead and run it. 274 | self.view.run_command('hg_command_runner', { 275 | 'cmd': hg_cmd, 276 | 'display_name': hg_cmd}) 277 | 278 | 279 | 280 | class HgCommandAskingCommand(sublime_plugin.TextCommand): 281 | """Asks the user for missing output and runs a Mercurial command. 282 | """ 283 | def run(self, edit, caption='', fmtstr='', **kwargs): 284 | self.fmtstr = fmtstr 285 | self.content = kwargs 286 | if caption: 287 | self.view.window().show_input_panel(caption, 288 | '', 289 | self.on_done, 290 | None, 291 | None) 292 | return 293 | 294 | self.view.run_command("hg_command_runner", {"cmd": self.fmtstr % 295 | self.content}) 296 | 297 | def on_done(self, s): 298 | self.content['input'] = escape(s, '"') 299 | self.view.run_command("hg_command_runner", {"cmd": self.fmtstr % 300 | self.content}) 301 | 302 | 303 | # XXX not ideal; missing commands 304 | COMPLETIONS = HG_COMMANDS_LIST 305 | 306 | 307 | #_____________________________________________________________________________ 308 | class HgCompletionsProvider(sublime_plugin.EventListener): 309 | CACHED_COMPLETIONS = [] 310 | CACHED_COMPLETION_PREFIXES = [] 311 | COMPLETIONS = [] 312 | 313 | def load_completions(self, view): 314 | extensions = view.settings().get('packages.sublime_hg.extensions', []) 315 | extensions.insert(0, 'default') 316 | self.COMPLETIONS = [] 317 | for ext in extensions: 318 | self.COMPLETIONS.extend(commands.HG_COMMANDS[ext].keys()) 319 | self.COMPLETIONS = set(sorted(self.COMPLETIONS)) 320 | 321 | def on_query_completions(self, view, prefix, locations): 322 | # Only provide completions to the SublimeHg command line. 323 | if view.score_selector(0, 'source.sublime_hg_cli') == 0: 324 | return [] 325 | 326 | if not self.COMPLETIONS: 327 | self.load_completions(view) 328 | 329 | # Only complete top level commands. 330 | current_line = view.substr(view.line(view.size()))[2:] 331 | if current_line != prefix: 332 | return [] 333 | 334 | if prefix and prefix in self.CACHED_COMPLETION_PREFIXES: 335 | return self.CACHED_COMPLETIONS 336 | 337 | new_completions = [x for x in self.COMPLETIONS if x.startswith(prefix)] 338 | self.CACHED_COMPLETION_PREFIXES = [prefix] + new_completions 339 | self.CACHED_COMPLETIONS = zip([prefix] + new_completions, 340 | new_completions + [prefix]) 341 | return self.CACHED_COMPLETIONS 342 | -------------------------------------------------------------------------------- /sublime_hg_cli.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | 3 | import os 4 | 5 | 6 | CLI_BUFFER_NAME = '==| SublimeHg Console |==' 7 | CLI_PROMPT = '> ' 8 | CLI_SYNTAX_FILE = 'Packages/SublimeHg/Support/Sublime Hg CLI.hidden-tmLanguage' 9 | 10 | current_path = None # Dirname of latest active view (other than the console). 11 | existing_console = None # View object (SublimeHg console). 12 | 13 | 14 | class ShowSublimeHgCli(sublime_plugin.TextCommand): 15 | """ 16 | Opens and initialises the SublimeHg cli. 17 | """ 18 | def is_enabled(self): 19 | # Do not show if the file does not have a known path. SublimeHg would 20 | # not be able to find the corresponding root repo for it. 21 | return self.view.file_name() 22 | 23 | def init_console(self): 24 | v = self.view.window().new_file() 25 | v.set_name(CLI_BUFFER_NAME) 26 | v.set_scratch(True) 27 | v.set_syntax_file(CLI_SYNTAX_FILE) 28 | edit = v.begin_edit() 29 | v.insert(edit, 0, CLI_PROMPT) 30 | v.end_edit(edit) 31 | 32 | return v 33 | 34 | def run(self, edit): 35 | global current_path, existing_console 36 | 37 | # Ensure there's a path to operate on. At the time this executes, we 38 | # assume the active view is the one the user wants to operate on 39 | # (because it's the one he's lookign at). 40 | current_path = os.path.dirname(self.view.file_name()) 41 | 42 | # Reuse existing console. existing_console will not work across sessions, 43 | # even if you leave a console open. 44 | if existing_console: 45 | self.view.window().focus_view(existing_console) 46 | if self.view.window().active_view().name() == CLI_BUFFER_NAME: 47 | return 48 | 49 | # Close "dead" consoles. There might be others open from a previous 50 | # session. 51 | for v in self.view.window().views(): 52 | if v.name() == CLI_BUFFER_NAME: 53 | self.view.window().focus_view(v) 54 | self.view.window().run_command('close') 55 | 56 | existing_console = self.init_console() 57 | 58 | 59 | class SublimeHgSendLine(sublime_plugin.TextCommand): 60 | """ 61 | Forwards the current line's text to the Mercurial command server. 62 | """ 63 | def append_chars(self, s): 64 | edit = self.view.begin_edit() 65 | self.view.insert(edit, self.view.size(), s) 66 | self.view.end_edit(edit) 67 | 68 | def new_line(self): 69 | self.append_chars("\n") 70 | 71 | def write_prompt(self): 72 | self.new_line() 73 | self.append_chars(CLI_PROMPT) 74 | 75 | def append_output(self, output): 76 | self.new_line() 77 | self.append_chars(output) 78 | 79 | def run(self, edit, cmd=None): 80 | global current_path 81 | if current_path is None: 82 | self.view.window().run_command('close') 83 | 84 | # Get line to be run and clean it. 85 | cmd = self.view.substr(self.view.line(self.view.sel()[0].a)) 86 | if cmd.startswith(CLI_PROMPT[0]): 87 | cmd = cmd[1:].strip() 88 | 89 | params = dict(cmd=cmd, cwd=current_path, append=True, 90 | # send only first token (for command search) 91 | display_name=cmd.split()[0]) 92 | self.view.run_command('hg_command_runner', params) 93 | 94 | 95 | class SublimeHgCliEventListener(sublime_plugin.EventListener): 96 | """ 97 | Ensures global state remains consistent. 98 | 99 | The SublimeHg console will always be the active view here, but Mercurial 100 | should operate on another view (the latest one that had a path). 101 | """ 102 | def record_path(self, view): 103 | # We're only interested in files that do have a path, so we can 104 | # record it. 105 | if view.file_name(): 106 | global current_path 107 | current_path = os.path.dirname(view.file_name()) 108 | 109 | def on_activated(self, view): 110 | self.record_path(view) 111 | 112 | def on_load(self, view): 113 | self.record_path(view) 114 | 115 | def on_close(self, view): 116 | global existing_console 117 | if view.name() == CLI_BUFFER_NAME: 118 | existing_console = None 119 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SublimeText/SublimeHg/9983cd427fef0ff0751b2b6d923510b9a2cf696d/tests/__init__.py -------------------------------------------------------------------------------- /tests/sublime.py: -------------------------------------------------------------------------------- 1 | def packages_path(): 2 | return 'XXX' 3 | -------------------------------------------------------------------------------- /tests/sublime_plugin.py: -------------------------------------------------------------------------------- 1 | class Plugin(object): 2 | pass 3 | 4 | 5 | class TextCommand(Plugin): 6 | pass 7 | 8 | 9 | class WindowCommand(Plugin): 10 | pass 11 | 12 | 13 | class EventListener(Plugin): 14 | pass 15 | -------------------------------------------------------------------------------- /tests/test_sublime_hg.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | import mock 5 | 6 | sys.path.insert(0, os.path.dirname(__file__)) 7 | 8 | 9 | from sublime_hg import find_hg_root 10 | 11 | 12 | def test_ThatHgRootIsFoundCorrectly(): 13 | paths = ( 14 | r'C:\No\Luck\Here', 15 | r'C:\Sometimes\You\Find\What\You\Are\Looking\For', 16 | r'C:\Come\Get\Some\If\You\Dare', 17 | ) 18 | old_exists = os.path.exists 19 | os.path.exists = lambda path: path.endswith('Some\.hg') 20 | results = [find_hg_root(x) for x in paths] 21 | os.path.exists = old_exists 22 | assert results == [None, None, 'C:\\Come\\Get\\Some'] 23 | 24 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | from test_runner import tests_state 5 | 6 | from shglib import utils 7 | 8 | 9 | class TestHelpers(unittest.TestCase): 10 | def sartUp(self): 11 | # ss = test_runner.test_view.settings.get('packages.sublime_hg.hg_exe') 12 | pass 13 | 14 | def testPushd(self): 15 | cwd = os.getcwdu() 16 | target = os.environ["TEMP"] 17 | with utils.pushd(target): 18 | self.assertEqual(os.getcwdu(), target) 19 | self.assertEqual(os.getcwdu(), cwd) 20 | --------------------------------------------------------------------------------