├── .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 |
--------------------------------------------------------------------------------