Skip to content

Commit

Permalink
Merge f32abe1 into 4ad0e59
Browse files Browse the repository at this point in the history
  • Loading branch information
kaste committed Jul 12, 2019
2 parents 4ad0e59 + f32abe1 commit 2c3623a
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 82 deletions.
136 changes: 56 additions & 80 deletions core/commands/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from contextlib import contextmanager
from functools import partial
from itertools import dropwhile, takewhile
from itertools import chain, dropwhile, takewhile
import os
import re
import bisect
Expand All @@ -20,7 +20,7 @@


if False:
from typing import Callable, Iterator, List, Optional, Tuple, TypeVar
from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar
from mypy_extensions import TypedDict

T = TypeVar('T')
Expand Down Expand Up @@ -357,6 +357,16 @@ def unpickle_sel(pickled_sel):
return [sublime.Region(a, b) for a, b in pickled_sel]


def unique(items):
# type: (Iterable[T]) -> List[T]
"""Remove duplicate entries but remain sorted/ordered."""
rv = [] # type: List[T]
for item in items:
if item not in rv:
rv.append(item)
return rv


def set_and_show_cursor(view, cursors):
sel = view.sel()
sel.clear()
Expand Down Expand Up @@ -500,93 +510,59 @@ def run(self, edit, reset=False):

# Filter out any cursors that are larger than a single point.
cursor_pts = tuple(cursor.a for cursor in self.view.sel() if cursor.a == cursor.b)
diff = parse_diff_in_view(self.view)

self.header_starts = tuple(region.a for region in self.view.find_all("^diff"))
self.header_ends = tuple(region.b for region in self.view.find_all(r"^\+\+\+.+\n(?=@@)"))
self.hunk_starts = tuple(region.a for region in self.view.find_all("^@@"))
self.hunk_ends = sorted(list(
# Hunks end when the next diff starts.
set(self.header_starts[1:]) |
# Hunks end when the next hunk starts, except for hunks
# immediately following diff headers.
(set(self.hunk_starts) - set(self.header_ends)) |
# The last hunk ends at the end of the file.
# It should include the last line (`+ 1`).
set((self.view.size() + 1, ))
))

self.apply_diffs_for_pts(cursor_pts, reset)

def apply_diffs_for_pts(self, cursor_pts, reset):
in_cached_mode = self.view.settings().get("git_savvy.diff_view.in_cached_mode")
context_lines = self.view.settings().get('git_savvy.diff_view.context_lines')

# Apply the diffs in reverse order - otherwise, line number will be off.
for pt in reversed(cursor_pts):
hunk_diff = self.get_hunk_diff(pt)
if not hunk_diff:
return

# The three argument combinations below result from the following
# three scenarios:
#
# 1) The user is in non-cached mode and wants to stage a hunk, so
# do NOT apply the patch in reverse, but do apply it only against
# the cached/indexed file (not the working tree).
# 2) The user is in non-cached mode and wants to undo a line/hunk, so
# DO apply the patch in reverse, and do apply it both against the
# index and the working tree.
# 3) The user is in cached mode and wants to undo a line hunk, so DO
# apply the patch in reverse, but only apply it against the cached/
# indexed file.
#
# NOTE: When in cached mode, no action will be taken when the user
# presses SUPER-BACKSPACE.

args = (
"apply",
"-R" if (reset or in_cached_mode) else None,
"--cached" if (in_cached_mode or not reset) else None,
"--unidiff-zero" if context_lines == 0 else None,
"-",
)
self.git(
*args,
stdin=hunk_diff
)

history = self.view.settings().get("git_savvy.diff_view.history")
history.append((args, hunk_diff, pt, in_cached_mode))
self.view.settings().set("git_savvy.diff_view.history", history)
self.view.settings().set("git_savvy.diff_view.just_hunked", hunk_diff)

self.view.run_command("gs_diff_refresh")
extract = partial(extract_content, self.view)
flatten = chain.from_iterable

def get_hunk_diff(self, pt):
"""
Given a cursor position, find and return the diff header and the
diff for the selected hunk/file.
"""
patches = unique(flatten(filter_(head_and_hunk_for_pt(diff, pt) for pt in cursor_pts)))
patch = ''.join(map(extract, patches))

for hunk_start, hunk_end in zip(self.hunk_starts, self.hunk_ends):
if hunk_start <= pt < hunk_end:
break
if patch:
self.apply_patch(patch, cursor_pts, reset)
else:
window = self.view.window()
if window:
window.status_message('Not within a hunk')
return # Error!

header_start, header_end = max(
(header_start, header_end)
for header_start, header_end in zip(self.header_starts, self.header_ends)
if (header_start, header_end) < (hunk_start, hunk_end)
def apply_patch(self, patch, pts, reset):
in_cached_mode = self.view.settings().get("git_savvy.diff_view.in_cached_mode")
context_lines = self.view.settings().get('git_savvy.diff_view.context_lines')

# The three argument combinations below result from the following
# three scenarios:
#
# 1) The user is in non-cached mode and wants to stage a hunk, so
# do NOT apply the patch in reverse, but do apply it only against
# the cached/indexed file (not the working tree).
# 2) The user is in non-cached mode and wants to undo a line/hunk, so
# DO apply the patch in reverse, and do apply it both against the
# index and the working tree.
# 3) The user is in cached mode and wants to undo a line hunk, so DO
# apply the patch in reverse, but only apply it against the cached/
# indexed file.
#
# NOTE: When in cached mode, no action will be taken when the user
# presses SUPER-BACKSPACE.

args = (
"apply",
"-R" if (reset or in_cached_mode) else None,
"--cached" if (in_cached_mode or not reset) else None,
"--unidiff-zero" if context_lines == 0 else None,
"-",
)
self.git(
*args,
stdin=patch
)

header = self.view.substr(sublime.Region(header_start, header_end))
diff = self.view.substr(sublime.Region(hunk_start, hunk_end))
history = self.view.settings().get("git_savvy.diff_view.history")
history.append((args, patch, pts, in_cached_mode))
self.view.settings().set("git_savvy.diff_view.history", history)
self.view.settings().set("git_savvy.diff_view.just_hunked", patch)

return header + diff
self.view.run_command("gs_diff_refresh")


class GsDiffOpenFileAtHunkCommand(TextCommand, GitCommand):
Expand Down Expand Up @@ -674,7 +650,7 @@ def run(self, edit):
window.status_message("Undo stack is empty")
return

args, stdin, cursor, in_cached_mode = history.pop()
args, stdin, cursors, in_cached_mode = history.pop()
# Toggle the `--reverse` flag.
args[1] = "-R" if not args[1] else None

Expand All @@ -686,4 +662,4 @@ def run(self, edit):

# The cursor is only applicable if we're still in the same cache/stage mode
if self.view.settings().get("git_savvy.diff_view.in_cached_mode") == in_cached_mode:
set_and_show_cursor(self.view, cursor)
set_and_show_cursor(self.view, cursors)
128 changes: 126 additions & 2 deletions tests/test_diff_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sublime

from unittesting import DeferrableTestCase, AWAIT_WORKER
from GitSavvy.tests.mockito import when, unstub, verify
from GitSavvy.tests.mockito import mock, unstub, verify, when
from GitSavvy.tests.parameterized import parameterized as p

import GitSavvy.core.commands.diff as module
Expand Down Expand Up @@ -315,7 +315,93 @@ def test_hunking_one_hunk(self, CURSOR, HUNK, IN_CACHED_MODE=False):
self.assertEqual(len(history), 1)

actual = history.pop()
expected = [['apply', None, '--cached', None, '-'], HUNK, CURSOR, IN_CACHED_MODE]
expected = [['apply', None, '--cached', None, '-'], HUNK, [CURSOR], IN_CACHED_MODE]
self.assertEqual(actual, expected)

HUNK3 = """\
diff --git a/fooz b/barz
--- a/fooz
+++ b/barz
@@ -16,1 +16,1 @@ Hi
one
two
@@ -20,1 +20,1 @@ Ho
three
four
"""

HUNK4 = """\
diff --git a/fooz b/barz
--- a/fooz
+++ b/barz
@@ -20,1 +20,1 @@ Ho
three
four
diff --git a/foxx b/boxx
--- a/foox
+++ b/boox
@@ -16,1 +16,1 @@ Hello
one
two
"""

@p.expand([
# De-duplicate cursors in the same hunk
([58, 79], HUNK1),
([58, 79, 84], HUNK1),
# Combine hunks
([58, 89], HUNK3),
([89, 170], HUNK4),
# Ignore cursors not in a hunk
([2, 11, 58, 79], HUNK1),
([58, 89, 123], HUNK3),
([11, 89, 123, 170], HUNK4),
])
def test_hunking_two_hunks(self, CURSORS, PATCH, IN_CACHED_MODE=False):
VIEW_CONTENT = """\
prelude
--
diff --git a/fooz b/barz
--- a/fooz
+++ b/barz
@@ -16,1 +16,1 @@ Hi
one
two
@@ -20,1 +20,1 @@ Ho
three
four
diff --git a/foxx b/boxx
--- a/foox
+++ b/boox
@@ -16,1 +16,1 @@ Hello
one
two
"""
view = self.window.new_file()
self.addCleanup(view.close)
view.run_command('append', {'characters': VIEW_CONTENT})
view.set_scratch(True)

view.settings().set('git_savvy.diff_view.in_cached_mode', IN_CACHED_MODE)
view.settings().set('git_savvy.diff_view.history', [])
cmd = module.GsDiffStageOrResetHunkCommand(view)
when(cmd).git(...)
when(cmd.view).run_command("gs_diff_refresh")
# when(module.GsDiffStageOrResetHunkCommand).git(...)
# when(module).refresh(view)

view.sel().clear()
for c in CURSORS:
view.sel().add(c)

cmd.run({'unused_edit'})

history = view.settings().get('git_savvy.diff_view.history')
self.assertEqual(len(history), 1)

actual = history.pop()
expected = [['apply', None, '--cached', None, '-'], PATCH, CURSORS, IN_CACHED_MODE]
self.assertEqual(actual, expected)

def test_sets_unidiff_zero_if_no_contextual_lines(self):
Expand Down Expand Up @@ -355,6 +441,44 @@ def test_sets_unidiff_zero_if_no_contextual_lines(self):
expected = ['apply', None, '--cached', '--unidiff-zero', '-']
self.assertEqual(actual, expected)

def test_status_message_if_not_in_hunk(self):
VIEW_CONTENT = """\
prelude
--
diff --git a/fooz b/barz
--- a/fooz
+++ b/barz
@@ -16,1 +16,1 @@ Hi
one
two
@@ -20,1 +20,1 @@ Ho
three
four
diff --git a/foxx b/boxx
--- a/foox
+++ b/boox
@@ -16,1 +16,1 @@ Hello
one
two
"""
view = self.window.new_file()
self.addCleanup(view.close)
view.run_command('append', {'characters': VIEW_CONTENT})
view.set_scratch(True)

window = mock()
when(view).window().thenReturn(window)
when(window).status_message(...)

view.sel().clear()
view.sel().add(0)

# Manually instantiate the cmd so we can inject our known view
cmd = module.GsDiffStageOrResetHunkCommand(view)
cmd.run('_unused_edit')

verify(window, times=1).status_message('Not within a hunk')


class TestZooming(DeferrableTestCase):
@classmethod
Expand Down

0 comments on commit 2c3623a

Please sign in to comment.