Skip to content

Commit

Permalink
Merge pull request #1960 from timbrel/iter-next-commits
Browse files Browse the repository at this point in the history
Iterate on next/previous commits navigation
  • Loading branch information
kaste authored Dec 10, 2024
2 parents c3e95ab + 6e886cf commit 5ae4e38
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 57 deletions.
6 changes: 2 additions & 4 deletions core/commands/blame.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@

NOT_COMMITED_HASH = "0000000000000000000000000000000000000000"
BLAME_TITLE = "BLAME: {}{}"
DEFAULT_COMMIT_HASH_LENGTH = 8


class BlameMixin(GsTextCommand):
Expand Down Expand Up @@ -285,8 +284,7 @@ def parse_blame(self, blame_porcelain):
match = re.match(r"([0-9a-f]{40}) (\d+) (\d+)( \d+)?", line)
assert match
commit_hash, orig_lineno, final_lineno, _ = match.groups()
short_hash_length = self.current_state().get("short_hash_length", DEFAULT_COMMIT_HASH_LENGTH)
commits[commit_hash]["short_hash"] = commit_hash[:short_hash_length]
commits[commit_hash]["short_hash"] = self.get_short_hash(commit_hash)
commits[commit_hash]["long_hash"] = commit_hash

next_line = next(lines_iter)
Expand Down Expand Up @@ -480,7 +478,7 @@ def run(self, edit):
commit_hash = self.find_selected_commit_hash()
self.window.run_command("gs_graph", {
"all": True,
"follow": self.get_short_hash(commit_hash) if commit_hash else "HEAD",
"follow": commit_hash or "HEAD",
})


Expand Down
6 changes: 3 additions & 3 deletions core/commands/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,15 +1084,15 @@ def load_file_at_line(self, commit_hash, filename, line, col):
Show file at target commit if `git_savvy.diff_view.target_commit` is non-empty.
Otherwise, open the file directly.
"""
target_commit = commit_hash or self.view.settings().get("git_savvy.diff_view.target_commit")
full_path = os.path.join(self.repo_path, filename)
window = self.view.window()
if not window:
return

if target_commit:
target_commit = self.view.settings().get("git_savvy.diff_view.target_commit")
if commit_hash or target_commit:
window.run_command("gs_show_file_at_commit", {
"commit_hash": target_commit,
"commit_hash": commit_hash or self.resolve_commitish(target_commit),
"filepath": full_path,
"position": Position(line - 1, col - 1, None),
})
Expand Down
10 changes: 4 additions & 6 deletions core/commands/inline_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,8 @@ def open_from_diff_view(self, view):
"syntax": syntax_file,
"cached": bool(cached),
"match_position": cur_pos,
"base_commit": base_commit,
"target_commit": target_commit
"base_commit": self.resolve_commitish(base_commit) if base_commit else None,
"target_commit": self.resolve_commitish(target_commit) if target_commit else None
})

def open_from_show_file_at_commit_view(self, view):
Expand Down Expand Up @@ -280,7 +280,7 @@ def open_from_commit_info(self, view):

file_path = os.path.normpath(os.path.join(repo_path, jump_position.filename))
syntax_file = util.file.guess_syntax_for_file(self.window, file_path)
target_commit = jump_position.commit_hash
target_commit = self.get_short_hash(jump_position.commit_hash)
base_commit = self.previous_commit(target_commit, file_path)
cur_pos = Position(
jump_position.line - 1,
Expand Down Expand Up @@ -983,9 +983,7 @@ def run(self, edit):
return

new_target_commit = base_commit
new_base_commit = self.previous_commit(base_commit, file_path)
if new_base_commit:
show_file_at_commit.remember_next_commit_for(view, {new_base_commit: base_commit})
new_base_commit = show_file_at_commit.get_previous_commit(self, view, base_commit, file_path)
settings.set("git_savvy.inline_diff_view.base_commit", new_base_commit)
settings.set("git_savvy.inline_diff_view.target_commit", new_target_commit)

Expand Down
7 changes: 4 additions & 3 deletions core/commands/show_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def run(self, commit_hash):
repo_path = self.repo_path
if commit_hash in {"", "HEAD"}:
commit_hash = self.git("rev-parse", "--short", "HEAD").strip()
else:
commit_hash = self.get_short_hash(commit_hash)

this_id = (
repo_path,
Expand All @@ -76,7 +78,7 @@ def run(self, commit_hash):
focus_view(view)
break
else:
title = SHOW_COMMIT_TITLE.format(self.get_short_hash(commit_hash))
title = SHOW_COMMIT_TITLE.format(commit_hash)
view = util.view.create_scratch_view(self.window, "show_commit", {
"title": title,
"syntax": "Packages/GitSavvy/syntax/show_commit.sublime-syntax",
Expand Down Expand Up @@ -325,12 +327,11 @@ def run(self, edit):
file_path: Optional[str] = settings.get("git_savvy.file_path")
commit_hash: str = settings.get("git_savvy.show_commit_view.commit")

previous_commit = self.previous_commit(commit_hash, file_path)
previous_commit = show_file_at_commit.get_previous_commit(self, view, commit_hash, file_path)
if not previous_commit:
flash(view, "No older commit found.")
return

show_file_at_commit.remember_next_commit_for(view, {previous_commit: commit_hash})
show_commit_info.remember_view_state(view)
settings.set("git_savvy.show_commit_view.commit", previous_commit)

Expand Down
2 changes: 2 additions & 0 deletions core/commands/show_commit_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def ensure_panel_is_visible(window, name=PANEL_NAME):

class gs_show_commit_info(WindowCommand, GitCommand):
def run(self, commit_hash, file_path=None, from_log_graph=False):
commit_hash = self.get_short_hash(commit_hash)

# We're running either blocking or lazy, and currently choose
# automatically. Generally, we run blocking to reduce multiple
# UI changes in short times. Since this panel is a companion
Expand Down
36 changes: 31 additions & 5 deletions core/commands/show_file_at_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ def run(self, commit_hash: str = None, filepath: str = None,
if lang is None:
lang = view.settings().get('syntax')

if not commit_hash:
if commit_hash:
commit_hash = self.get_short_hash(commit_hash)
else:
commit_hash = self.recent_commit("HEAD", filepath)
if not commit_hash:
self.window.status_message("No older revision of this file found.")
Expand Down Expand Up @@ -119,7 +121,7 @@ def create_view(self, commit_hash: str, file_path: str,
active_view = self.window.active_view()
title = SHOW_COMMIT_TITLE.format(
os.path.basename(file_path),
self.get_short_hash(commit_hash),
commit_hash,
)
view = util.view.create_scratch_view(self.window, "show_file_at_commit", {
"title": title,
Expand Down Expand Up @@ -239,12 +241,11 @@ def run(self, edit) -> None:
file_path = settings.get("git_savvy.file_path")
commit_hash = settings.get("git_savvy.show_file_at_commit_view.commit")

previous_commit = self.previous_commit(commit_hash, file_path)
previous_commit = get_previous_commit(self, view, commit_hash, file_path)
if not previous_commit:
flash(view, "No older commit found.")
return

remember_next_commit_for(view, {previous_commit: commit_hash})
settings.set("git_savvy.show_file_at_commit_view.commit", previous_commit)

position = capture_cur_position(view)
Expand Down Expand Up @@ -302,6 +303,7 @@ def get_next_commit(
commit_hash: str,
file_path: str | None = None
) -> str | None:
commit_hash = cmd.get_short_hash(commit_hash)
if next_commit := recall_next_commit_for(view, commit_hash):
return next_commit

Expand All @@ -310,6 +312,21 @@ def get_next_commit(
return next_commits.get(commit_hash)


def get_previous_commit(
cmd: GitCommand,
view: sublime.View,
commit_hash: str,
file_path: str | None = None
) -> Optional[str]:
commit_hash = cmd.get_short_hash(commit_hash)
if previous := recall_previous_commit_for(view, commit_hash):
return previous

if previous := cmd.previous_commit(commit_hash, file_path):
remember_next_commit_for(view, {previous: commit_hash})
return previous


def remember_next_commit_for(view: sublime.View, mapping: Dict[str, str]) -> None:
settings = view.settings()
store: Dict[str, str] = settings.get("git_savvy.next_commits", {})
Expand All @@ -323,6 +340,15 @@ def recall_next_commit_for(view: sublime.View, commit_hash: str) -> Optional[str
return store.get(commit_hash)


def recall_previous_commit_for(view: sublime.View, commit_hash: str) -> Optional[str]:
settings = view.settings()
store: Dict[str, str] = settings.get("git_savvy.next_commits", {})
try:
return next(previous for previous, next_commit in store.items() if next_commit == commit_hash)
except StopIteration:
return None


def pass_next_commits_info_along(view: Optional[sublime.View], to: sublime.View) -> None:
if not view:
return
Expand Down Expand Up @@ -412,7 +438,7 @@ def selected_index(self, commit_hash): # type: ignore[override]

view = self.view
shown_hash = view.settings().get("git_savvy.show_file_at_commit_view.commit")
return commit_hash == shown_hash
return commit_hash.startswith(shown_hash)


class gs_show_file_at_commit_open_commit(TextCommand):
Expand Down
15 changes: 13 additions & 2 deletions core/fns.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from __future__ import annotations
from collections import deque
from functools import partial
from itertools import accumulate as accumulate_, chain, islice, tee

from typing import Callable, Iterable, Iterator, List, Optional, Set, Tuple, TypeVar
from typing import Any, Callable, Iterable, Iterator, List, Optional, Set, Tuple, TypeVar
T = TypeVar('T')
U = TypeVar('U')


NOT_SET: Any = object()
filter_ = partial(filter, None) # type: Callable[[Iterable[Optional[T]]], Iterator[T]]
flatten = chain.from_iterable

Expand Down Expand Up @@ -71,6 +72,16 @@ def tail(iterable):
return drop(1, iterable)


def last(iterable, default=NOT_SET):
# type: (Iterable[T], T | None) -> Optional[T]
try:
return deque(iterable, 1)[0]
except IndexError:
if default is NOT_SET:
raise
return default


def unzip(zipped):
# type: (Iterable[Tuple[T, U]]) -> Tuple[Tuple[T, ...], Tuple[U, ...]]
return tuple(zip(*zipped)) # type: ignore
Expand Down
76 changes: 42 additions & 34 deletions core/git_mixins/history.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from __future__ import annotations
import email.utils
from itertools import chain
from itertools import chain, takewhile

from ..exceptions import GitSavvyError
from ...common import util
from GitSavvy.core.fns import pairwise
from GitSavvy.core.fns import last, pairwise
from GitSavvy.core.git_command import mixin_base
from GitSavvy.core.utils import cached

Expand Down Expand Up @@ -191,6 +191,9 @@ def get_short_hash(self, commit_hash):
self.update_store({"short_hash_length": len(short_hash)})
return short_hash

def resolve_commitish(self, ref: str) -> str:
return self.git("rev-parse", "--short", ref).strip()

def filename_at_commit(self, filename, commit_hash):
# type: (str, str) -> str
lines = self.git(
Expand Down Expand Up @@ -359,31 +362,25 @@ def commit_subject_and_date_from_patch(self, patch: str) -> CommitInfo:
@cached(not_if={"current_commit": is_dynamic_ref})
def previous_commit(self, current_commit, file_path=None, follow=False):
# type: (str, Optional[str], bool) -> Optional[str]
try:
return self._log_commits(
current_commit, file_path, follow, limit=2
)[1]
except IndexError:
return None
return last(
self._log_commits_linewise(current_commit, file_path, follow, limit=2),
None
)

@cached(not_if={"current_commit": is_dynamic_ref})
def recent_commit(self, current_commit, file_path=None, follow=False):
# type: (str, Optional[str], bool) -> Optional[str]
try:
return self._log_commits(
current_commit, file_path, follow, limit=1
)[0]
except IndexError:
return None
return last(
self._log_commits_linewise(current_commit, file_path, follow, limit=1),
None
)

def next_commit(self, current_commit, file_path=None, follow=False):
# type: (str, Optional[str], bool) -> Optional[str]
try:
return self._log_commits(
f"{current_commit}..", file_path, follow
)[-1]
except IndexError:
return None
return last(
self._log_commits_linewise(f"{current_commit}..", file_path, follow),
None
)

def next_commits(
self,
Expand All @@ -392,6 +389,9 @@ def next_commits(
follow: bool = False,
branch_hint: str | None = None,
) -> dict[str, str]:
if current_commit != self.get_short_hash(current_commit):
raise RuntimeError("`next_commits` must be called with a short commit hash.")

if branch_hint is None:
try:
branch_hint = next(iter(
Expand All @@ -400,7 +400,8 @@ def next_commits(
"--format=%(refname)",
"--contains",
current_commit,
"--sort=-committerdate"
"--sort=-committerdate",
"--sort=-HEAD"
).strip().splitlines()
))
except (GitSavvyError, StopIteration):
Expand All @@ -412,25 +413,32 @@ def next_commits(
return {
right: left
for left, right in pairwise(chain(
self._log_commits(f"{current_commit}..{branch_hint}", file_path, follow),
takewhile(
lambda c: c != current_commit,
self._log_commits_linewise(f"{branch_hint}", file_path, follow)
),
[current_commit]
))
}

def _log_commits(
def _log_commits_linewise(
self,
commitish: Optional[str],
file_path: Optional[str],
follow: bool,
limit: Optional[int] = None
) -> List[str]:
return self.git_throwing_silently(
"log",
"--format=%H",
"--topo-order",
"--follow" if follow else None,
None if limit is None else f"-{limit}",
commitish,
"--",
file_path
).strip().splitlines()
) -> Iterator[str]:
return (
line.strip()
for line in self.git_streaming(
"log",
"--format=%h",
"--topo-order",
"--follow" if follow else None,
None if limit is None else f"-{limit}",
commitish,
"--",
file_path,
show_panel_on_error=False
)
)
1 change: 1 addition & 0 deletions tests/test_graph_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def set_global_setting(self, key, value):
def register_commit_info(self, info):
for sha1, info in info.items():
when(gs_show_commit_info).read_commit(sha1, ...).thenReturn(info)
when(gs_show_commit_info).get_short_hash(sha1).thenReturn(sha1)

def create_graph_view_async(self, repo_path, log, wait_for):
when(gs_log_graph_refresh).read_graph(...).thenReturn(
Expand Down

0 comments on commit 5ae4e38

Please sign in to comment.