Skip to content

Commit a32decb

Browse files
committed
GitAddSelectedHunkCommand: added feature to apply patches from a Diff view
1 parent b6c5724 commit a32decb

File tree

2 files changed

+81
-22
lines changed

2 files changed

+81
-22
lines changed

git/add.py

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import sublime
77
from . import GitTextCommand, GitWindowCommand, git_root
88
from .status import GitStatusCommand
9+
from collections import namedtuple
10+
from .diff import get_GitDiffRootInView
911

1012

1113
class GitAddChoiceCommand(GitStatusCommand):
@@ -45,62 +47,105 @@ def rerun(self, result):
4547

4648

4749
class GitAddSelectedHunkCommand(GitTextCommand):
50+
def is_gitDiffView(self, view):
51+
return view.name() == "Git Diff" and get_GitDiffRootInView(view) is not None
52+
53+
def is_enabled(self):
54+
55+
view = self.active_view()
56+
if self.is_gitDiffView(view):
57+
return True
58+
59+
# First, is this actually a file on the file system?
60+
return super().is_enabled()
61+
4862
def run(self, edit, edit_patch=False):
49-
self.run_command(['git', 'diff', '--no-color', '-U1', self.get_file_name()], lambda result: self.cull_diff(result, edit_patch))
63+
if self.is_gitDiffView(self.view):
64+
kwargs = {}
65+
kwargs['working_dir'] = get_GitDiffRootInView(self.view)
66+
full_diff = self.view.substr(sublime.Region(0,self.view.size()))
67+
self.cull_diff(full_diff, edit_patch=edit_patch, direct_select=True, **kwargs)
68+
else:
69+
self.run_command(['git', 'diff', '--no-color', '-U1', self.get_file_name()], lambda result: self.cull_diff(result, edit_patch))
5070

51-
def cull_diff(self, result, edit_patch=False):
71+
def cull_diff(self, result, edit_patch=False, direct_select=False, **kwargs):
5272
selection = []
5373
for sel in self.view.sel():
5474
selection.append({
5575
"start": self.view.rowcol(sel.begin())[0] + 1,
5676
"end": self.view.rowcol(sel.end())[0] + 1,
5777
})
5878

59-
hunks = [{"diff": ""}]
60-
i = 0
79+
# We devide the diff output into hunk groups. A file header starts a new group.
80+
# Each group can contain zero or more hunks.
81+
HunkGroup = namedtuple("HunkGroup", ["fileHeader","hunkList"])
82+
section = []
83+
hunks = [HunkGroup(section,[])] # Initial lines before hunks
6184
matcher = re.compile('^@@ -([0-9]*)(?:,([0-9]*))? \+([0-9]*)(?:,([0-9]*))? @@')
62-
for line in result.splitlines(keepends=True): # if different line endings, patch will not apply
63-
if line.startswith('@@'):
64-
i += 1
85+
for line_num, line in enumerate(result.splitlines(keepends=True)): # if different line endings, patch will not apply
86+
if line.startswith('diff'): # new file
87+
section = []
88+
hunks.append(HunkGroup(section,[]))
89+
elif line.startswith('@@'): # new hunk
6590
match = matcher.match(line)
6691
start = int(match.group(3))
6792
end = match.group(4)
6893
if end:
6994
end = start + int(end)
7095
else:
7196
end = start
72-
hunks.append({"diff": "", "start": start, "end": end})
73-
hunks[i]["diff"] += line
97+
section = []
98+
hunks[-1].hunkList.append({"diff": section, "start": start, "end": end, "diff_start": line_num+1, "diff_end": line_num+1})
99+
elif hunks[-1].hunkList: # new line for file header or hunk
100+
hunks[-1].hunkList[-1]["diff_end"] = line_num+1 # update hunk end
101+
102+
section.append(line)
74103

75-
diffs = hunks[0]["diff"]
104+
diffs = "".join(hunks[0][0])
76105
hunks.pop(0)
77106
selection_is_hunky = False
78-
for hunk in hunks:
79-
for sel in selection:
80-
if sel["end"] < hunk["start"]:
81-
continue
82-
if sel["start"] > hunk["end"]:
83-
continue
84-
diffs += hunk["diff"] # + "\n\nEND OF HUNK\n\n"
85-
selection_is_hunky = True
107+
for file_header, hunkL in hunks:
108+
file_header = "".join(file_header)
109+
file_header_added = False
110+
for hunk in hunkL:
111+
for sel in selection:
112+
# In direct mode the selected view lines correspond directly to the lines of the diff file
113+
# In indirect mode the selected view lines correspond to the lines in the "@@" hunk header
114+
if direct_select:
115+
hunk_start = hunk["diff_start"]
116+
hunk_end = hunk["diff_end"]
117+
else:
118+
hunk_start = hunk["start"]
119+
hunk_end = hunk["end"]
120+
if sel["end"] < hunk_start:
121+
continue
122+
if sel["start"] > hunk_end:
123+
continue
124+
# Only print the file header once
125+
if not file_header_added:
126+
file_header_added = True
127+
diffs += file_header
128+
hunk_str = "".join(hunk["diff"])
129+
diffs += hunk_str # + "\n\nEND OF HUNK\n\n"
130+
selection_is_hunky = True
86131

87132
if selection_is_hunky:
88133
if edit_patch: # open an input panel to modify the patch
89134
patch_view = self.get_window().show_input_panel(
90135
"Message", diffs,
91-
lambda edited_patch: self.on_input(edited_patch), None, None
136+
lambda edited_patch: self.on_input(edited_patch,**kwargs), None, None
92137
)
93138
s = sublime.load_settings("Git.sublime-settings")
94139
syntax = s.get("diff_syntax", "Packages/Diff/Diff.tmLanguage")
95140
patch_view.set_syntax_file(syntax)
96141
patch_view.settings().set('word_wrap', False)
97142
else:
98-
self.on_input(diffs)
143+
self.on_input(diffs,**kwargs)
99144
else:
100145
sublime.status_message("No selected hunk")
101146

102-
def on_input(self, patch):
103-
self.run_command(['git', 'apply', '--cached'], stdin=patch)
147+
def on_input(self, patch, **kwargs):
148+
self.run_command(['git', 'apply', '--cached'], stdin=patch, **kwargs)
104149

105150
# Also, sometimes we want to undo adds
106151

git/diff.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
import re
77
from . import GitTextCommand, GitWindowCommand, do_when, goto_xy, git_root, get_open_folder_from_window
88

9+
gitDiffRootPrefix = "## git_diff_root: "
10+
11+
def get_GitDiffRootInView(view):
12+
git_root_prefix = gitDiffRootPrefix
13+
line_0 = view.substr(view.line(0))
14+
if git_root_prefix == line_0[:len(git_root_prefix)]:
15+
git_root = line_0[len(git_root_prefix):].strip("\"")
16+
return git_root
17+
return None
18+
19+
def add_gitDiffRootToDiffOutput(output, gitRoot):
20+
return gitDiffRootPrefix + "\"" + gitRoot + "\"\n" + output
921

1022
class GitDiff (object):
1123
def run(self, edit=None, ignore_whitespace=False):
@@ -21,6 +33,8 @@ def diff_done(self, result):
2133
return
2234
s = sublime.load_settings("Git.sublime-settings")
2335
syntax = s.get("diff_syntax", "Packages/Diff/Diff.tmLanguage")
36+
# We add meta-information from which we can infer the git_root after sublime restart
37+
result = add_gitDiffRootToDiffOutput(result, git_root(self.get_working_dir()))
2438
if s.get('diff_panel'):
2539
self.panel(result, syntax=syntax)
2640
else:

0 commit comments

Comments
 (0)