stage/unstage all actions added
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import sublime # the stub, via conftest
|
||||
@@ -599,6 +601,45 @@ def test_stage_all_and_unstage_all_need_something_to_do(monkeypatch):
|
||||
assert messages[-1] == "stage all works on uncommitted changes only"
|
||||
|
||||
|
||||
def watched(monkeypatch, tmp_path, all_mode):
|
||||
"""A working-tree view of a.py and b.py on disk with its fingerprint taken; returns (st, refreshes)"""
|
||||
for name in ("a.py", "b.py", "index"):
|
||||
(tmp_path / name).write_text(name)
|
||||
monkeypatch.setattr(commands.git, "index_path", lambda repo: str(tmp_path / "index"))
|
||||
refreshes = []
|
||||
monkeypatch.setattr(commands, "refresh_listing", lambda view, st: refreshes.append(st))
|
||||
st = state.ViewState(str(tmp_path))
|
||||
st.files = [change("a.py"), change("b.py")]
|
||||
st.change, st.all_mode = st.files[0], all_mode
|
||||
st.fingerprint = commands.render.fingerprint(st)
|
||||
return st, refreshes
|
||||
|
||||
|
||||
def touch(path):
|
||||
"""Push the file's mtime forward, as an edit would"""
|
||||
stat = path.stat()
|
||||
os.utime(path, (stat.st_atime, stat.st_mtime + 5))
|
||||
|
||||
|
||||
def test_refresh_if_changed_watches_every_file_of_the_all_files_view(monkeypatch, tmp_path):
|
||||
st, refreshes = watched(monkeypatch, tmp_path, all_mode=True)
|
||||
commands.refresh_if_changed(sublime.View(), st)
|
||||
assert refreshes == []
|
||||
touch(tmp_path / "b.py") # not the file under the caret
|
||||
commands.refresh_if_changed(sublime.View(), st)
|
||||
assert refreshes == [st]
|
||||
|
||||
|
||||
def test_refresh_if_changed_watches_the_shown_file_and_the_index(monkeypatch, tmp_path):
|
||||
st, refreshes = watched(monkeypatch, tmp_path, all_mode=False)
|
||||
touch(tmp_path / "b.py") # another file: the single-file view does not show it
|
||||
commands.refresh_if_changed(sublime.View(), st)
|
||||
assert refreshes == []
|
||||
touch(tmp_path / "index")
|
||||
commands.refresh_if_changed(sublime.View(), st)
|
||||
assert refreshes == [st]
|
||||
|
||||
|
||||
def test_staging_the_only_file_leaves_an_empty_listing(monkeypatch):
|
||||
messages = []
|
||||
monkeypatch.setattr(commands.render, "render_message", lambda view, st, text: messages.append(text))
|
||||
|
||||
@@ -97,6 +97,16 @@ def test_stage_all_unstage_all(repo):
|
||||
assert changes["sub/n.py"].status == "?"
|
||||
|
||||
|
||||
def test_index_path_finds_a_worktrees_index(repo, tmp_path_factory):
|
||||
assert git.index_path(repo) == os.path.join(repo, ".git", "index")
|
||||
# a worktree's .git is a file pointing into the main repository
|
||||
tree = str(tmp_path_factory.mktemp("tree") / "wt")
|
||||
sh(repo, "worktree", "add", "-q", tree)
|
||||
index = git.index_path(tree)
|
||||
assert os.path.isfile(os.path.join(tree, ".git")) and os.path.isfile(index)
|
||||
assert os.path.realpath(index).startswith(os.path.realpath(os.path.join(repo, ".git", "worktrees")))
|
||||
|
||||
|
||||
def test_commits(repo):
|
||||
write(repo, "a.py", "def f():\n return 10\n\n\ndef g():\n return 2\n")
|
||||
write(repo, "c.py", "c\n")
|
||||
|
||||
13
vdiff/git.py
13
vdiff/git.py
@@ -1,4 +1,5 @@
|
||||
"""Git access via subprocess; results are plain tuples, no sublime imports (testable)"""
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -115,6 +116,18 @@ def has_head(repo: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def index_path(repo: str) -> Optional[str]:
|
||||
"""Absolute path of the repository's index file, None outside a repository
|
||||
|
||||
A worktree or submodule keeps it outside <repo>/.git, which there is a file, not a directory
|
||||
"""
|
||||
try:
|
||||
return os.path.join(repo, run(repo, ["rev-parse", "--git-path", "index"]).strip())
|
||||
except (GitError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def base_ref(repo: str) -> str:
|
||||
"""HEAD, or the empty tree when there is no commit yet"""
|
||||
return "HEAD" if has_head(repo) else EMPTY_TREE
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Dict, List, NamedTuple, Optional, Sequence, Tuple, Union
|
||||
import sublime
|
||||
import sublime_plugin
|
||||
|
||||
from . import palette, parse
|
||||
from . import git, palette, parse
|
||||
from .git import FileChange, STATUS_NAMES
|
||||
from .parse import Block, Line
|
||||
from .state import VIEW_FLAG, ViewState
|
||||
@@ -215,11 +215,13 @@ def caret_index(view: sublime.View) -> int:
|
||||
return view.rowcol(view.sel()[0].begin())[0] - _HEADER_ROWS
|
||||
|
||||
|
||||
def fingerprint(st: ViewState) -> Optional[Tuple[Optional[float], Optional[float]]]:
|
||||
"""Cheap "did anything change" key for working-tree mode: mtimes of the file and of the git index"""
|
||||
def fingerprint(st: ViewState) -> Optional[Tuple[Optional[float], ...]]:
|
||||
"""Cheap "did anything change" key for working-tree mode: mtimes of the shown files and of the git index"""
|
||||
if st.in_commit or st.change is None:
|
||||
return None
|
||||
return (_mtime(os.path.join(st.repo, st.change.path)), _mtime(os.path.join(st.repo, ".git", "index")))
|
||||
files = st.files if st.all_mode else [st.change]
|
||||
index = git.index_path(st.repo)
|
||||
return tuple(_mtime(os.path.join(st.repo, c.path)) for c in files) + (_mtime(index) if index else None,)
|
||||
|
||||
|
||||
def _mtime(path: str) -> Optional[float]:
|
||||
|
||||
@@ -48,12 +48,9 @@ class ViewState:
|
||||
token: int = 0 # bumped per show request; a stale async result is dropped
|
||||
rendered_token: int = -1 # token of the request that produced the current render
|
||||
highlight: int = 0 # bumped per quick-panel highlight, to debounce previews
|
||||
# (file mtime, index mtime) at render time, working-tree mode; either mtime is None when unreadable
|
||||
fingerprint: Optional[Tuple[Optional[float], Optional[float]]] = None
|
||||
# all-files view: diffs cached so collapse/expand is instant
|
||||
fingerprint: Optional[Tuple[Optional[float], ...]] = None # all-files view: diffs cached so collapse/expand is instant
|
||||
all_mode: bool = False
|
||||
all_diffs: Dict[str, Tuple[List["Line"], List["Block"], bool]] = field(default_factory=dict)
|
||||
# path -> {(old_no, new_no): [(start, end, colour)]}: syntax colours per row, offsets within the line
|
||||
all_colours: Dict[str, Dict[Tuple[Optional[int], Optional[int]], List[Tuple[int, int, str]]]] = field(default_factory=dict)
|
||||
collapsed: set = field(default_factory=set)
|
||||
all_rows: List[Tuple[Optional["FileChange"], Optional["Line"], bool]] = field(default_factory=list)
|
||||
@@ -94,8 +91,11 @@ class ViewState:
|
||||
|
||||
def set_all_diffs(self, diffs: Dict[str, Tuple[List["Line"], List["Block"], bool]]) -> None:
|
||||
"""Replace the all-files diffs; a file's cached colours survive only if its diff is unchanged"""
|
||||
self.all_colours = {path: colours for path, colours in self.all_colours.items()
|
||||
if path in diffs and path in self.all_diffs and diffs[path][0] == self.all_diffs[path][0]}
|
||||
self.all_colours = {
|
||||
path: colours
|
||||
for path, colours in self.all_colours.items()
|
||||
if path in diffs and path in self.all_diffs and diffs[path][0] == self.all_diffs[path][0]
|
||||
}
|
||||
self.all_diffs = diffs
|
||||
|
||||
def persist(self, view: "sublime.View") -> None:
|
||||
|
||||
Reference in New Issue
Block a user