├── .gitignore ├── LICENSE ├── Main.sublime-menu ├── README.md ├── easy_diff.py ├── easy_diff.sublime-settings ├── easy_diff_basic.py ├── easy_diff_dynamic_menu.py ├── easy_diff_global.py ├── easy_diff_version_control.py └── lib ├── __init__.py ├── git.py ├── hg.py ├── multiconf.py └── svn.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | __pycache__ 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | nosetests.xml 27 | 28 | # Translations 29 | *.mo 30 | 31 | # Mr Developer 32 | .mr.developer.cfg 33 | .project 34 | .pydevproject 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Isaac Muse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "EasyDiff", 16 | "children": 17 | [ 18 | { 19 | "command": "open_file", "args": 20 | { 21 | "file": "${packages}/EasyDiff/easy_diff.sublime-settings" 22 | }, 23 | "caption": "Settings – Default" 24 | }, 25 | { 26 | "command": "open_file", "args": 27 | { 28 | "file": "${packages}/User/easy_diff.sublime-settings" 29 | }, 30 | "caption": "Settings – User" 31 | }, 32 | { "caption": "-" } 33 | ] 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyDiff 2 | 3 | Diff plugin for Sublime Text 4 | 5 | 6 | 7 | - Allows comparing views, selections, multi-selections, and clipboard combinations. 8 | - Can compare working copy against the base or previous revsion of a file in SVN, Git, and Mercurial (requires some setup and configuration). 9 | - Dynamic context menus for selecting left side and right side compare. Dynamic menus show what file is on *left* side (think Beyond Compare context menus on windows). 10 | - View diffs in a view buffer or output panel 11 | - You can selectively hide version control menus or disable the command completely via the settings file. 12 | - Can open diffs in external diff programs if desired (requires some setup and configuration) 13 | - Show only interanl diff options, only external options, or show both depending on your needs and preferences. 14 | 15 | # Usage 16 | Easy diff is easy to use. Simply select the `Set Left Side` option in the context menus to set what is to be compared on the left side when in a view. Then select the what to compare to via `Compare with` menu option (select the right side view first if comparing to a view). 17 | 18 | For version control, just select the applicable option when in a view that is versioned controlled. 19 | 20 | # General Settings 21 | Easy diff, by default, shows diffs in a spearate view, you can display the diff in an output panel if desired using the following setting: 22 | 23 | ```javascript 24 | // If enabled, this allows for multiple selections. 25 | // Each selection is separated with a new line. 26 | "multi_select": false 27 | ``` 28 | 29 | Easy allows for diffing with the clipboard selections, and multi-selections. These all can be turned off or on as desired: 30 | 31 | ```javascript 32 | // Use a buffer instead of the output panel 33 | "use_buffer": true, 34 | 35 | // Enable clipboard commands 36 | "use_clipboard": true, 37 | 38 | // Enable selection commands 39 | "use_selections": false, 40 | ``` 41 | 42 | # Dynamic Menu 43 | Easy diff creates a dynamic menu in `User/EasyDiff/Context.sublime-menu`, `User/EasyDiff/Tab Context.sublime-menu`, and `User/EasyDiff/Side Bar.sublime-menu`. The content of this context menu changes depending on what is enabled or disabled, hidden or shown, and depending on whether a view, selection, or clipboard has been selected for left side compare. If a view that was previously set has been closed, that view will no longer be reported in the context menu. You can look here to see how the commands are constructed if you would like to bind the options to shortcuts or to the command palette. 44 | 45 | ## Excluding Dynamic Menu from Certain UI Elements 46 | Easy diff shows access commands in the view, tab, and sidebar context menu. If it is desired to exclude access in one of these UI elements, you can remove the element from the following setting: 47 | 48 | ```javascript 49 | // Menus to show (view|tab|sidebar) 50 | "menu_types": ["view", "tab", "sidebar"], 51 | ``` 52 | 53 | # Version Control Setup 54 | EasyDiff currently supports SVN, Git, and Mercurial. These options should only appear in the context menus if EasyDiff can find the binaries `svn`, `git`, and `hg` respectively. 55 | 56 | If one of these options shows up, and you wish to disable them, you can go to your settings file and disable them completely with the following settings: 57 | 58 | ```javascript 59 | // Turn off svn completely 60 | "svn_disabled": false, 61 | 62 | // Turn off git completely 63 | "git_disabled": false, 64 | 65 | // Turn off (Mercurial) hg completely 66 | "hg_disabled": false, 67 | ``` 68 | 69 | If you would simply like to hide the options (because you have bound their operations to a shortcut or to the command palette), you can hide the options without disabling them: 70 | 71 | ```javascript 72 | // Turn off svn menu access 73 | "svn_hide_menu": false, 74 | 75 | // Turn off git menu access 76 | "git_hide_menu": false, 77 | 78 | // Turn off (Mercurial) hg menu access 79 | "hg_hide_menu": false, 80 | ``` 81 | 82 | If your binaries are not in your system's path, you will need to configure the following settings with the path to your binaries: 83 | 84 | ```javascript 85 | // SVN path 86 | "svn": "", 87 | 88 | // Git Path 89 | "git": "", 90 | 91 | // (Mercurial) Hg path 92 | "hg": "", 93 | ``` 94 | 95 | Currently, by default, EasyDiff will check if the current view is versioned controlled by one of your enabled version control options when displaying the context menu. This allows the for non-pertinent options to be grayed out. With some version control systems, this can occasionally cause a lag when displaying those options. You can turn off this functionality if it becomes a problem with the following settings: 96 | 97 | ```javascript 98 | // Do not perform a version check on files 99 | // when evaluating whether version control 100 | // commands are enabled. May avoid slowing 101 | // down context menu draw in large version 102 | // controlled projects. 103 | "skip_version_check_on_is_enabled": false, 104 | ``` 105 | 106 | # Diffing with External Diff Tools 107 | Easy diff, by default, has options to diff everything internally in a single view. But, it can be configured to diff in external tools instead. 108 | 109 | Configure the external binary setting to point to diff tool binary, and then enable external diff options: 110 | 111 | ```javascript 112 | // Show external options (options to send files to external diff tool) 113 | "show_external": false, 114 | 115 | // External diff tool path (absolute) 116 | "external_diff": "", 117 | ``` 118 | 119 | The external option assumes the diff the tool takes arguments as such: `tool file1 file2`. If this is not the case, you will probably have to wrap the command in a shell script that takes the options as described, and call it directly instead. For instance, using DeltaWalker on Mac, I copied their provided workflow configuration to a shell script, and call it directly: 120 | 121 | ```bash 122 | #!/bin/sh 123 | DW_PATH=/Applications/DeltaWalker.app/Contents/MacOS 124 | 125 | java -Ddw.path=$DW_PATH -jar $DW_PATH/dw.jar "$1" "$2" "$3" "$4" "$5" "$6" 126 | 127 | ``` 128 | 129 | # Hiding External or Internal Diffing from Context Menu 130 | EasyDiff allows you to hide either internal diffing options, external diffing options, or both. The later options is useful if you do not use the context menu, but have bound the commands to keyboard shortcuts or to the command palette. 131 | 132 | The settings are: 133 | ```javascript 134 | // Show internal diff options (Easy Diff in a view or buffer) 135 | "show_internal": true, 136 | 137 | // Show external options (options to send files to external diff tool) 138 | "show_external": false, 139 | ``` 140 | 141 | # License 142 | 143 | EasyDiff is released under the MIT license. 144 | 145 | Copyright (c) 2013 Isaac Muse 146 | 147 | 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: 148 | 149 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 150 | 151 | 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. 152 | -------------------------------------------------------------------------------- /easy_diff.py: -------------------------------------------------------------------------------- 1 | """ 2 | Easy Diff 3 | 4 | Copyright (c) 2013 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | import time 9 | import difflib 10 | from os.path import basename, join, splitext 11 | from os import stat as osstat 12 | import tempfile 13 | from EasyDiff.easy_diff_global import load_settings, get_encoding, notify 14 | import subprocess 15 | 16 | LEFT = 1 17 | RIGHT = 2 18 | 19 | 20 | class EasyDiffView(object): 21 | def __init__(self, name, content, encoding): 22 | self.filename = name 23 | self.content = content 24 | self.time = time.ctime() 25 | self.encode = encoding 26 | 27 | def encoding(self): 28 | return self.encode 29 | 30 | def get_time(self): 31 | return self.time 32 | 33 | def file_name(self): 34 | return self.filename 35 | 36 | def substr(self, region): 37 | return self.content[region.begin():region.end() + 1] 38 | 39 | def size(self): 40 | return len(self.content) 41 | 42 | 43 | class EasyDiffInput(object): 44 | def __init__(self, v1, v2, external=False): 45 | self.untitled = False 46 | self.temp_folder = None 47 | self.process_view(v1, LEFT, external) 48 | self.process_view(v2, RIGHT, external) 49 | 50 | def process_view(self, view, side, external): 51 | self.side = side 52 | name = view.file_name() 53 | if name is None: 54 | self.set_view_buffer(view, self.untitled, external) 55 | elif isinstance(view, EasyDiffView): 56 | self.set_special(view, external) 57 | else: 58 | self.set_view(view) 59 | 60 | self.set_buffer(view, external) 61 | 62 | def set_buffer(self, view, external): 63 | setattr( 64 | self, 65 | "b%d" % self.side, 66 | view.substr(sublime.Region(0, view.size())).splitlines() if not external else [] 67 | ) 68 | 69 | def set_view(self, view): 70 | setattr(self, "f%d" % self.side, view.file_name()) 71 | setattr(self, "t%d" % self.side, time.ctime(osstat(view.file_name()).st_mtime)) 72 | 73 | def set_special(self, view, external): 74 | setattr(self, "f%d" % self.side, view.file_name()) 75 | if external: 76 | setattr(self, "f%d" % self.side, self.create_temp(view, view.file_name().replace("*", ""))) 77 | setattr(self, "t%d" % self.side, view.get_time()) 78 | 79 | def set_view_buffer(self, view, untitled, external): 80 | setattr( 81 | self, 82 | "f%d" % self.side, 83 | self.create_temp(view, "Untitled2" if self.untitled else "Untitled") if external else "Untitled2" if self.untitled else "Untitled" 84 | ) 85 | setattr(self, "t%d" % self.side, time.ctime()) 86 | self.untitled = True 87 | 88 | def create_temp(self, v, name): 89 | file_name = None 90 | if self.temp_folder is None: 91 | self.temp_folder = tempfile.mkdtemp(prefix="easydiff") 92 | file_name = self.create_file(v, name) 93 | else: 94 | file_name = self.create_file(v, name) 95 | return file_name 96 | 97 | def create_file(self, v, name): 98 | root, ext = splitext(name) 99 | with open(join(self.temp_folder, "%s-%s%s" % (root, "LEFT" if self.side == LEFT else "RIGHT", ext)), "wb") as f: 100 | encoding = get_encoding(v) 101 | try: 102 | bfr = v.substr(sublime.Region(0, v.size())).encode(encoding) 103 | except: 104 | bfr = v.substr(sublime.Region(0, v.size())).encode("utf-8") 105 | f.write(bfr) 106 | return f.name 107 | 108 | 109 | class EasyDiff(object): 110 | @classmethod 111 | def extcompare(cls, inputs, ext_diff): 112 | subprocess.Popen( 113 | [ 114 | ext_diff, 115 | inputs.f1, 116 | inputs.f2 117 | ] 118 | ) 119 | 120 | @classmethod 121 | def compare(cls, inputs): 122 | diff = difflib.unified_diff( 123 | inputs.b1, inputs.b2, 124 | inputs.f1, inputs.f2, 125 | inputs.t1, inputs.t2, 126 | lineterm='' 127 | ) 128 | result = u"\n".join(line for line in diff) 129 | 130 | if result == "": 131 | notify("No Difference") 132 | return 133 | 134 | use_buffer = bool(load_settings().get("use_buffer", False)) 135 | 136 | win = sublime.active_window() 137 | if use_buffer: 138 | v = win.new_file() 139 | v.set_name("EasyDiff: %s -> %s (%s)" % (basename(inputs.f1), basename(inputs.f2), time.ctime())) 140 | v.set_scratch(True) 141 | v.assign_syntax('Packages/Diff/Diff.tmLanguage') 142 | v.run_command('append', {'characters': result}) 143 | else: 144 | v = win.create_output_panel('easy_diff') 145 | v.assign_syntax('Packages/Diff/Diff.tmLanguage') 146 | v.run_command('append', {'characters': result}) 147 | win.run_command("show_panel", {"panel": "output.easy_diff"}) 148 | -------------------------------------------------------------------------------- /easy_diff.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Show internal diff options (Easy Diff in a view or buffer) 3 | "show_internal": true, 4 | 5 | // Show external options (options to send files to external diff tool) 6 | "show_external": false, 7 | 8 | // External diff tool path (absolute) 9 | "external_diff": "", 10 | 11 | // SVN path 12 | "svn": "", 13 | 14 | // Git Path 15 | "git": "", 16 | 17 | // (Mercurial) Hg path 18 | "hg": "", 19 | 20 | // Turn off svn completely 21 | "svn_disabled": false, 22 | 23 | // Turn off git completely 24 | "git_disabled": false, 25 | 26 | // Turn off (Mercurial) hg completely 27 | "hg_disabled": false, 28 | 29 | // Turn off svn menu access 30 | "svn_hide_menu": false, 31 | 32 | // Turn off git menu access 33 | "git_hide_menu": false, 34 | 35 | // Turn off (Mercurial) hg menu access 36 | "hg_hide_menu": false, 37 | 38 | // Menus to show (view|tab|sidebar) 39 | "menu_types": ["view", "tab", "sidebar"], 40 | 41 | // Do not perform a version check on files 42 | // when evaluating whether version control 43 | // commands are enabled. May avoid slowing 44 | // down context menu draw in large version 45 | // controlled projects. 46 | "skip_version_check_on_is_enabled": false, 47 | 48 | // Use a buffer instead of the output panel 49 | "use_buffer": true, 50 | 51 | // Enable clipboard commands 52 | "use_clipboard": true, 53 | 54 | // Enable selection commands 55 | "use_selections": true, 56 | 57 | // If enabled, this allows for multiple selections. 58 | // Each selection is separated with a new line. 59 | "multi_select": false, 60 | 61 | "use_sub_notify": true 62 | } 63 | -------------------------------------------------------------------------------- /easy_diff_basic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Easy Diff Basic Commands 3 | 4 | Copyright (c) 2013 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | import sublime_plugin 9 | from os.path import basename 10 | from EasyDiff.easy_diff_global import load_settings, log, get_external_diff, get_target 11 | from EasyDiff.easy_diff_dynamic_menu import update_menu 12 | from EasyDiff.easy_diff import EasyDiffView, EasyDiffInput, EasyDiff 13 | 14 | LEFT = None 15 | 16 | 17 | ############################### 18 | # Helper Functions 19 | ############################### 20 | def diff(right, external=False): 21 | lw = None 22 | rw = None 23 | lv = None 24 | rv = None 25 | 26 | for w in sublime.windows(): 27 | if w.id() == LEFT["win_id"]: 28 | lw = w 29 | if w.id() == right["win_id"]: 30 | rw = w 31 | if lw is not None and rw is not None: 32 | break 33 | 34 | if lw is not None: 35 | for v in lw.views(): 36 | if v.id() == LEFT["view_id"]: 37 | lv = v 38 | break 39 | else: 40 | if LEFT["clip"]: 41 | lv = LEFT["clip"] 42 | 43 | if rw is not None: 44 | for v in rw.views(): 45 | if v.id() == right["view_id"]: 46 | rv = v 47 | break 48 | else: 49 | if right["clip"]: 50 | rv = right["clip"] 51 | 52 | if lv is not None and rv is not None: 53 | ext_diff = get_external_diff() 54 | if external: 55 | EasyDiff.extcompare(EasyDiffInput(lv, rv, external=True), ext_diff) 56 | else: 57 | EasyDiff.compare(EasyDiffInput(lv, rv)) 58 | else: 59 | log("Can't compare") 60 | 61 | 62 | ############################### 63 | # Helper Classes 64 | ############################### 65 | class _EasyDiffSelection(object): 66 | def get_selections(self): 67 | bfr = "" 68 | length = len(self.view.sel()) 69 | for s in self.view.sel(): 70 | if s.size() == 0: 71 | continue 72 | bfr += self.view.substr(s) 73 | if length > 1: 74 | bfr += "\n" 75 | length -= 1 76 | return bfr 77 | 78 | def get_encoding(self): 79 | return self.view.encoding() 80 | 81 | def has_selections(self): 82 | selections = False 83 | if bool(load_settings().get("multi_select", False)): 84 | for s in self.view.sel(): 85 | if s.size() > 0: 86 | selections = True 87 | break 88 | else: 89 | selections = len(self.view.sel()) == 1 and self.view.sel()[0].size() > 0 90 | return selections 91 | 92 | 93 | class _EasyDiffCompareBothTextCommand(sublime_plugin.TextCommand): 94 | def run(self, edit, external=False, group=-1, index=-1): 95 | if index != -1: 96 | # Ensure we have the correct view 97 | self.view = sublime.active_window().views_in_group(group)[index] 98 | diff(self.get_right(), external=external) 99 | 100 | def view_has_selections(self, group=-1, index=-1): 101 | has_selections = False 102 | if index != -1: 103 | view = sublime.active_window().views_in_group(group)[index] 104 | if bool(load_settings().get("multi_select", False)): 105 | for sel in view.sel(): 106 | if sel.size() > 0: 107 | has_selections = True 108 | break 109 | else: 110 | has_selections = len(view.sel()) == 1 and view.sel()[0].size() > 0 111 | else: 112 | has_selections = self.has_selections() 113 | return has_selections 114 | 115 | def get_right(self): 116 | return None 117 | 118 | def check_enabled(self, group=-1, index=-1): 119 | return True 120 | 121 | def is_enabled(self, external=False, group=-1, index=-1): 122 | return LEFT is not None and self.check_enabled(group, index) 123 | 124 | 125 | class _EasyDiffCompareBothWindowCommand(sublime_plugin.WindowCommand): 126 | no_view = False 127 | 128 | def run(self, external=False, paths=[], group=-1, index=-1): 129 | self.external = external 130 | self.set_view(paths, group, index) 131 | if not self.no_view and self.view is None: 132 | return 133 | if not self.no_view: 134 | sublime.set_timeout(self.is_loaded, 100) 135 | else: 136 | self.diff() 137 | 138 | def is_loaded(self): 139 | if self.view.is_loading(): 140 | sublime.set_timeout(self.is_loaded, 100) 141 | else: 142 | self.diff() 143 | 144 | def diff(self): 145 | diff(self.get_right(), external=self.external) 146 | 147 | def set_view(self, paths, group=-1, index=-1, open_file=True): 148 | if len(paths): 149 | file_path = get_target(paths) 150 | if file_path is None: 151 | return 152 | if open_file: 153 | self.view = self.window.open_file(file_path) 154 | elif index != -1: 155 | self.view = self.window.views_in_group(group)[index] 156 | else: 157 | self.view = self.window.active_view() 158 | 159 | def get_right(self): 160 | return None 161 | 162 | def check_enabled(self, paths=[], group=-1, index=-1): 163 | return True 164 | 165 | def is_enabled(self, external=False, paths=[], group=-1, index=-1): 166 | return LEFT is not None and self.check_enabled(paths) 167 | 168 | 169 | ############################### 170 | # Set View 171 | ############################### 172 | class EasyDiffSetLeftCommand(sublime_plugin.WindowCommand): 173 | def run(self, paths=[], group=-1, index=-1): 174 | global LEFT 175 | self.set_view(paths, group, index) 176 | if self.view is None: 177 | return 178 | LEFT = {"win_id": self.view.window().id(), "view_id": self.view.id(), "clip": None} 179 | name = self.view.file_name() 180 | if name is None: 181 | name = "Untitled" 182 | update_menu(basename(name)) 183 | 184 | def set_view(self, paths, group=-1, index=-1, open_file=True): 185 | if len(paths): 186 | file_path = get_target(paths) 187 | if file_path is None: 188 | return 189 | if open_file: 190 | self.view = self.window.open_file(file_path) 191 | elif index != -1: 192 | self.view = self.window.views_in_group(group)[index] 193 | else: 194 | self.view = self.window.active_view() 195 | 196 | def is_enabled(self, paths=[], group=-1, index=-1): 197 | return get_target(paths, group, index) is not None if len(paths) or index != -1 else True 198 | 199 | 200 | class EasyDiffCompareBothViewCommand(_EasyDiffCompareBothWindowCommand): 201 | def get_right(self): 202 | return {"win_id": self.view.window().id(), "view_id": self.view.id(), "clip": None} 203 | 204 | def check_enabled(self, paths=[], group=-1, index=-1): 205 | return True 206 | 207 | def is_enabled(self, external=False, paths=[], group=-1, index=-1): 208 | return LEFT is not None and (get_target(paths, group, index) is not None if len(paths) or index != -1 else True) and self.check_enabled() 209 | 210 | 211 | ############################### 212 | # Set Clipboard 213 | ############################### 214 | class EasyDiffSetLeftClipboardCommand(sublime_plugin.WindowCommand): 215 | def run(self, paths=[], group=-1, index=-1): 216 | global LEFT 217 | LEFT = {"win_id": None, "view_id": None, "clip": EasyDiffView("**clipboard**", sublime.get_clipboard(), "UTF-8")} 218 | update_menu("**clipboard**") 219 | 220 | def is_enabled(self, paths=[], group=-1, index=-1): 221 | valid_path = get_target(paths, group, index) is not None if len(paths) or index != -1 else True 222 | return bool(load_settings().get("use_clipboard", True)) and valid_path 223 | 224 | def is_visible(self, paths=[], group=-1, index=-1): 225 | return bool(load_settings().get("use_clipboard", True)) 226 | 227 | 228 | class EasyDiffCompareBothClipboardCommand(_EasyDiffCompareBothWindowCommand): 229 | no_view = True 230 | 231 | def get_right(self): 232 | return {"win_id": None, "view_id": None, "clip": EasyDiffView("**clipboard**", sublime.get_clipboard(), "UTF-8")} 233 | 234 | def check_enabled(self, paths=[], group=-1, index=-1): 235 | valid_path = get_target(paths, group, index) is not None if len(paths) or index != -1 else True 236 | return bool(load_settings().get("use_clipboard", True)) and valid_path 237 | 238 | def is_visible(self, external=False, paths=[], group=-1, index=-1): 239 | return bool(load_settings().get("use_clipboard", True)) 240 | 241 | 242 | ############################### 243 | # Set Selection 244 | ############################### 245 | class EasyDiffSetLeftSelectionCommand(sublime_plugin.TextCommand, _EasyDiffSelection): 246 | def run(self, edit, group=-1, index=-1): 247 | global LEFT 248 | if index != -1: 249 | # Ensure we have the correct view 250 | self.view = sublime.active_window().views_in_group(group)[index] 251 | LEFT = {"win_id": None, "view_id": None, "clip": EasyDiffView("**selection**", self.get_selections(), self.get_encoding())} 252 | update_menu("**selection**") 253 | 254 | def view_has_selections(self, group=-1, index=-1): 255 | has_selections = False 256 | if index != -1: 257 | view = sublime.active_window().views_in_group(group)[index] 258 | if bool(load_settings().get("multi_select", False)): 259 | for sel in view.sel(): 260 | if sel.size() > 0: 261 | has_selections = True 262 | break 263 | else: 264 | has_selections = len(view.sel()) == 1 and view.sel()[0].size() > 0 265 | else: 266 | has_selections = self.has_selections() 267 | return has_selections 268 | 269 | def is_enabled(self, group=-1, index=-1): 270 | return bool(load_settings().get("use_selections", True)) and self.view_has_selections(group, index) 271 | 272 | def is_visible(self, group=-1, index=-1): 273 | return bool(load_settings().get("use_selections", True)) 274 | 275 | 276 | class EasyDiffCompareBothSelectionCommand(_EasyDiffCompareBothTextCommand, _EasyDiffSelection): 277 | def get_right(self): 278 | return {"win_id": None, "view_id": None, "clip": EasyDiffView("**selection**", self.get_selections(), self.get_encoding())} 279 | 280 | def check_enabled(self, group=-1, index=-1): 281 | return bool(load_settings().get("use_selections", True)) and self.view_has_selections(group, index) 282 | 283 | def is_visible(self, external=False, group=-1, index=-1): 284 | return bool(load_settings().get("use_selections", True)) 285 | 286 | 287 | ############################### 288 | # View Close Listener 289 | ############################### 290 | class EasyDiffListener(sublime_plugin.EventListener): 291 | def on_close(self, view): 292 | global LEFT 293 | vid = view.id() 294 | if LEFT is not None and vid == LEFT["view_id"]: 295 | LEFT = None 296 | update_menu() 297 | 298 | 299 | ############################### 300 | # Loaders 301 | ############################### 302 | def basic_reload(): 303 | global LEFT 304 | LEFT = None 305 | update_menu() 306 | settings = load_settings() 307 | settings.clear_on_change('reload_basic') 308 | settings.add_on_change('reload_basic', basic_reload) 309 | 310 | 311 | def plugin_loaded(): 312 | basic_reload() 313 | -------------------------------------------------------------------------------- /easy_diff_dynamic_menu.py: -------------------------------------------------------------------------------- 1 | """ 2 | Easy Diff Dynamic Menu 3 | 4 | Copyright (c) 2013 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | from os.path import join, exists 9 | from os import makedirs, remove 10 | from EasyDiff.easy_diff_global import load_settings, debug, get_external_diff 11 | from EasyDiff.lib.multiconf import get as multiget 12 | 13 | MENU_FOLDER = "EasyDiff" 14 | CONTEXT_MENU = "Context.sublime-menu" 15 | SIDEBAR_MENU = "Side Bar.sublime-menu" 16 | TAB_MENU = "Tab Context.sublime-menu" 17 | 18 | 19 | ############################### 20 | # General Menus 21 | ############################### 22 | DIFF_MENU = '''[ 23 | %(internal)s 24 | %(vc_internal)s 25 | %(external)s 26 | %(vc_external)s 27 | { "caption": "-"} 28 | ] 29 | ''' 30 | 31 | VC_INTERNAL_MENU = '''{ 32 | "caption": "EasyDiff Version Control", 33 | "children": 34 | [ 35 | %(vc)s 36 | ] 37 | }, 38 | ''' 39 | 40 | VC_EXTERNAL_MENU = '''{ 41 | "caption": "Diff Version Control", 42 | "children": 43 | [ 44 | %(vc)s 45 | ] 46 | }, 47 | ''' 48 | 49 | ############################### 50 | # View Menus 51 | ############################### 52 | INTERNAL_MENU = '''{ "caption": "-" }, 53 | { 54 | "caption": "EasyDiff Set Left Side", 55 | "children": 56 | [ 57 | { 58 | "caption": "View", 59 | "command": "easy_diff_set_left" 60 | }, 61 | { 62 | "caption": "Clipboard", 63 | "command": "easy_diff_set_left_clipboard" 64 | }, 65 | { 66 | "caption": "Selection", 67 | "command": "easy_diff_set_left_selection" 68 | } 69 | ] 70 | }, 71 | { 72 | "caption": "EasyDiff Compare with \\"%(file_name)s\\"", 73 | "children": 74 | [ 75 | { 76 | "caption": "View", 77 | "command": "easy_diff_compare_both_view" 78 | }, 79 | { 80 | "caption": "Clipboard", 81 | "command": "easy_diff_compare_both_clipboard" 82 | }, 83 | { 84 | "caption": "Selection", 85 | "command": "easy_diff_compare_both_selection" 86 | } 87 | ] 88 | }, 89 | ''' 90 | 91 | EXTERNAL_MENU = '''{ "caption": "-" }, 92 | { 93 | "caption": "Diff Set Left Side", 94 | "children": 95 | [ 96 | { 97 | "caption": "View", 98 | "command": "easy_diff_set_left" 99 | }, 100 | { 101 | "caption": "Clipboard", 102 | "command": "easy_diff_set_left_clipboard" 103 | }, 104 | { 105 | "caption": "Selection", 106 | "command": "easy_diff_set_left_selection" 107 | } 108 | ] 109 | }, 110 | { 111 | "caption": "Diff Compare with \\"%(file_name)s\\"", 112 | "children": 113 | [ 114 | { 115 | "caption": "View", 116 | "command": "easy_diff_compare_both_view", 117 | "args": {"external": true} 118 | }, 119 | { 120 | "caption": "Clipboard", 121 | "command": "easy_diff_compare_both_clipboard", 122 | "args": {"external": true} 123 | }, 124 | { 125 | "caption": "Selection", 126 | "command": "easy_diff_compare_both_selection", 127 | "args": {"external": true} 128 | } 129 | ] 130 | }, 131 | ''' 132 | 133 | SVN_INTERNAL_MENU = ''' 134 | { 135 | "caption": "SVN Diff", 136 | "command": "easy_diff_svn" 137 | }, 138 | { 139 | "caption": "SVN Diff with Previous Revision", 140 | "command": "easy_diff_svn", 141 | "args": {"last": true} 142 | }, 143 | { 144 | "caption": "SVN Revert", 145 | "command": "easy_diff_svn", 146 | "args": {"revert": true} 147 | }, 148 | { "caption": "-"}''' 149 | 150 | GIT_INTERNAL_MENU = ''' 151 | { 152 | "caption": "Git Diff", 153 | "command": "easy_diff_git" 154 | }, 155 | { 156 | "caption": "Git Diff with Previous Revision", 157 | "command": "easy_diff_git", 158 | "args": {"last": true} 159 | }, 160 | { 161 | "caption": "Git Revert", 162 | "command": "easy_diff_git", 163 | "args": {"revert": true} 164 | }, 165 | { "caption": "-"}''' 166 | 167 | HG_INTERNAL_MENU = ''' 168 | { 169 | "caption": "Mercurial Diff", 170 | "command": "easy_diff_hg" 171 | }, 172 | { 173 | "caption": "Mercurial Diff with Previous Revision", 174 | "command": "easy_diff_hg", 175 | "args": {"last": true} 176 | }, 177 | { 178 | "caption": "Mercurial Revert", 179 | "command": "easy_diff_hg", 180 | "args": {"revert": true} 181 | }, 182 | { "caption": "-"}''' 183 | 184 | SVN_EXTERNAL_MENU = ''' 185 | { 186 | "caption": "SVN Diff", 187 | "command": "easy_diff_svn", 188 | "args": {"external": true} 189 | }, 190 | { 191 | "caption": "SVN Diff with Previous Revision", 192 | "command": "easy_diff_svn", 193 | "args": {"external": true, "last": true} 194 | }, 195 | { 196 | "caption": "SVN Revert", 197 | "command": "easy_diff_svn", 198 | "args": {"revert": true} 199 | }, 200 | { "caption": "-"}''' 201 | 202 | GIT_EXTERNAL_MENU = ''' 203 | { 204 | "caption": "Git Diff", 205 | "command": "easy_diff_git", 206 | "args": {"external": true} 207 | }, 208 | { 209 | "caption": "Git Diff with Previous Revision", 210 | "command": "easy_diff_git", 211 | "args": {"external": true, "last": true} 212 | }, 213 | { 214 | "caption": "Git Revert", 215 | "command": "easy_diff_git", 216 | "args": {"revert": true} 217 | }, 218 | { "caption": "-"}''' 219 | 220 | HG_EXTERNAL_MENU = ''' 221 | { 222 | "caption": "Mercurial Diff", 223 | "command": "easy_diff_hg", 224 | "args": {"external": true} 225 | }, 226 | { 227 | "caption": "Mercurial Diff with Previous Revision", 228 | "command": "easy_diff_hg", 229 | "args": {"external": true, "last": true} 230 | }, 231 | { 232 | "caption": "Mercurial Revert", 233 | "command": "easy_diff_hg", 234 | "args": {"revert": true} 235 | }, 236 | { "caption": "-"}''' 237 | 238 | 239 | ############################### 240 | # Sidebar Menus 241 | ############################### 242 | INTERNAL_SIDEBAR_MENU = '''{ "caption": "-" }, 243 | { 244 | "caption": "EasyDiff Set Left Side", 245 | "children": 246 | [ 247 | { 248 | "caption": "View", 249 | "command": "easy_diff_set_left", 250 | "args": {"paths": []} 251 | }, 252 | { 253 | "caption": "Clipboard", 254 | "command": "easy_diff_set_left_clipboard", 255 | "args": {"paths": []} 256 | } 257 | ] 258 | }, 259 | { 260 | "caption": "EasyDiff Compare with \\"%(file_name)s\\"", 261 | "children": 262 | [ 263 | { 264 | "caption": "View", 265 | "command": "easy_diff_compare_both_view", 266 | "args": {"paths": []} 267 | }, 268 | { 269 | "caption": "Clipboard", 270 | "command": "easy_diff_compare_both_clipboard", 271 | "args": {"paths": []} 272 | } 273 | ] 274 | }, 275 | ''' 276 | 277 | EXTERNAL_SIDEBAR_MENU = '''{ "caption": "-" }, 278 | { 279 | "caption": "Diff Set Left Side", 280 | "children": 281 | [ 282 | { 283 | "caption": "View", 284 | "command": "easy_diff_set_left", 285 | "args": {"paths": []} 286 | }, 287 | { 288 | "caption": "Clipboard", 289 | "command": "easy_diff_set_left_clipboard", 290 | "args": {"paths": []} 291 | } 292 | ] 293 | }, 294 | { 295 | "caption": "Diff Compare with \\"%(file_name)s\\"", 296 | "children": 297 | [ 298 | { 299 | "caption": "View", 300 | "command": "easy_diff_compare_both_view", 301 | "args": {"external": true, "paths": []} 302 | }, 303 | { 304 | "caption": "Clipboard", 305 | "command": "easy_diff_compare_both_clipboard", 306 | "args": {"external": true, "paths": []} 307 | } 308 | ] 309 | }, 310 | ''' 311 | 312 | SVN_SIDEBAR_INTERNAL_MENU = ''' 313 | { 314 | "caption": "SVN Diff", 315 | "command": "easy_diff_svn", 316 | "args": {"paths": []} 317 | }, 318 | { 319 | "caption": "SVN Diff with Previous Revision", 320 | "command": "easy_diff_svn", 321 | "args": {"last": true, "paths": []} 322 | }, 323 | { 324 | "caption": "SVN Revert", 325 | "command": "easy_diff_svn", 326 | "args": {"revert": true, "paths": []} 327 | }, 328 | { "caption": "-"}''' 329 | 330 | GIT_SIDEBAR_INTERNAL_MENU = ''' 331 | { 332 | "caption": "Git Diff", 333 | "command": "easy_diff_git", 334 | "args": {"paths": []} 335 | }, 336 | { 337 | "caption": "Git Diff with Previous Revision", 338 | "command": "easy_diff_git", 339 | "args": {"last": true, "paths": []} 340 | }, 341 | { 342 | "caption": "Git Revert", 343 | "command": "easy_diff_git", 344 | "args": {"revert": true, "paths": []} 345 | }, 346 | { "caption": "-"}''' 347 | 348 | HG_SIDEBAR_INTERNAL_MENU = ''' 349 | { 350 | "caption": "Mercurial Diff", 351 | "command": "easy_diff_hg", 352 | "args": {"paths": []} 353 | }, 354 | { 355 | "caption": "Mercurial Diff with Previous Revision", 356 | "command": "easy_diff_hg", 357 | "args": {"last": true, "paths": []} 358 | }, 359 | { 360 | "caption": "Mercurial Revert", 361 | "command": "easy_diff_hg", 362 | "args": {"revert": true, "paths": []} 363 | }, 364 | { "caption": "-"}''' 365 | 366 | SVN_SIDEBAR_EXTERNAL_MENU = ''' 367 | { 368 | "caption": "SVN Diff", 369 | "command": "easy_diff_svn", 370 | "args": {"external": true, "paths": []} 371 | }, 372 | { 373 | "caption": "SVN Diff with Previous Revision", 374 | "command": "easy_diff_svn", 375 | "args": {"external": true, "last": true, "paths": []} 376 | }, 377 | { 378 | "caption": "SVN Revert", 379 | "command": "easy_diff_svn", 380 | "args": {"revert": true, "paths": []} 381 | }, 382 | { "caption": "-"}''' 383 | 384 | GIT_SIDEBAR_EXTERNAL_MENU = ''' 385 | { 386 | "caption": "Git Diff", 387 | "command": "easy_diff_git", 388 | "args": {"external": true, "paths": []} 389 | }, 390 | { 391 | "caption": "Git Diff with Previous Revision", 392 | "command": "easy_diff_git", 393 | "args": {"external": true, "last": true, "paths": []} 394 | }, 395 | { 396 | "caption": "Git Revert", 397 | "command": "easy_diff_git", 398 | "args": {"revert": true, "paths": []} 399 | }, 400 | { "caption": "-"}''' 401 | 402 | HG_SIDEBAR_EXTERNAL_MENU = ''' 403 | { 404 | "caption": "Mercurial Diff", 405 | "command": "easy_diff_hg", 406 | "args": {"external": true, "paths": []} 407 | }, 408 | { 409 | "caption": "Mercurial Diff with Previous Revision", 410 | "command": "easy_diff_hg", 411 | "args": {"external": true, "last": true, "paths": []} 412 | }, 413 | { 414 | "caption": "Mercurial Revert", 415 | "command": "easy_diff_hg", 416 | "args": {"revert": true, "paths": []} 417 | }, 418 | { "caption": "-"}''' 419 | 420 | 421 | ############################### 422 | # Tab Menus 423 | ############################### 424 | INTERNAL_TAB_MENU = '''{ "caption": "-" }, 425 | { 426 | "caption": "EasyDiff Set Left Side", 427 | "children": 428 | [ 429 | { 430 | "caption": "View", 431 | "command": "easy_diff_set_left", 432 | "args": {"group": -1, "index": -1} 433 | }, 434 | { 435 | "caption": "Clipboard", 436 | "command": "easy_diff_set_left_clipboard", 437 | "args": {"group": -1, "index": -1} 438 | }, 439 | { 440 | "caption": "Selection", 441 | "command": "easy_diff_set_left_selection", 442 | "args": {"group": -1, "index": -1} 443 | } 444 | ] 445 | }, 446 | { 447 | "caption": "EasyDiff Compare with \\"%(file_name)s\\"", 448 | "children": 449 | [ 450 | { 451 | "caption": "View", 452 | "command": "easy_diff_compare_both_view", 453 | "args": {"group": -1, "index": -1} 454 | }, 455 | { 456 | "caption": "Clipboard", 457 | "command": "easy_diff_compare_both_clipboard", 458 | "args": {"group": -1, "index": -1} 459 | }, 460 | { 461 | "caption": "Selection", 462 | "command": "easy_diff_compare_both_selection", 463 | "args": {"group": -1, "index": -1} 464 | } 465 | ] 466 | }, 467 | ''' 468 | 469 | EXTERNAL_TAB_MENU = '''{ "caption": "-" }, 470 | { 471 | "caption": "Diff Set Left Side", 472 | "children": 473 | [ 474 | { 475 | "caption": "View", 476 | "command": "easy_diff_set_left", 477 | "args": {"group": -1, "index": -1} 478 | }, 479 | { 480 | "caption": "Clipboard", 481 | "command": "easy_diff_set_left_clipboard", 482 | "args": {"group": -1, "index": -1} 483 | }, 484 | { 485 | "caption": "Selection", 486 | "command": "easy_diff_set_left_selection", 487 | "args": {"group": -1, "index": -1} 488 | } 489 | ] 490 | }, 491 | { 492 | "caption": "Diff Compare with \\"%(file_name)s\\"", 493 | "children": 494 | [ 495 | { 496 | "caption": "View", 497 | "command": "easy_diff_compare_both_view", 498 | "args": {"external": true, "group": -1, "index": -1} 499 | }, 500 | { 501 | "caption": "Clipboard", 502 | "command": "easy_diff_compare_both_clipboard", 503 | "args": {"external": true, "group": -1, "index": -1} 504 | }, 505 | { 506 | "caption": "Selection", 507 | "command": "easy_diff_compare_both_selection", 508 | "args": {"external": true, "group": -1, "index": -1} 509 | } 510 | ] 511 | }, 512 | ''' 513 | 514 | SVN_TAB_INTERNAL_MENU = ''' 515 | { 516 | "caption": "SVN Diff", 517 | "command": "easy_diff_svn", 518 | "args": {"group": -1, "index": -1} 519 | }, 520 | { 521 | "caption": "SVN Diff with Previous Revision", 522 | "command": "easy_diff_svn", 523 | "args": {"last": true, "group": -1, "index": -1} 524 | }, 525 | { 526 | "caption": "SVN Revert", 527 | "command": "easy_diff_svn", 528 | "args": {"revert": true, "group": -1, "index": -1} 529 | }, 530 | { "caption": "-"}''' 531 | 532 | GIT_TAB_INTERNAL_MENU = ''' 533 | { 534 | "caption": "Git Diff", 535 | "command": "easy_diff_git", 536 | "args": {"group": -1, "index": -1} 537 | }, 538 | { 539 | "caption": "Git Diff with Previous Revision", 540 | "command": "easy_diff_git", 541 | "args": {"last": true, "group": -1, "index": -1} 542 | }, 543 | { 544 | "caption": "Git Revert", 545 | "command": "easy_diff_git", 546 | "args": {"revert": true, "group": -1, "index": -1} 547 | }, 548 | { "caption": "-"}''' 549 | 550 | HG_TAB_INTERNAL_MENU = ''' 551 | { 552 | "caption": "Mercurial Diff", 553 | "command": "easy_diff_hg", 554 | "args": {"group": -1, "index": -1} 555 | }, 556 | { 557 | "caption": "Mercurial Diff with Previous Revision", 558 | "command": "easy_diff_hg", 559 | "args": {"last": true, "group": -1, "index": -1} 560 | }, 561 | { 562 | "caption": "Mercurial Revert", 563 | "command": "easy_diff_hg", 564 | "args": {"revert": true, "group": -1, "index": -1} 565 | }, 566 | { "caption": "-"}''' 567 | 568 | SVN_TAB_EXTERNAL_MENU = ''' 569 | { 570 | "caption": "SVN Diff", 571 | "command": "easy_diff_svn", 572 | "args": {"external": true, "group": -1, "index": -1} 573 | }, 574 | { 575 | "caption": "SVN Diff with Previous Revision", 576 | "command": "easy_diff_svn", 577 | "args": {"external": true, "last": true, "group": -1, "index": -1} 578 | }, 579 | { 580 | "caption": "SVN Revert", 581 | "command": "easy_diff_svn", 582 | "args": {"revert": true, "group": -1, "index": -1} 583 | }, 584 | { "caption": "-"}''' 585 | 586 | GIT_TAB_EXTERNAL_MENU = ''' 587 | { 588 | "caption": "Git Diff", 589 | "command": "easy_diff_git", 590 | "args": {"external": true, "group": -1, "index": -1} 591 | }, 592 | { 593 | "caption": "Git Diff with Previous Revision", 594 | "command": "easy_diff_git", 595 | "args": {"external": true, "last": true, "group": -1, "index": -1} 596 | }, 597 | { 598 | "caption": "Git Revert", 599 | "command": "easy_diff_git", 600 | "args": {"revert": true, "group": -1, "index": -1} 601 | }, 602 | { "caption": "-"}''' 603 | 604 | HG_TAB_EXTERNAL_MENU = ''' 605 | { 606 | "caption": "Mercurial Diff", 607 | "command": "easy_diff_hg", 608 | "args": {"external": true, "group": -1, "index": -1} 609 | }, 610 | { 611 | "caption": "Mercurial Diff with Previous Revision", 612 | "command": "easy_diff_hg", 613 | "args": {"external": true, "last": true, "group": -1, "index": -1} 614 | }, 615 | { 616 | "caption": "Mercurial Revert", 617 | "command": "easy_diff_hg", 618 | "args": {"revert": true, "group": -1, "index": -1} 619 | }, 620 | { "caption": "-"}''' 621 | 622 | 623 | ############################### 624 | # Menu Updater 625 | ############################### 626 | class MenuUpdater(object): 627 | def __init__(self, name): 628 | self.name = name 629 | self.menu_path = join(sublime.packages_path(), "User", MENU_FOLDER) 630 | if not exists(self.menu_path): 631 | makedirs(self.menu_path) 632 | settings = load_settings() 633 | self.menu_types = multiget(settings, "menu_types", []) 634 | self.svn_disabled = multiget(settings, "svn_disabled", False) or multiget(settings, "svn_hide_menu", False) 635 | self.git_disabled = multiget(settings, "git_disabled", False) or multiget(settings, "git_hide_menu", False) 636 | self.hg_disabled = multiget(settings, "hg_disabled", False) or multiget(settings, "hg_hide_menu", False) 637 | self.show_ext = multiget(settings, "show_external", False) and get_external_diff() is not None 638 | self.show_int = multiget(settings, "show_internal", True) 639 | 640 | def update_menu(self, menu_name, menus): 641 | if exists(self.menu_path): 642 | menu = join(self.menu_path, menu_name) 643 | vc_internal = [] 644 | vc_internal_menu = None 645 | if self.show_int: 646 | if not self.svn_disabled: 647 | vc_internal.append(menus["svn"]["internal"]) 648 | if not self.git_disabled: 649 | vc_internal.append(menus["git"]["internal"]) 650 | if not self.hg_disabled: 651 | vc_internal.append(menus["hg"]["internal"]) 652 | if len(vc_internal): 653 | vc_internal_menu = ",\n".join(vc_internal) 654 | 655 | vc_external = [] 656 | vc_external_menu = None 657 | if self.show_ext: 658 | if not self.svn_disabled: 659 | vc_external.append(menus["svn"]["external"]) 660 | if not self.git_disabled: 661 | vc_external.append(menus["git"]["external"]) 662 | if not self.hg_disabled: 663 | vc_external.append(menus["hg"]["external"]) 664 | if len(vc_external): 665 | vc_external_menu = ",\n".join(vc_external) 666 | with open(menu, "w") as f: 667 | f.write( 668 | DIFF_MENU % { 669 | "internal": ("" if not self.show_int else menus["internal"] % {"file_name": self.name}), 670 | "external": ("" if not self.show_ext else menus["external"] % {"file_name": self.name}), 671 | "vc_internal": ("" if vc_internal_menu is None or not self.show_int else VC_INTERNAL_MENU % {"vc": vc_internal_menu}), 672 | "vc_external": ("" if vc_external_menu is None or not self.show_ext else VC_EXTERNAL_MENU % {"vc": vc_external_menu}) 673 | } 674 | ) 675 | 676 | def remove_menu(self, menu_name): 677 | if exists(self.menu_path): 678 | menu = join(self.menu_path, menu_name) 679 | if exists(menu): 680 | remove(menu) 681 | 682 | def update_context_menu(self): 683 | menus = { 684 | "internal": INTERNAL_MENU, 685 | "external": EXTERNAL_MENU, 686 | "svn": { 687 | "internal": SVN_INTERNAL_MENU, 688 | "external": SVN_EXTERNAL_MENU 689 | }, 690 | "git": { 691 | "internal": GIT_INTERNAL_MENU, 692 | "external": GIT_EXTERNAL_MENU 693 | }, 694 | "hg": { 695 | "internal": HG_INTERNAL_MENU, 696 | "external": HG_EXTERNAL_MENU 697 | } 698 | } 699 | if "view" in self.menu_types: 700 | self.update_menu(CONTEXT_MENU, menus) 701 | else: 702 | self.remove_menu(CONTEXT_MENU) 703 | 704 | def update_sidebar_menu(self): 705 | menus = { 706 | "internal": INTERNAL_SIDEBAR_MENU, 707 | "external": EXTERNAL_SIDEBAR_MENU, 708 | "svn": { 709 | "internal": SVN_SIDEBAR_INTERNAL_MENU, 710 | "external": SVN_SIDEBAR_EXTERNAL_MENU 711 | }, 712 | "git": { 713 | "internal": GIT_SIDEBAR_INTERNAL_MENU, 714 | "external": GIT_SIDEBAR_EXTERNAL_MENU 715 | }, 716 | "hg": { 717 | "internal": HG_SIDEBAR_INTERNAL_MENU, 718 | "external": HG_SIDEBAR_EXTERNAL_MENU 719 | } 720 | } 721 | if "sidebar" in self.menu_types: 722 | self.update_menu(SIDEBAR_MENU, menus) 723 | else: 724 | self.remove_menu(SIDEBAR_MENU) 725 | 726 | def update_tab_menu(self): 727 | menus = { 728 | "internal": INTERNAL_TAB_MENU, 729 | "external": EXTERNAL_TAB_MENU, 730 | "svn": { 731 | "internal": SVN_TAB_INTERNAL_MENU, 732 | "external": SVN_TAB_EXTERNAL_MENU 733 | }, 734 | "git": { 735 | "internal": GIT_TAB_INTERNAL_MENU, 736 | "external": GIT_TAB_EXTERNAL_MENU 737 | }, 738 | "hg": { 739 | "internal": HG_TAB_INTERNAL_MENU, 740 | "external": HG_TAB_EXTERNAL_MENU 741 | } 742 | } 743 | if "tab" in self.menu_types: 744 | self.update_menu(TAB_MENU, menus) 745 | else: 746 | self.remove_menu(TAB_MENU) 747 | 748 | 749 | def update_menu(name="..."): 750 | menu_updater = MenuUpdater(name) 751 | menu_updater.update_context_menu() 752 | menu_updater.update_sidebar_menu() 753 | menu_updater.update_tab_menu() 754 | 755 | 756 | ############################### 757 | # Loaders 758 | ############################### 759 | def refresh_menu(): 760 | update_menu() 761 | debug("refresh menu") 762 | settings = load_settings() 763 | settings.clear_on_change('reload_menu') 764 | settings.add_on_change('reload_menu', refresh_menu) 765 | 766 | 767 | def plugin_loaded(): 768 | refresh_menu() 769 | -------------------------------------------------------------------------------- /easy_diff_global.py: -------------------------------------------------------------------------------- 1 | """ 2 | Easy Diff Global 3 | 4 | Copyright (c) 2013 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | from os.path import exists, normpath, abspath, isdir 9 | from EasyDiff.lib.multiconf import get as multiget 10 | import re 11 | try: 12 | from SubNotify.sub_notify import SubNotifyIsReadyCommand as Notify 13 | except: 14 | class Notify: 15 | @classmethod 16 | def is_ready(cls): 17 | return False 18 | 19 | DEBUG = False 20 | SETTINGS = "easy_diff.sublime-settings" 21 | 22 | 23 | def log(msg, status=False): 24 | string = str(msg) 25 | print("EasyDiff: %s" % string) 26 | if status: 27 | notify(string) 28 | 29 | 30 | def debug(msg, status=False): 31 | if DEBUG: 32 | log(msg, status) 33 | 34 | 35 | def load_settings(): 36 | return sublime.load_settings(SETTINGS) 37 | 38 | 39 | def global_reload(): 40 | set_debug_flag() 41 | settings = load_settings() 42 | settings.clear_on_change('reload_global') 43 | settings.add_on_change('reload_global', global_reload) 44 | 45 | 46 | def set_debug_flag(): 47 | global DEBUG 48 | settings = load_settings() 49 | DEBUG = settings.get("debug", False) 50 | debug("debug logging enabled") 51 | 52 | 53 | def get_encoding(view): 54 | encoding = view.encoding() 55 | mapping = [ 56 | ("with BOM", ""), 57 | ("Windows", "cp"), 58 | ("-", "_"), 59 | (" ", "") 60 | ] 61 | encoding = view.encoding() 62 | m = re.match(r'.+\((.*)\)', encoding) 63 | if m is not None: 64 | encoding = m.group(1) 65 | 66 | for item in mapping: 67 | encoding = encoding.replace(item[0], item[1]) 68 | 69 | return "utf_8" if encoding in ["Undefined", "Hexidecimal"] else encoding 70 | 71 | 72 | def get_external_diff(): 73 | settings = load_settings() 74 | ext_diff = multiget(settings, "external_diff", None) 75 | diff_path = None if ext_diff is None or ext_diff == "" or not exists(abspath(normpath(ext_diff))) else abspath(normpath(ext_diff)) 76 | debug("External diff was not found!" if diff_path is None else "External diff \"%s\" found." % diff_path) 77 | return diff_path 78 | 79 | 80 | def get_target(paths=[], group=-1, index=-1): 81 | target = None 82 | if index != -1: 83 | view = sublime.active_window().views_in_group(group)[index] 84 | target = view.file_name() 85 | if target is None: 86 | target = "" 87 | elif len(paths) and exists(paths[0]) and not isdir(paths[0]): 88 | target = paths[0] 89 | return target 90 | 91 | 92 | def notify(msg): 93 | if load_settings().get("use_sub_notify", False) and Notify.is_ready(): 94 | sublime.run_command("sub_notify", {"title": "EasyDiff", "msg": msg}) 95 | else: 96 | sublime.status_message(msg) 97 | 98 | 99 | def plugin_loaded(): 100 | global_reload() 101 | -------------------------------------------------------------------------------- /easy_diff_version_control.py: -------------------------------------------------------------------------------- 1 | """ 2 | Easy Diff Version Control 3 | 4 | Copyright (c) 2013 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | import sublime_plugin 9 | from os.path import basename, splitext, join 10 | import EasyDiff.lib.svn as svn 11 | import EasyDiff.lib.git as git 12 | import EasyDiff.lib.hg as hg 13 | from EasyDiff.lib.multiconf import get as multiget 14 | from EasyDiff.easy_diff_global import load_settings, log, debug, get_encoding, get_external_diff, get_target, notify 15 | import subprocess 16 | import tempfile 17 | 18 | SVN_ENABLED = False 19 | GIT_ENABLED = False 20 | HG_ENABLED = False 21 | 22 | 23 | ############################### 24 | # Version Control Base 25 | ############################### 26 | class _VersionControlDiff(object): 27 | control_type = "" 28 | control_enabled = False 29 | temp_folder = None 30 | 31 | def get_diff(self, name, **kwargs): 32 | return None 33 | 34 | def is_versioned(self, name): 35 | return False 36 | 37 | def vc_is_enabled(self, name): 38 | enabled = False 39 | if name is not None: 40 | try: 41 | enabled = ( 42 | self.control_enabled and 43 | ( 44 | multiget(load_settings(), "skip_version_check_on_is_enabled", False) or 45 | self.is_versioned(name) 46 | ) 47 | ) 48 | except: 49 | pass 50 | return enabled 51 | 52 | def decode(self, result): 53 | try: 54 | debug("decoding with %s" % self.encoding) 55 | return result.decode(self.encoding) 56 | except: 57 | debug("fallback to utf-8 decode") 58 | return result.decode('utf-8') 59 | 60 | def get_encoding(self): 61 | return get_encoding(self.view) 62 | 63 | def create_temp(self): 64 | if self.temp_folder is None: 65 | self.temp_folder = tempfile.mkdtemp(prefix="easydiff") 66 | 67 | def get_files(name, **kwargs): 68 | return None, None 69 | 70 | def revert_file(self, name): 71 | pass 72 | 73 | def revert(self, name): 74 | result = self.get_diff(name) 75 | 76 | if result == "": 77 | notify("Nothing to Revert") 78 | result = None 79 | 80 | if result is not None and sublime.ok_cancel_dialog("Are you sure you want to revert \"%s\"?" % basename(name)): 81 | try: 82 | self.revert_file(name) 83 | except Exception as e: 84 | debug(e) 85 | sublime.error_message("Could not revert \"%s\"!" % basename(name)) 86 | 87 | def internal_diff(self, name, **kwargs): 88 | result = self.get_diff(name, **kwargs) 89 | 90 | if result == "": 91 | notify("No Difference") 92 | result = None 93 | 94 | if result is not None: 95 | use_buffer = bool(load_settings().get("use_buffer", False)) 96 | 97 | win = sublime.active_window() 98 | if use_buffer: 99 | v = win.new_file() 100 | v.set_name("EasyDiff: %s (%s)" % (self.control_type, basename(name))) 101 | v.set_scratch(True) 102 | v.assign_syntax('Packages/Diff/Diff.tmLanguage') 103 | v.run_command('append', {'characters': result}) 104 | else: 105 | v = win.create_output_panel('easy_diff') 106 | v.assign_syntax('Packages/Diff/Diff.tmLanguage') 107 | v.run_command('append', {'characters': result}) 108 | win.run_command("show_panel", {"panel": "output.easy_diff"}) 109 | 110 | def external_diff(self, name, **kwargs): 111 | self.create_temp() 112 | f1, f2 = self.get_files(name, **kwargs) 113 | ext_diff = get_external_diff() 114 | if f1 is not None and f2 is not None: 115 | subprocess.Popen( 116 | [ 117 | ext_diff, 118 | f1, 119 | f2 120 | ] 121 | ) 122 | 123 | def is_loaded(self): 124 | if self.view.is_loading(): 125 | sublime.set_timeout(self.is_loaded, 100) 126 | else: 127 | self.diff() 128 | 129 | def vc_run(self, **kwargs): 130 | self.kwargs = kwargs 131 | sublime.set_timeout(self.is_loaded, 100) 132 | 133 | def diff(self): 134 | name = self.view.file_name() if self.view is not None else None 135 | self.encoding = self.get_encoding() 136 | if name is not None: 137 | if self.kwargs.get("revert"): 138 | self.revert(name) 139 | else: 140 | external = self.kwargs.get("external", False) 141 | if not external: 142 | self.internal_diff(name, **self.kwargs) 143 | else: 144 | self.external_diff(name, **self.kwargs) 145 | 146 | 147 | class _VersionControlCommand(sublime_plugin.WindowCommand): 148 | def run(self, paths=[], group=-1, index=-1, **kwargs): 149 | if len(paths): 150 | name = get_target(paths) 151 | elif index != -1: 152 | self.view = sublime.active_window().views_in_group(group)[index] 153 | name = self.view.file_name() 154 | else: 155 | self.view = self.window.active_view() 156 | if self.view is None: 157 | return False 158 | name = self.view.file_name() if self.view is not None else None 159 | 160 | if name is None: 161 | return False 162 | 163 | if len(paths): 164 | self.view = self.window.open_file(name) 165 | 166 | self.vc_run(**kwargs) 167 | 168 | def is_enabled(self, paths=[], group=-1, index=-1, **kwargs): 169 | if len(paths) or index != -1: 170 | name = get_target(paths, group, index) 171 | else: 172 | self.view = self.window.active_view() 173 | if self.view is None: 174 | return False 175 | name = self.view.file_name() if self.view is not None else None 176 | 177 | if name is None: 178 | return False 179 | 180 | return self.vc_is_enabled(name) 181 | 182 | 183 | ############################### 184 | # Version Control Specific Classes 185 | ############################### 186 | class _EasyDiffSvn(_VersionControlDiff): 187 | def setup(self): 188 | self.control_type = "SVN" 189 | self.control_enabled = SVN_ENABLED 190 | 191 | def revert_file(self, name): 192 | svn.revert(name) 193 | 194 | def get_files(self, name, **kwargs): 195 | f1 = None 196 | f2 = None 197 | if self.is_versioned(name): 198 | f2 = name 199 | root, ext = splitext(basename(name)) 200 | rev = None 201 | if kwargs.get("last", False): 202 | rev = "PREV" 203 | f1 = join(self.temp_folder, "%s-r%s-LEFT%s" % (root, rev, ext)) 204 | else: 205 | rev = "BASE" 206 | f1 = join(self.temp_folder, "%s-r%s-LEFT%s" % (root, rev, ext)) 207 | svn.export(f2, f1, rev=rev) 208 | else: 209 | log("View not versioned under SVN!", status=True) 210 | return f1, f2 211 | 212 | def is_versioned(self, name): 213 | disabled = multiget(load_settings(), "svn_disabled", False) 214 | return not disabled and svn.is_versioned(name) 215 | 216 | def get_diff(self, name, **kwargs): 217 | result = None 218 | if self.is_versioned(name): 219 | result = self.decode(svn.diff(name, last=kwargs.get("last", False))).replace('\r', '') 220 | else: 221 | log("View not versioned under SVN!", status=True) 222 | return result 223 | 224 | 225 | class _EasyDiffGit(_VersionControlDiff): 226 | def setup(self): 227 | self.control_type = "GIT" 228 | self.control_enabled = GIT_ENABLED 229 | 230 | def revert_file(self, name): 231 | git.checkout(name) 232 | 233 | def get_files(self, name, **kwargs): 234 | f1 = None 235 | f2 = None 236 | if self.is_versioned(name): 237 | f2 = name 238 | root, ext = splitext(basename(name)) 239 | rev = None 240 | if kwargs.get("last", False): 241 | revs = git.getrevision(f2, 2) 242 | if revs is not None and len(revs) == 2: 243 | rev = revs[1] 244 | if rev is not None: 245 | f1 = join(self.temp_folder, "%s-r%s-LEFT%s" % (root, rev, ext)) 246 | else: 247 | rev = "HEAD" 248 | f1 = join(self.temp_folder, "%s-r%s-LEFT%s" % (root, rev, ext)) 249 | if f1 is not None: 250 | with open(f1, "wb") as f: 251 | bfr = git.show(f2, rev) 252 | if bfr is not None: 253 | f.write(bfr) 254 | else: 255 | f1 = None 256 | else: 257 | log("View not versioned under Git!", status=True) 258 | return f1, f2 259 | 260 | def is_versioned(self, name): 261 | disabled = multiget(load_settings(), "git_disabled", False) 262 | return not disabled and git.is_versioned(name) 263 | 264 | def get_diff(self, name, **kwargs): 265 | result = None 266 | if git.is_versioned(name): 267 | result = self.decode( 268 | git.diff( 269 | name, 270 | last=kwargs.get("last", False) 271 | ) 272 | ).replace('\r', '') 273 | else: 274 | log("View not versioned under Git!", status=True) 275 | return result 276 | 277 | 278 | class _EasyDiffHg(_VersionControlDiff): 279 | def setup(self): 280 | self.control_type = "HG" 281 | self.control_enabled = HG_ENABLED 282 | 283 | def revert_file(self, name): 284 | hg.revert(name) 285 | 286 | def get_files(self, name, **kwargs): 287 | f1 = None 288 | f2 = None 289 | if self.is_versioned(name): 290 | f2 = name 291 | root, ext = splitext(basename(name)) 292 | rev = None 293 | if kwargs.get("last", False): 294 | revs = hg.getrevision(f2, 2) 295 | if revs is not None and len(revs) == 2: 296 | rev = revs[1] 297 | if rev is not None: 298 | f1 = join(self.temp_folder, "%s-r%s-LEFT%s" % (root, rev, ext)) 299 | else: 300 | # Leave rev as None 301 | f1 = join(self.temp_folder, "%s-r%s-LEFT%s" % (root, "BASE", ext)) 302 | if f1 is not None: 303 | with open(f1, "wb") as f: 304 | bfr = hg.cat(f2, rev) 305 | if bfr is not None: 306 | f.write(bfr) 307 | else: 308 | f1 = None 309 | else: 310 | log("View not versioned under Mercurial!", status=True) 311 | return f1, f2 312 | 313 | def is_versioned(self, name): 314 | disabled = multiget(load_settings(), "hg_disabled", False) 315 | return not disabled and hg.is_versioned(name) 316 | 317 | def get_diff(self, name, **kwargs): 318 | result = None 319 | if self.is_versioned(name): 320 | result = self.decode(hg.diff(name, last=kwargs.get("last", False))).replace('\r', '') 321 | else: 322 | log("View not versioned under Mercurial!", status=True) 323 | return result 324 | 325 | class _EasyDiffAuto(_VersionControlDiff): 326 | def setup(self): 327 | self.control_type = "Auto" 328 | self.control_enabled = True 329 | self.vcs = None 330 | 331 | def revert_file(self, name): 332 | return self.vcs.revert(name) if self.vcs != None else None 333 | 334 | def get_files(self, name, **kwargs): 335 | return self.vcs.get_files(name, **kwargs) if self.vcs != None else None 336 | 337 | def what_vcs(self, name): 338 | if (not multiget(load_settings(), "svn_disabled", False) and _EasyDiffSvn().is_versioned(name)): 339 | return _EasyDiffSvn() 340 | elif (not multiget(load_settings(), "git_disabled", False) and _EasyDiffGit().is_versioned(name)): 341 | return _EasyDiffGit() 342 | elif (not multiget(load_settings(), "hg_disabled", False) and _EasyDiffHg().is_versioned(name)): 343 | return _EasyDiffHg() 344 | 345 | return None 346 | 347 | def is_versioned(self, name): 348 | #if self.vcs == None: 349 | self.vcs = self.what_vcs(name) 350 | 351 | return self.vcs.is_versioned(name) if self.vcs != None else False 352 | 353 | def get_diff(self, name, **kwargs): 354 | #if self.vcs == None: 355 | self.vcs = self.what_vcs(name) 356 | 357 | return self.vcs.get_diff(name, **kwargs) if self.vcs != None else None 358 | 359 | ############################### 360 | # Version Control Commands 361 | ############################### 362 | class EasyDiffAutoCommand(_VersionControlCommand, _EasyDiffAuto): 363 | def __init__(self, window): 364 | super().__init__(window) 365 | self.setup() 366 | 367 | class EasyDiffSvnCommand(_VersionControlCommand, _EasyDiffSvn): 368 | def __init__(self, window): 369 | super().__init__(window) 370 | self.setup() 371 | 372 | 373 | class EasyDiffGitCommand(_VersionControlCommand, _EasyDiffGit): 374 | def __init__(self, window): 375 | super().__init__(window) 376 | self.setup() 377 | 378 | 379 | class EasyDiffHgCommand(_VersionControlCommand, _EasyDiffHg): 380 | def __init__(self, window): 381 | super().__init__(window) 382 | self.setup() 383 | 384 | 385 | ############################### 386 | # Loaders 387 | ############################### 388 | def setup_vc_binaries(): 389 | global SVN_ENABLED 390 | global GIT_ENABLED 391 | global HG_ENABLED 392 | 393 | settings = load_settings() 394 | svn_path = multiget(settings, "svn", None) 395 | git_path = multiget(settings, "git", None) 396 | hg_path = multiget(settings, "hg", None) 397 | if svn_path is not None and svn_path != "": 398 | svn.set_svn_path(svn_path) 399 | if git_path is not None and git_path != "": 400 | git.set_git_path(git_path) 401 | if hg_path is not None and hg_path != "": 402 | hg.set_hg_path(hg_path) 403 | 404 | try: 405 | log("svn %s" % svn.version()) 406 | SVN_ENABLED = True 407 | except: 408 | log("svn not found or is not working!") 409 | pass 410 | try: 411 | log("git %s" % git.version()) 412 | GIT_ENABLED = True 413 | except: 414 | log("git not found or is not working!") 415 | pass 416 | try: 417 | log("hg %s" % hg.version()) 418 | HG_ENABLED = True 419 | except: 420 | log("hg not found or is not working!") 421 | pass 422 | 423 | settings.clear_on_change('reload_vc') 424 | settings.add_on_change('reload_vc', setup_vc_binaries) 425 | 426 | 427 | def plugin_loaded(): 428 | setup_vc_binaries() 429 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derjanb/EasyDiff/2933ed6c75a0691bed9d2bb297a1ba80d81df810/lib/__init__.py -------------------------------------------------------------------------------- /lib/git.py: -------------------------------------------------------------------------------- 1 | """ 2 | git 3 | 4 | Copyright (c) 2013 Isaac Muse 5 | License: MIT 6 | """ 7 | # import xml.etree.ElementTree as ET 8 | from os import environ 9 | import re 10 | import subprocess 11 | import sys 12 | from os.path import exists, isfile, dirname, join 13 | 14 | if sys.platform.startswith('win'): 15 | _PLATFORM = "windows" 16 | elif sys.platform == "darwin": 17 | _PLATFORM = "osx" 18 | else: 19 | _PLATFORM = "linux" 20 | 21 | _git_path = "git.exe" if _PLATFORM == "windows" else "git" 22 | 23 | # UNSTAGED_DIFF = 0 24 | # STAGED_DIFF = 1 25 | # ALL_DIFF = 2 26 | 27 | 28 | def is_system_root(target): 29 | """ 30 | Check if target is the root folder 31 | """ 32 | 33 | root = False 34 | windows = _PLATFORM == "windows" 35 | if windows and re.match(r"^[A-Za-z]{1}:\\$", target) is not None: 36 | root = True 37 | elif not windows and target == '/': 38 | root = True 39 | 40 | return root 41 | 42 | 43 | def get_git_tree(target): 44 | """ 45 | Recursively get Git tree 46 | """ 47 | 48 | root = is_system_root(target) 49 | is_file = isfile(target) 50 | folder = dirname(target) if is_file else target 51 | if exists(join(folder, ".git")): 52 | return folder 53 | else: 54 | if root: 55 | return None 56 | else: 57 | return get_git_tree(dirname(folder)) 58 | 59 | 60 | def get_git_dir(tree): 61 | """ 62 | Get Git directory from tree 63 | """ 64 | 65 | return join(tree, ".git") 66 | 67 | 68 | def gitopen(args, git_tree=None): 69 | """ 70 | Call Git with arguments 71 | """ 72 | 73 | returncode = None 74 | output = None 75 | 76 | if git_tree is not None: 77 | cmd = [_git_path, "--work-tree=%s" % git_tree, "--git-dir=%s" % get_git_dir(git_tree)] + args 78 | else: 79 | cmd = [_git_path] + args 80 | 81 | env = environ.copy() 82 | 83 | if _PLATFORM == "windows": 84 | env['LC_ALL'] = 'en_US' 85 | startupinfo = subprocess.STARTUPINFO() 86 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 87 | process = subprocess.Popen( 88 | cmd, 89 | startupinfo=startupinfo, 90 | stdout=subprocess.PIPE, 91 | stderr=subprocess.STDOUT, 92 | stdin=subprocess.PIPE, 93 | shell=False, 94 | env=env 95 | ) 96 | else: 97 | env['LC_ALL'] = 'en_US.UTF-8' 98 | process = subprocess.Popen( 99 | cmd, 100 | stdout=subprocess.PIPE, 101 | stderr=subprocess.STDOUT, 102 | stdin=subprocess.PIPE, 103 | shell=False, 104 | env=env 105 | ) 106 | output = process.communicate() 107 | returncode = process.returncode 108 | 109 | assert returncode == 0, "Runtime Error: %s" % output[0].rstrip() 110 | 111 | return output[0] 112 | 113 | 114 | def show(target, rev): 115 | """ 116 | Show file at revision 117 | """ 118 | 119 | assert exists(target), "%s does not exist!" % target 120 | git_tree = get_git_tree(target) 121 | bfr = None 122 | target = target.replace(git_tree, "", 1).lstrip("\\" if _PLATFORM == "windows" else "/") 123 | 124 | if _PLATFORM == "windows": 125 | target = target.replace("\\", "/") 126 | if git_tree is not None: 127 | bfr = gitopen(["show", "%s:%s" % (rev, target)], git_tree) 128 | return bfr 129 | 130 | 131 | def getrevision(target, count=1): 132 | """ 133 | Get revision(s) 134 | """ 135 | 136 | assert exists(target), "%s does not exist!" % target 137 | git_tree = get_git_tree(target) 138 | revs = None 139 | 140 | if git_tree is not None: 141 | revs = [] 142 | lg = gitopen(["log", "--no-color", "--pretty=oneline", "-n", str(count), target], git_tree) 143 | for m in re.finditer(br"([a-f\d]{40}) .*\r?\n", lg): 144 | revs.append(m.group(1).decode("utf-8")) 145 | return revs 146 | 147 | 148 | def checkout(target, rev=None): 149 | """ 150 | Checkout file 151 | """ 152 | 153 | assert exists(target), "%s does not exist!" % target 154 | git_tree = get_git_tree(target) 155 | 156 | if git_tree is not None: 157 | args = ["checkout"] 158 | if rev is not None: 159 | args.append(rev) 160 | args.append(target) 161 | 162 | gitopen(args, git_tree) 163 | 164 | 165 | def diff(target, last=False): 166 | """ 167 | Diff current file against last revision 168 | """ 169 | 170 | assert exists(target), "%s does not exist!" % target 171 | # assert diff_type in [ALL_DIFF, STAGED_DIFF, UNSTAGED_DIFF], "diff_type is bad!" 172 | git_tree = get_git_tree(target) 173 | results = b"" 174 | 175 | if git_tree is not None: 176 | args = ["diff", "--no-color", "--src-prefix=%s/" % git_tree, "--dst-prefix=%s/" % git_tree] 177 | 178 | if last: 179 | revs = getrevision(target, 2) 180 | 181 | if len(revs) == 2: 182 | args += [revs[1], "--"] 183 | else: 184 | args = None 185 | else: 186 | args += ["HEAD", "--"] 187 | 188 | # Staged only 189 | # elif diff_type == STAGED_DIFF: 190 | # args.append("--cached") 191 | 192 | if args: 193 | results = gitopen(args + [target], git_tree) 194 | return results 195 | 196 | 197 | def is_versioned(target): 198 | """ 199 | Check if file/folder is versioned 200 | """ 201 | 202 | assert exists(target), "%s does not exist!" % target 203 | git_tree = get_git_tree(target) 204 | 205 | versioned = False 206 | if git_tree is not None: 207 | output = gitopen(["status", "--ignored", "--porcelain", target], git_tree) 208 | if not (output.startswith(b"!!") or output.startswith(b"??")): 209 | versioned = True 210 | 211 | return versioned 212 | 213 | 214 | def version(): 215 | """ 216 | Get Git app version 217 | """ 218 | 219 | version = None 220 | output = gitopen(['--version']) 221 | m = re.search(br" version ([\d\.A-Za-z]+)", output) 222 | if m is not None: 223 | version = m.group(1).decode('utf-8') 224 | return version 225 | 226 | 227 | def set_git_path(pth): 228 | """ 229 | Set Git path 230 | """ 231 | 232 | global _git_path 233 | _git_path = pth 234 | -------------------------------------------------------------------------------- /lib/hg.py: -------------------------------------------------------------------------------- 1 | """ 2 | hg 3 | 4 | Copyright (c) 2013 Isaac Muse 5 | License: MIT 6 | """ 7 | import xml.etree.ElementTree as ET 8 | from os import environ 9 | import re 10 | import subprocess 11 | import sys 12 | from os.path import exists, dirname 13 | 14 | if sys.platform.startswith('win'): 15 | _PLATFORM = "windows" 16 | elif sys.platform == "darwin": 17 | _PLATFORM = "osx" 18 | else: 19 | _PLATFORM = "linux" 20 | 21 | _hg_path = "hg.exe" if _PLATFORM == "windows" else "hg" 22 | 23 | 24 | def hgopen(args, cwd=None): 25 | """ 26 | Call Git with arguments 27 | """ 28 | 29 | returncode = None 30 | output = None 31 | 32 | cmd = [_hg_path] + args 33 | 34 | env = environ.copy() 35 | 36 | if _PLATFORM == "windows": 37 | env['LC_ALL'] = 'en_US' 38 | startupinfo = subprocess.STARTUPINFO() 39 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 40 | process = subprocess.Popen( 41 | cmd, 42 | startupinfo=startupinfo, 43 | stdout=subprocess.PIPE, 44 | stderr=subprocess.STDOUT, 45 | stdin=subprocess.PIPE, 46 | cwd=cwd, 47 | shell=False, 48 | env=env 49 | ) 50 | else: 51 | env['LC_ALL'] = 'en_US.UTF-8' 52 | process = subprocess.Popen( 53 | cmd, 54 | stdout=subprocess.PIPE, 55 | stderr=subprocess.STDOUT, 56 | stdin=subprocess.PIPE, 57 | cwd=cwd, 58 | shell=False, 59 | env=env 60 | ) 61 | output = process.communicate() 62 | returncode = process.returncode 63 | 64 | assert returncode == 0, "Runtime Error: %s\n%s" % (output[0].rstrip(), str(cmd)) 65 | 66 | return output[0] 67 | 68 | 69 | def cat(target, rev=None): 70 | """ 71 | Show file at revision 72 | """ 73 | 74 | assert exists(target), "%s does not exist!" % target 75 | args = ["cat", target] 76 | if rev is not None: 77 | args += ["-r", str(rev)] 78 | return hgopen(args) 79 | 80 | 81 | def revert(target): 82 | """ 83 | Revert file 84 | """ 85 | 86 | assert exists(target), "%s does not exist!" % target 87 | hgopen(["revert", "--no-backup", target], dirname(target)) 88 | 89 | 90 | def getrevision(target, count=1): 91 | """ 92 | Get revision(s) 93 | """ 94 | 95 | assert exists(target), "%s does not exist!" % target 96 | results = log(target, count) 97 | assert results is not None, "Failed to acquire log info!" 98 | revs = [] 99 | for entry in results.findall('logentry'): 100 | revs.append(entry.attrib["node"]) 101 | return revs 102 | 103 | 104 | def diff(target, last=False): 105 | """ 106 | Diff current file against last revision 107 | """ 108 | 109 | args = None 110 | 111 | assert exists(target), "%s does not exist!" % target 112 | if last: 113 | revs = getrevision(target, 2) 114 | if len(revs) == 2: 115 | args = ["diff", "-p", "-r", revs[1]] 116 | else: 117 | args = ["diff", "-p"] 118 | 119 | return hgopen(args + [target]) if args is not None else b"" 120 | 121 | 122 | def log(target=None, limit=0): 123 | """ 124 | Get hg log(s) 125 | """ 126 | 127 | assert exists(target), "%s does not exist!" % target 128 | 129 | args = ["log", "--style=xml"] 130 | if limit != 0: 131 | args.append("-l") 132 | args.append(str(limit)) 133 | if target is not None: 134 | args.append(target) 135 | output = hgopen(args) 136 | 137 | if output != "": 138 | results = ET.fromstring(output) 139 | else: 140 | results = None 141 | 142 | return results 143 | 144 | 145 | def is_versioned(target): 146 | """ 147 | Check if file/folder is versioned 148 | """ 149 | 150 | assert exists(target), "%s does not exist!" % target 151 | versioned = False 152 | try: 153 | results = log(target, 1) 154 | if results is not None: 155 | if results.find("logentry") is not None: 156 | versioned = True 157 | except: 158 | pass 159 | 160 | return versioned 161 | 162 | 163 | def version(): 164 | """ 165 | Get hg app version 166 | """ 167 | 168 | version = None 169 | output = hgopen(['--version']) 170 | m = re.search(br"\bversion ([\d\.A-Za-z]+)", output) 171 | if m is not None: 172 | version = m.group(1).decode('utf-8') 173 | return version 174 | 175 | 176 | def set_hg_path(pth): 177 | """ 178 | Set hg path 179 | """ 180 | 181 | global _hg_path 182 | _hg_path = pth 183 | -------------------------------------------------------------------------------- /lib/multiconf.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sublime 3 | import re 4 | 5 | """ 6 | Copyright (C) 2012 Isaac Muse 7 | 8 | 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: 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 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. 11 | 12 | Licensed under MIT. 13 | 14 | Thanks to: biermeester and matthjes for their ideas and contributions 15 | 16 | Multiconf is a module that allows you to read platforma and/or host 17 | specific configuration values to be used by Sublime Text 2 plugins. 18 | 19 | Using this module's `get` function, allows the user to replace any settings 20 | value in a '.settings' file with a dictionary containing multiple values. 21 | 22 | Mulitconf does this by using a dictionary with a special identifier 23 | "#multiconf#" and a list of dictionaries identified by a qualifier of the form 24 | 25 | ":[;:]..." 26 | 27 | For example, the following setting 28 | 29 | "user_home": "/home" 30 | 31 | would result in `get("user_home")` returning the value "/home" but it could also 32 | be replaced with 33 | 34 | "user_home": { 35 | "#multiconf#": [ 36 | {"os:windows": "C:\\Users"}, 37 | {"os:linux;host:his_pc": "/home"}, 38 | {"os:linux;host:her_pc": "/home/her/special"} 39 | ] 40 | } 41 | 42 | Now the same configuration file will provide different values depending on the 43 | machine it's on. On an MS Windows machine the value returned by `get` will be 44 | "C:\\Users", and on a Linux machine with the host name 'his_pc' the value will be 45 | "/home". 46 | """ 47 | 48 | __version__ = "1.0" 49 | 50 | __CURRENT_HOSTNAME = socket.gethostname().lower() 51 | 52 | QUALIFIERS = r"""([A-Za-z\d_]*):([^;]*)(?:;|$)""" 53 | 54 | 55 | def get(settings_obj, key, default=None, callback=None): 56 | """ 57 | Return a Sublime Text plugin setting value 58 | 59 | Parameters: 60 | settings_obj - a sublime.Settings object or a dictionary containing 61 | settings 62 | key - the name of the setting 63 | default - the default value to return if the key value is not found. 64 | callback - a callback function that, if provided, will be called with 65 | the found and default values as parameters. 66 | 67 | """ 68 | # Parameter validation 69 | if not isinstance(settings_obj, (dict, sublime.Settings)): 70 | raise AttributeError("Invalid settings object") 71 | if not isinstance(key, str): 72 | raise AttributeError("Invalid callback function") 73 | if callback is not None and not hasattr(callback, '__call__'): 74 | raise AttributeError("Invalid callback function") 75 | 76 | setting = settings_obj.get(key, default) 77 | final_val = None 78 | 79 | if isinstance(setting, dict) and "#multiconf#" in setting: 80 | reject_item = False 81 | for entry in setting["#multiconf#"]: 82 | reject_item = False if isinstance(entry, dict) and len(entry) else True 83 | 84 | k, v = entry.popitem() 85 | 86 | if reject_item: 87 | continue 88 | 89 | for qual in re.compile(QUALIFIERS).finditer(k): 90 | if Qualifications.exists(qual.group(1)): 91 | reject_item = not Qualifications.eval_qual(qual.group(1), qual.group(2)) 92 | else: 93 | reject_item = True 94 | if reject_item: 95 | break 96 | 97 | if not reject_item: 98 | final_val = v 99 | break 100 | 101 | if reject_item: 102 | final_val = default 103 | else: 104 | final_val = setting 105 | 106 | return callback(final_val, default) if callback else final_val 107 | 108 | 109 | class QualException(Exception): 110 | pass 111 | 112 | 113 | class Qualifications(object): 114 | __qualifiers = {} 115 | 116 | @classmethod 117 | def add_qual(cls, key, callback): 118 | if isinstance(key, str) and re.match(r"^[a-zA-Z][a-zA-Z\d_]*$", key) is None: 119 | raise QualException("'%s' is not a valid function name." % key) 120 | if not hasattr(callback, '__call__'): 121 | raise QualException("Bad function callback.") 122 | if key in cls.__qualifiers: 123 | raise QualException("'%s' qualifier already exists." % key) 124 | 125 | cls.__qualifiers[key] = callback 126 | 127 | @classmethod 128 | def exists(cls, key): 129 | return (key in cls.__qualifiers) 130 | 131 | @classmethod 132 | def eval_qual(cls, key, value): 133 | try: 134 | return cls.__qualifiers[key](value) 135 | except: 136 | raise QualException("Failed to execute %s qualifier" % key) 137 | 138 | 139 | def __host_match(h): 140 | return (h.lower() == __CURRENT_HOSTNAME) 141 | 142 | 143 | def __os_match(os): 144 | return (os == sublime.platform()) 145 | 146 | 147 | Qualifications.add_qual("host", __host_match) 148 | Qualifications.add_qual("os", __os_match) 149 | -------------------------------------------------------------------------------- /lib/svn.py: -------------------------------------------------------------------------------- 1 | """ 2 | svn 3 | 4 | Copyright (c) 2013 Isaac Muse 5 | License: MIT 6 | """ 7 | import xml.etree.ElementTree as ET 8 | from os import environ 9 | import re 10 | import subprocess 11 | import sys 12 | from os.path import exists, isfile 13 | 14 | NO_LOCK = 0 15 | LOCAL_LOCK = 1 16 | REMOTE_LOCK = 2 17 | ORPHAN_LOCK = 3 18 | 19 | if sys.platform.startswith('win'): 20 | _PLATFORM = "windows" 21 | elif sys.platform == "darwin": 22 | _PLATFORM = "osx" 23 | else: 24 | _PLATFORM = "linux" 25 | 26 | _svn_path = "svn.exe" if _PLATFORM == "windows" else "svn" 27 | 28 | 29 | def svnopen(args): 30 | """ 31 | Call SVN with arguments 32 | """ 33 | 34 | returncode = None 35 | output = None 36 | cmd = [_svn_path, "--non-interactive"] + args 37 | 38 | env = environ.copy() 39 | 40 | if _PLATFORM == "windows": 41 | env['LC_ALL'] = 'en_US' 42 | startupinfo = subprocess.STARTUPINFO() 43 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 44 | process = subprocess.Popen( 45 | cmd, 46 | startupinfo=startupinfo, 47 | stdout=subprocess.PIPE, 48 | stderr=subprocess.STDOUT, 49 | stdin=subprocess.PIPE, 50 | shell=False, 51 | env=env 52 | ) 53 | else: 54 | env['LC_ALL'] = 'en_US.UTF-8' 55 | process = subprocess.Popen( 56 | cmd, 57 | stdout=subprocess.PIPE, 58 | stderr=subprocess.STDOUT, 59 | stdin=subprocess.PIPE, 60 | shell=False, 61 | env=env 62 | ) 63 | output = process.communicate() 64 | returncode = process.returncode 65 | 66 | assert returncode == 0, "Runtime Error: %s" % output[0].rstrip() 67 | 68 | return output[0] 69 | 70 | 71 | def revert(target): 72 | """ 73 | Revert file 74 | """ 75 | 76 | assert exists(target), "%s does not exist!" % target 77 | svnopen(["revert", target]) 78 | 79 | 80 | def info(target): 81 | """ 82 | Get general SVN info 83 | """ 84 | 85 | assert (target.startswith("http://") or target.startswith("https://")) or exists(target), "%s does not exist!" % target 86 | output = svnopen(['info', "--xml", target]) 87 | return ET.fromstring(output) 88 | 89 | 90 | def searchinfo(xml, *args): 91 | """ 92 | Search the info (acquired via the info method) for the keys given. 93 | Return the related info for each key in a dictionary. 94 | """ 95 | 96 | entry = xml.find("entry") 97 | 98 | if len(args) == 0: 99 | return {} 100 | 101 | keys = {} 102 | for a in args: 103 | try: 104 | if a == "url": 105 | keys[a] = entry.find(a).text 106 | elif a in ["root", "uuid"]: 107 | repo = entry.find("repository") 108 | keys[a] = repo.find(a).text 109 | elif a in ["revision", "author", "date"]: 110 | com = entry.find("commit") 111 | if a == "revision": 112 | keys[a] = com.attrib["revision"] 113 | else: 114 | keys[a] = com.find(a).text 115 | elif a in ["token", "owner", "created", "expires"]: 116 | lk = entry.find("lock") 117 | keys[a] = lk.find(a).text 118 | except: 119 | keys[a] = None 120 | return keys 121 | 122 | 123 | def geturl(pth): 124 | """ 125 | Get SVN url from file 126 | """ 127 | 128 | output = info(pth) 129 | search_targets = ["url"] 130 | keys = searchinfo(output, *search_targets) 131 | return keys.get(search_targets[0]) 132 | 133 | 134 | def getrevision(pth): 135 | """ 136 | Get SVN revision 137 | """ 138 | 139 | output = info(pth) 140 | search_targets = ["revision"] 141 | keys = searchinfo(output, *search_targets) 142 | return keys.get(search_targets[0]) 143 | 144 | 145 | def diff(target, last=False): 146 | """ 147 | Get SVN diff of last version 148 | """ 149 | 150 | assert exists(target), "%s does not exist!" % target 151 | assert isfile(target), "%s is not a file!" % target 152 | return svnopen(['diff', '-rPREV', target]) if last else svnopen(['diff', target]) 153 | 154 | 155 | def commit(pth, msg=""): 156 | """ 157 | Commit changes 158 | """ 159 | 160 | assert exists(pth), "%s does not exist!" % pth 161 | svnopen(["commit", pth, "-m", msg]) 162 | 163 | 164 | def checklock(pth): 165 | """ 166 | Check if file is locked 167 | """ 168 | 169 | lock_msg = "" 170 | lock_type = NO_LOCK 171 | lock_token = None 172 | last_token = None 173 | 174 | url = geturl(pth) 175 | 176 | for obj in [pth, url]: 177 | output = info(pth) 178 | search_targets = ["owner", "created", "token"] 179 | keys = searchinfo(output, *search_targets) 180 | owner = keys.get(search_targets[0]) 181 | when = keys.get(search_targets[1]) 182 | lock_token = keys.get(search_targets[2]) 183 | 184 | if owner is not None: 185 | msg = "SVN (checklock): %s was locked by '%s' at %s" % (pth, owner, when) 186 | if obj == pth: 187 | lock_type = ORPHAN_LOCK 188 | last_token = lock_token 189 | lock_token = None 190 | lock_msg = msg + " : BAD LOCK" 191 | elif obj == url and last_token is not None and last_token == lock_token: 192 | lock_type = LOCAL_LOCK 193 | lock_msg = msg + " : LOCAL LOCK" 194 | elif obj == url and last_token is None: 195 | lock_type = REMOTE_LOCK 196 | lock_msg = msg + " : REMOTE LOCK" 197 | 198 | return lock_msg, lock_type 199 | 200 | 201 | def lock(pth): 202 | """ 203 | Lock file 204 | """ 205 | 206 | assert exists(pth), "%s does not exist!" % pth 207 | svnopen(['lock', pth]) 208 | 209 | 210 | def breaklock(pth, force=False): 211 | """ 212 | Breack file lock 213 | """ 214 | 215 | assert exists(pth), "%s does not exist!" % pth 216 | 217 | args = ['unlock'] 218 | if force: 219 | args += ["--force", pth] 220 | else: 221 | args.append(pth) 222 | 223 | svnopen(args) 224 | 225 | 226 | def checkout(url, pth): 227 | """ 228 | Checkout SVN url 229 | """ 230 | 231 | svnopen(['checkout', url, pth]) 232 | assert exists(pth) 233 | 234 | 235 | def update(pth): 236 | """ 237 | Update SVN directory 238 | """ 239 | 240 | assert exists(pth), "%s does not exist!" % pth 241 | svnopen(['update', pth]) 242 | 243 | 244 | def export(url, name, rev=None): 245 | """ 246 | Export file 247 | """ 248 | 249 | args = ["export"] 250 | if rev is not None: 251 | args.append("-r%s" % str(rev)) 252 | args += [url, name] 253 | 254 | svnopen(args) 255 | assert exists(name), "%s appears to not have been exported!" % name 256 | 257 | 258 | def add(pth): 259 | """ 260 | Add a file 261 | """ 262 | 263 | assert exists(pth), "%s does not exist!" % pth 264 | svnopen(['add', pth]) 265 | 266 | 267 | def cleanup(pth): 268 | """ 269 | Clean up a folder 270 | """ 271 | 272 | assert exists(pth), "%s does not exist!" % pth 273 | svnopen(['cleanup', pth]) 274 | 275 | 276 | def status(pth, ignore_externals=False, ignore_unversioned=False, depth="infinity"): 277 | """ 278 | Get the SVN status for the folder 279 | """ 280 | 281 | assert exists(pth), "%s does not exist!" % pth 282 | 283 | attributes = { 284 | "added": [], 285 | "conflicted": [], 286 | "deleted": [], 287 | "external": [], 288 | "ignored": [], 289 | "incomplete": [], 290 | "merged": [], 291 | "missing": [], 292 | "modified": [], 293 | "none": [], 294 | "normal": [], 295 | "obstructed": [], 296 | "replaced": [], 297 | "unversioned": [] 298 | } 299 | 300 | args = ['status', '--xml', '--depth', depth] 301 | if ignore_externals: 302 | args.append('--ignore-externals') 303 | 304 | args.append(pth) 305 | 306 | output = svnopen(args) 307 | root = ET.fromstring(output) 308 | 309 | target = root.find("target") 310 | entries = target.findall("entry") 311 | if len(entries): 312 | for entry in entries: 313 | s = entry.find("wc-status") 314 | item = s.attrib["item"] 315 | if ignore_unversioned and item == "unversioned": 316 | continue 317 | if ignore_externals and item == "external": 318 | continue 319 | if item in attributes: 320 | attributes[item].append(entry.attrib["path"]) 321 | elif not ignore_unversioned and re.search(r"svn: warning: .* is not a working copy", target.text.lstrip()) is not None: 322 | attributes["unversioned"].append(target.attrib["path"]) 323 | 324 | return attributes 325 | 326 | 327 | def is_versioned(target): 328 | """ 329 | Check if file/folder is versioned 330 | """ 331 | 332 | assert exists(target), "%s does not exist!" % target 333 | 334 | versioned = False 335 | try: 336 | entries = status(target, depth="empty") 337 | if len(entries["unversioned"]) == 0: 338 | versioned = True 339 | except: 340 | pass 341 | 342 | return versioned 343 | 344 | 345 | def version(): 346 | """ 347 | Get SVN app version 348 | """ 349 | 350 | version = None 351 | output = svnopen(['--version']) 352 | m = re.search(br" version (\d+\.\d+\.\d+) ", output) 353 | if m is not None: 354 | version = m.group(1).decode('utf-8') 355 | return version 356 | 357 | 358 | def set_svn_path(pth): 359 | """ 360 | Set SVN path 361 | """ 362 | 363 | global _svn_path 364 | _svn_path = pth 365 | --------------------------------------------------------------------------------