Skip to content

Commit bf03ff6

Browse files
Try (#15)
* stage/unstage hunk * select multiple files to stage/unstage * performance improvements * add new keybindings, 'space' to stage/unstage, 'backspace' to discard changes
1 parent c927259 commit bf03ff6

20 files changed

+517
-250
lines changed

README.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,35 @@
22

33
Get a quick overview of changes before committing them.
44

5-
![Example](img/showcase.png)
6-
75
### Features
86

9-
- Show all modified files
10-
- Show the diff view for a file
11-
- Stage/Unstage files
12-
- Discard changes to a file
7+
- Show modified files (AKA "Status View")
8+
- View diff for a file. (AKA "Diff View")
9+
- Stage/Unstage files/hunks
10+
- Discard changes to files/hunks
1311
- Goto a file
1412

15-
### Installation
13+
![Example](img/showcase.png)
14+
15+
### Getting Started
1616

1717
Open the command palette and run `Package Control: Install Package`, then select `GitDiffView`.
1818

19-
### Instructions
20-
21-
From the command palette select: `Git Diff View: Toggle`.
22-
Or toggle the git diff view with `ctrl+shift+g`(Linux) or `alt+shift+g`(Mac).
19+
Toggle the git diff view with `ctrl+shift+g`(Linux) or `alt+shift+g`(Mac) or via the command palette by selecting: `Git Diff View: Toggle`.
2320
The git diff view won't open if there are no git changes.
2421

25-
Inside the status view, the following keybindings are available:
2622

27-
```
28-
a - stage/unstage a file
29-
d - dismiss changes to a file
30-
g - go to a file
31-
```
23+
### Keybindings in Status View (the right view)
24+
25+
- <kbd>a</kbd> / <kbd>space</kbd> - stage/unstage file
26+
- <kbd>d</kbd> / <kbd>backspace</kbd> - dismiss file changes
27+
- <kbd>g</kbd> - open file
28+
29+
30+
### Keybindings in Diff View (the left view)
31+
32+
- <kbd>a</kbd> / <kbd>space</kbd> - stage/unstage hunk
33+
- <kbd>d</kbd> / <kbd>backspace</kbd> - dismiss hunk change
3234

3335
Type of modification will be shown in the git status, next to the file name.
3436
Here is a list of the types:

command_clear_diff_view.py

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .command_update_diff_view import update_diff_view
12
from .core.git_commands import Git
23
from .core.git_diff_view import GitDiffView
34
from .utils import get_line
@@ -20,12 +21,12 @@ def run(self, _: sublime.Edit) -> None:
2021
if not git_status:
2122
return
2223

23-
def done(option):
24+
def done(option: int) -> None:
2425
if option == -1:
2526
return
2627
# 0 -> Discard changes
2728
if git_status["is_staged"]:
28-
git.reset_head(git_status["file_name"])
29+
git.unstage_files([git_status["file_name"]])
2930
if git_status["modification_type"] == '??':
3031
git.clean(git_status["file_name"])
3132
else:
@@ -34,10 +35,13 @@ def done(option):
3435
self.view.run_command('update_status_view', {
3536
'git_statuses': git_statuses,
3637
})
37-
self.view.run_command("update_diff_view", {
38-
'git_status': git_status,
39-
})
38+
try:
39+
new_git_status = git_statuses[line]
40+
update_diff_view(self.view, new_git_status)
41+
except:
42+
update_diff_view(self.view, None)
4043

4144
self.view.show_popup_menu([
4245
'Confirm Discard'
4346
], done)
47+
window.focus_view(self.view)

command_dismiss_hunk.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import os
2+
3+
from .command_update_diff_view import update_diff_view
4+
from .core.status_view import get_status_view
5+
from .core.git_commands import Git
6+
from .core.git_diff_view import GitDiffView
7+
from .utils import get_line
8+
import sublime
9+
import sublime_plugin
10+
11+
12+
# command: git_diff_view_dismiss_hunk_changes
13+
class GitDiffViewDismissHunkChangesCommand(sublime_plugin.TextCommand):
14+
def run(self, _: sublime.Edit) -> None:
15+
def done(option: int) -> None:
16+
if option == -1:
17+
return
18+
window = self.view.window()
19+
if not window:
20+
return
21+
diff_view = self.view
22+
sel = diff_view.sel()
23+
if not sel:
24+
return
25+
cursor = sel[0].b
26+
status_view = get_status_view(window.views())
27+
if not status_view:
28+
return
29+
line = get_line(status_view)
30+
if line is None:
31+
return
32+
git = Git(window)
33+
git_statuses = GitDiffView.git_statuses[window.id()]
34+
git_status = git_statuses[line]
35+
if not git_status:
36+
return
37+
head = git.diff_head(git_status['file_name'])
38+
39+
start_patch = diff_view.find('^@@', cursor, sublime.REVERSE)
40+
if not start_patch:
41+
return
42+
43+
end_patch = diff_view.find('^@@', cursor)
44+
if not end_patch:
45+
end_patch = sublime.Region(diff_view.size())
46+
hunk_content = diff_view.substr(sublime.Region(start_patch.begin(), end_patch.begin()))
47+
patch_content = f"{head}\n{hunk_content}"
48+
49+
not_staged = not git_status['is_staged'] or diff_view.find('^Unstaged', cursor, sublime.REVERSE)
50+
temp_patch_file = os.path.join(sublime.cache_path(), 'temp_patch_file.patch')
51+
patch_file = open(temp_patch_file, 'w', encoding='utf-8')
52+
try:
53+
patch_file.write(patch_content)
54+
patch_file.close()
55+
if not_staged:
56+
git.discard_patch(temp_patch_file)
57+
else:
58+
# for staged files first unstage patch
59+
git.unstage_patch(temp_patch_file)
60+
git.discard_patch(temp_patch_file)
61+
finally:
62+
patch_file.close()
63+
os.remove(patch_file.name)
64+
65+
new_git_statuses = git.git_statuses()
66+
status_view.run_command('update_status_view', {
67+
'git_statuses': new_git_statuses,
68+
})
69+
try:
70+
new_git_status = new_git_statuses[line]
71+
update_diff_view(self.view, new_git_status)
72+
except:
73+
update_diff_view(self.view, None)
74+
75+
self.view.show_popup_menu([
76+
'Discard Hunk'
77+
], done)

command_goto_file.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ def run(self, _: sublime.Edit) -> None:
3030
git_status["file_name"])
3131
window.run_command('toggle_git_diff_view')
3232
view = window.open_file(absolute_path_to_file)
33-
sublime.set_timeout(lambda: window.focus_view(view))
33+
34+
def deffer_focus_view() -> None:
35+
if window:
36+
window.focus_view(view)
37+
38+
sublime.set_timeout(deffer_focus_view)

command_goto_hunk.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from .core.status_view import get_status_view
2+
from .core.git_commands import Git
3+
from .core.git_diff_view import GitDiffView
4+
from .utils import get_line
5+
from os import path
6+
import sublime
7+
import sublime_plugin
8+
9+
10+
# command: git_diff_view_goto_file
11+
class GitDiffViewGotoHunkCommand(sublime_plugin.TextCommand):
12+
def run(self, _: sublime.Edit) -> None:
13+
diff_view = self.view
14+
window = self.view.window()
15+
if not window:
16+
return
17+
status_view = get_status_view(window.views())
18+
if not status_view:
19+
return
20+
line = get_line(status_view)
21+
if line is None:
22+
return
23+
git = Git(window)
24+
git_statuses = GitDiffView.git_statuses[window.id()]
25+
git_status = git_statuses[line]
26+
if not git_status:
27+
return
28+
if 'D' in git_status["modification_type"]:
29+
window.status_message("GitDiffVIew: Can't go to a deleted file")
30+
return
31+
file_name = git_status["file_name"]
32+
if not git.git_root_dir or not file_name:
33+
return
34+
absolute_path_to_file = path.join(git.git_root_dir,
35+
git_status["file_name"])
36+
sel = diff_view.sel()
37+
if not sel:
38+
return
39+
cursor = sel[0].b
40+
start_patch = diff_view.find('^@@.+@@', cursor, sublime.REVERSE)
41+
row = 0
42+
if start_patch:
43+
header = diff_view.substr(start_patch).split(' ') # ['@@', '-23,5', '+23,10', '@@']
44+
row = int(header[2].replace('-', '').replace('+', '').split(',')[0]) # row = 23
45+
window.run_command('toggle_git_diff_view')
46+
47+
def deffer_goto_file():
48+
if window.is_valid():
49+
view = window.open_file(f"{absolute_path_to_file}:{row}", sublime.ENCODED_POSITION)
50+
window.focus_view(view)
51+
52+
sublime.set_timeout(deffer_goto_file, 50)

command_stage_unstage_file.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import sublime
22
from .core.git_commands import Git
33
from .core.git_diff_view import GitDiffView
4-
from .utils import get_line
4+
from .utils import get_selected_lines, get_line
55
import sublime_plugin
6-
6+
from .command_update_diff_view import update_diff_view
77

88
# command: git_diff_view_stage_unstage
99
class GitDiffViewStageUnstageCommand(sublime_plugin.TextCommand):
1010
def run(self, _: sublime.Edit) -> None:
1111
window = self.view.window()
1212
if not window:
1313
return
14+
selected_lines = get_selected_lines(self.view)
1415
line = get_line(self.view)
1516
if line is None:
1617
return
@@ -19,14 +20,14 @@ def run(self, _: sublime.Edit) -> None:
1920
git_status = git_statuses[line]
2021
if not git_status:
2122
return
23+
24+
file_names = [git_statuses[line]['file_name'] for line in selected_lines]
2225
if git_status["is_staged"]:
23-
git.reset_head(git_status["file_name"])
26+
git.unstage_files(file_names)
2427
else:
25-
git.add(git_status["file_name"])
28+
git.stage_files(file_names)
2629
git_statuses = git.git_statuses()
2730
self.view.run_command('update_status_view', {
2831
'git_statuses': git_statuses,
2932
})
30-
self.view.run_command("update_diff_view", {
31-
'git_status': git_status,
32-
})
33+
update_diff_view(self.view, git_status)

command_stage_unstage_hunk.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from .command_update_diff_view import update_diff_view
2+
import sublime
3+
from .core.git_commands import Git
4+
from .core.status_view import get_status_view
5+
from .core.git_diff_view import GitDiffView
6+
from .utils import get_line
7+
import sublime_plugin
8+
import os
9+
10+
11+
# command: git_diff_view_stage_unstage_hunk
12+
class GitDiffViewStageUnstageHunkCommand(sublime_plugin.TextCommand):
13+
def run(self, _: sublime.Edit) -> None:
14+
window = self.view.window()
15+
if not window:
16+
return
17+
diff_view = self.view
18+
sel = diff_view.sel()
19+
if not sel:
20+
return
21+
cursor = sel[0].b
22+
status_view = get_status_view(window.views())
23+
if not status_view:
24+
return
25+
line = get_line(status_view)
26+
if line is None:
27+
return
28+
git = Git(window)
29+
git_statuses = GitDiffView.git_statuses[window.id()]
30+
git_status = git_statuses[line]
31+
if not git_status:
32+
return
33+
head = git.diff_head(git_status['file_name'])
34+
35+
start_patch = diff_view.find('^@@', cursor, sublime.REVERSE)
36+
if not start_patch:
37+
return
38+
39+
end_patch = diff_view.find('^@@', cursor)
40+
if not end_patch:
41+
end_patch = sublime.Region(diff_view.size())
42+
hunk_content = diff_view.substr(sublime.Region(start_patch.begin(), end_patch.begin()))
43+
patch_content = f"{head}\n{hunk_content}"
44+
45+
not_staged = not git_status['is_staged'] or diff_view.find('^Unstaged', cursor, sublime.REVERSE)
46+
temp_patch_file = os.path.join(sublime.cache_path(), 'temp_patch_file.patch')
47+
patch_file = open(temp_patch_file, 'w', encoding='utf-8')
48+
try:
49+
patch_file.write(patch_content)
50+
patch_file.close()
51+
if not_staged:
52+
git.stage_patch(temp_patch_file)
53+
else:
54+
git.unstage_patch(temp_patch_file)
55+
finally:
56+
patch_file.close()
57+
os.remove(patch_file.name)
58+
59+
new_git_statuses = git.git_statuses()
60+
new_git_status = new_git_statuses[line]
61+
status_view.run_command('update_status_view', {
62+
'git_statuses': new_git_statuses,
63+
})
64+
update_diff_view(self.view, new_git_status)

0 commit comments

Comments
 (0)