diff --git a/Default.sublime-commands b/Default.sublime-commands index 0da4e99..b41f15c 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -1,6 +1,32 @@ [ { - "caption": "Patch Apply: Apply", - "command": "patch_apply_do" + "caption": "Preferences: Patch Apply Settings – Default", + "command": "open_file", "args": + { + "file": "${packages}/Patch Apply/Patch Apply.sublime-settings" + } + }, + { + "caption": "Preferences: Patch Apply Settings – User", + "command": "open_file", "args": + { + "file": "${packages}/User/Patch Apply.sublime-settings" + } + }, + { + "caption": "Patch apply: Apply", + "command": "patch_apply" + }, + { + "caption": "Patch apply: Apply reverse", + "command": "patch_apply_reverse" + }, + { + "caption": "Patch apply: Quick apply", + "command": "patch_quick_apply" + }, + { + "caption": "Patch apply: Quick apply reverse", + "command": "patch_quick_apply_reverse" } ] \ No newline at end of file diff --git a/Default.sublime-commands.bak b/Default.sublime-commands.bak deleted file mode 100644 index b248ed5..0000000 --- a/Default.sublime-commands.bak +++ /dev/null @@ -1,68 +0,0 @@ -[ - // { - // "caption": "Preferences: Patch Apply Settings – Default", - // "command": "open_file", "args": - // { - // "file": "${packages}/Patch Apply/Patch Apply.sublime-settings" - // } - // }, - // { - // "caption": "Preferences: Patch Apply Settings – User", - // "command": "open_file", "args": - // { - // "file": "${packages}/User/Patch Apply.sublime-settings" - // } - // }, - // { - // "caption": "Preferences: Patch Apply Key Bindings – Default", - // "command": "open_file", "args": - // { - // "file": "${packages}/Patch Apply/Default (Linux).sublime-keymap", - // "platform": "Linux" - // } - // }, - // { - // "caption": "Preferences: Patch Apply Key Bindings – User", - // "command": "open_file", "args": - // { - // "file": "${packages}/User/Default (Linux).sublime-keymap", - // "platform": "Linux" - // } - // }, - // { - // "caption": "Preferences: Patch Apply Key Bindings – Default", - // "command": "open_file", "args": - // { - // "file": "${packages}/Patch Apply/Default (OSX).sublime-keymap", - // "platform": "OSX" - // } - // }, - // { - // "caption": "Preferences: Patch Apply Key Bindings – User", - // "command": "open_file", "args": - // { - // "file": "${packages}/User/Default (OSX).sublime-keymap", - // "platform": "OSX" - // } - // }, - // { - // "caption": "Preferences: Patch Apply Key Bindings – Default", - // "command": "open_file", "args": - // { - // "file": "${packages}/Patch Apply/Default (Windows).sublime-keymap", - // "platform": "Windows" - // } - // }, - // { - // "caption": "Preferences: Patch Apply Key Bindings – User", - // "command": "open_file", "args": - // { - // "file": "${packages}/User/Default (Windows).sublime-keymap", - // "platform": "Windows" - // } - // }, - // { - // "caption": "Patch Apply: Command name", - // "command": "patch_apply_do" - // } -] \ No newline at end of file diff --git a/Patch Apply.sublime-settings b/Patch Apply.sublime-settings index 154c486..da08064 100644 --- a/Patch Apply.sublime-settings +++ b/Patch Apply.sublime-settings @@ -1,3 +1,6 @@ { - // "first_name": "John" + "excluded_dirs": [ + ".git", ".svn", ".hg", ".cvs" + ], + "diff_syntax": "Packages/Diff/Diff.tmLanguage" } \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..7e23bc4 --- /dev/null +++ b/README @@ -0,0 +1,24 @@ +Description +----------- + +Sublime Text 3 plugin, which allows to apply patches. After choosing command +from Command Pallette new tab is open where diff needs to be pasted. Patch +will be applied after closing tab. + + +Commands +-------- + +* **Apply** - before appling plugin asks for directory where to apply patch + and strip number +* **Patch apply reverse** - plugin asks same questions as in Apply but applies + reverse patch +* **Quick apply** - plugin takes root folder and assumes "1" as strip number +* **Quick apply reverse** - plugin takes same assumptions as in Quick apply but + applies reverse patch + + +Contributors +------------ +1. Kamil Kujawinski +2. Anders Nøklestad (directory selection taken from https://github.com/noklesta/SublimeQuickFileCreator) \ No newline at end of file diff --git a/patch_apply.py b/patch_apply.py index 60944a6..dc5fac7 100644 --- a/patch_apply.py +++ b/patch_apply.py @@ -1,6 +1,11 @@ +import os +import re +import sys + +from functools import partial + import sublime import sublime_plugin -import sys from subprocess import Popen, PIPE @@ -18,32 +23,138 @@ class PatchingFailure(Exception): pass -def apply_patch(content, dir_path, strip=1): - p = Popen( - ["patch", "-p", str(strip), "-d", dir_path], - stdout=PIPE, stdin=PIPE, stderr=PIPE - ) +def apply_patch(content, dir_path, strip=1, reverse=False): + command = ['patch', '-p', str(strip), '-d', dir_path] + if reverse: + command.append('-R') + p = Popen(command, stdout=PIPE, stdin=PIPE, stderr=PIPE) result = p.communicate(input=b(content)) if p.returncode == 0: - return result[0].decode("utf-8") + return result[0].decode('utf-8') else: - raise PatchingFailure(result[1].decode("utf-8")) + output = ' '.join(command) + '\n\n' + output += (result[0] or result[1]).decode('utf-8') + raise PatchingFailure(output) + + +def prepare_exclude_pattern(excluded_dirs): + sep = re.escape(os.path.sep) + excluded_dirs = [re.escape(d) for d in excluded_dirs] + return re.compile( + '^(.*{sep})?({excluded_dirs})({sep}.*)?$'.format( + sep=sep, excluded_dirs='|'.join(excluded_dirs) + ) + ) class PatchApplyListener(sublime_plugin.EventListener): + def __init__(self): + s = sublime.load_settings('Patch Apply.sublime-settings') + excluded_dirs = s.get('excluded_dirs', []) + self.excluded = prepare_exclude_pattern(excluded_dirs) + + def full_process_dir_path(self, callback): + relative_paths, full_torelative_paths = self.build_relative_paths() + self.directory_selector(callback, + relative_paths, full_torelative_paths) + + def quick_process_dir_path(self, callback): + folders = sublime.active_window().folders() + if len(folders) == 1: + callback(folders[0]) + else: + relative_paths, full_torelative_paths = self.build_relative_paths( + top_directories=True) + self.directory_selector(callback, + relative_paths, full_torelative_paths) + + def quick_process_patch_strip(self, callback): + callback('1') + + def full_process_patch_strip(self, callback): + sublime.active_window().show_input_panel( + 'Strip NUM leading components from file names', '1', callback, + None, None + ) + def on_pre_close(self, view): - if not view.settings().get('patch_apply'): + settings = view.settings() + if not settings.get('patch_apply'): return - dir_path = sublime.active_window().extract_variables()['folder'] content = view.substr(sublime.Region(0, view.size())) + reverse_patch = bool(settings.get('reverse_apply')) + quick_apply = settings.get('quick_apply') + + def dir_path_callback(dir_path): + def patch_strip_callback(patch_strip): + self.apply_patch(content, dir_path, patch_strip, + reverse=reverse_patch) + if quick_apply: + self.quick_process_patch_strip(patch_strip_callback) + else: + self.full_process_patch_strip(patch_strip_callback) + + if quick_apply: + self.quick_process_dir_path(dir_path_callback) + else: + self.full_process_dir_path(dir_path_callback) + + def apply_patch(self, content, dir_path, patch_strip, reverse=False): try: - output = apply_patch(content, dir_path) + output = apply_patch(content, dir_path, patch_strip, + reverse=reverse) except PatchingFailure as e: self.results(str(e)) else: self.results(output) + def dir_selected(self, callback, relative_paths, + full_torelative_paths, selected_index): + if selected_index != -1: + selected_dir = relative_paths[selected_index] + selected_dir = full_torelative_paths[selected_dir] + callback(selected_dir) + + def directory_selector(self, callback, relative_paths, + full_torelative_paths): + if len(relative_paths) == 1: + selected_dir = relative_paths[0] + selected_dir = full_torelative_paths[selected_dir] + callback(selected_dir) + elif len(relative_paths) > 1: + # self.move_current_directory_to_top() + sublime.active_window().show_quick_panel( + relative_paths, + partial(self.dir_selected, callback, relative_paths, + full_torelative_paths) + ) + + def build_relative_paths(self, top_directories=False): + folders = sublime.active_window().folders() + relative_paths = [] + full_torelative_paths = {} + for path in folders: + rootfolders = os.path.split(path)[-1] + rel_path_start = os.path.split(path)[0] + + full_torelative_paths[rootfolders] = path + if not self.excluded.search(rootfolders): + relative_paths.append(rootfolders) + + if top_directories: + continue + + for base, dirs, files in os.walk(path): + for dir_ in dirs: + relative_path = os.path.relpath(os.path.join(base, dir_), + rel_path_start) + local_path = os.path.join(base, dir_) + full_torelative_paths[relative_path] = local_path + if not self.excluded.search(relative_path): + relative_paths.append(relative_path) + return relative_paths, full_torelative_paths + def results(self, content): window = sublime.active_window() output = window.create_output_panel('patch_apply_results') @@ -53,12 +164,43 @@ def results(self, content): {'panel': 'output.patch_apply_results'}) -class PatchApplyDoCommand(sublime_plugin.TextCommand): - def run(self, edit): - diff_syntax = "Packages/Diff/Diff.tmLanguage" - +class PatchApplyCommand(sublime_plugin.TextCommand): + def _get_new_view(self): + s = sublime.load_settings('Patch Apply.sublime-settings') + diff_syntax = s.get('diff_syntax', 'Packages/Diff/Diff.tmLanguage') new_view = sublime.active_window().new_file() - new_view.set_name("Paste & apply patch") + new_view.set_name('Paste & apply patch') new_view.set_syntax_file(diff_syntax) new_view.set_scratch(True) new_view.settings().set('patch_apply', True) + return new_view + + def run(self, edit): + self._get_new_view() + + +class QuickApplyMixin(object): + def _get_new_view(self): + new_view = super(QuickApplyMixin, self)._get_new_view() + new_view.settings().set('quick_apply', True) + return new_view + + +class ReverseApplyMixin(object): + def _get_new_view(self): + new_view = super(ReverseApplyMixin, self)._get_new_view() + new_view.settings().set('reverse_apply', True) + return new_view + + +class PatchApplyReverseCommand(ReverseApplyMixin, PatchApplyCommand): + pass + + +class PatchQuickApplyCommand(QuickApplyMixin, PatchApplyCommand): + pass + + +class PatchQuickApplyReverseCommand(QuickApplyMixin, ReverseApplyMixin, + PatchApplyCommand): + pass diff --git a/sample.diff b/sample.diff new file mode 100644 index 0000000..8631fc1 --- /dev/null +++ b/sample.diff @@ -0,0 +1,35 @@ +diff --git a/abc/Main.sublime-menu b/abc/Main.sublime-menu +index 076a129..4eaef42 100644 +--- a/abc/Main.sublime-menu ++++ b/abc/Main.sublime-menu +@@ -1,17 +1,17 @@ + [ +- // { +- // "id": "tools", +- // "children": +- // [ +- // { +- // "caption": "Patch Apply", +- // "children": +- // [ +- // { "caption": "Command name", "command": "patch_apply_do" } +- // ] +- // } +- // ] +- // }, ++ { ++ "id": "tools", ++ "children": ++ [ ++ { ++ "caption": "Patch Apply", ++ "children": ++ [ ++ { "caption": "Command name", "command": "patch_apply_do" } ++ ] ++ } ++ ] ++ }, + // { + // "caption": "Preferences", + // "mnemonic": "n",