stage/unstage all actions added

This commit is contained in:
Vaclav Tvrdik
2026-10-10 10:19:06 +02:00
parent 904b941be7
commit 2d9db745b0
5 changed files with 76 additions and 10 deletions

View File

@@ -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))

View File

@@ -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")

View File

@@ -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

View File

@@ -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]:

View File

@@ -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: