more info in commit list
This commit is contained in:
@@ -196,3 +196,43 @@ def test_commit_message_counts_one_file(monkeypatch):
|
|||||||
commands.ACTIONS["commit"](sublime.View(), window, staged_state(True, False))
|
commands.ACTIONS["commit"](sublime.View(), window, staged_state(True, False))
|
||||||
window.input[2]("one")
|
window.input[2]("one")
|
||||||
assert refreshed == ["committed 1 file"]
|
assert refreshed == ["committed 1 file"]
|
||||||
|
|
||||||
|
|
||||||
|
def commit(subject, files=1, insertions=1, deletions=0, short="abc1234"):
|
||||||
|
return git.Commit("a" * 40, short, "Ann", "2 days ago", subject, files, insertions, deletions)
|
||||||
|
|
||||||
|
|
||||||
|
def test_commit_items_show_totals_before_the_hash():
|
||||||
|
plural, single, merge = commands.commit_items(
|
||||||
|
[
|
||||||
|
commit("fix the parser", 23, 210, 0, "fda1368"),
|
||||||
|
commit("tweak", 1, 1, 2, "d45a250"),
|
||||||
|
commit("merge branch 'side'", None, None, None, "beef123"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert plural.annotation == "23 files changed -0 +210 fda1368"
|
||||||
|
assert single.annotation == "1 file changed -2 +1 d45a250"
|
||||||
|
# no diffstat for a merge, so the row falls back to the hash alone
|
||||||
|
assert merge.annotation == "beef123"
|
||||||
|
assert plural.trigger == "fix the parser" and plural.details == "Ann · 2 days ago"
|
||||||
|
|
||||||
|
|
||||||
|
def test_long_subject_is_clipped_to_clear_the_annotation():
|
||||||
|
long_subject = "a subject that runs on and on " * 6
|
||||||
|
item = commands.commit_items([commit(long_subject, 3, 9, 4)])[0]
|
||||||
|
|
||||||
|
assert item.trigger.endswith("..") and item.trigger[:-2] == long_subject[: len(item.trigger) - 2]
|
||||||
|
# the clipped subject plus the annotation fit the row, leaving the totals and hash readable
|
||||||
|
assert len(item.trigger) + len(item.annotation) <= commands.COMMIT_ROW_WIDTH
|
||||||
|
# a subject that already fits is left alone
|
||||||
|
assert commands.commit_items([commit("short one")])[0].trigger == "short one"
|
||||||
|
|
||||||
|
|
||||||
|
def test_commit_items_keep_a_readable_subject_when_the_annotation_is_huge():
|
||||||
|
# an annotation long enough to eat the whole row: the subject still keeps its floor
|
||||||
|
item = commands.commit_items([commit("x" * 200, 999999, 999999, 999999, "a" * 90)])[0]
|
||||||
|
assert len(item.trigger) == commands.MIN_SUBJECT_WIDTH and item.trigger.endswith("..")
|
||||||
|
|
||||||
|
|
||||||
|
def test_commit_items_fall_back_for_an_empty_subject():
|
||||||
|
assert commands.commit_items([commit("")])[0].trigger == "(no subject)"
|
||||||
|
|||||||
@@ -109,6 +109,37 @@ def test_commits(repo):
|
|||||||
assert info.sha == commits[0].sha
|
assert info.sha == commits[0].sha
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_commits_carry_shortstat_totals(repo):
|
||||||
|
write(repo, "new.py", "one\ntwo\n")
|
||||||
|
sh(repo, "add", "-A")
|
||||||
|
sh(repo, "commit", "-q", "-m", "added a file")
|
||||||
|
write(repo, "a.py", "x\n") # 6 lines become 1
|
||||||
|
write(repo, "b.txt", "y\n") # 1 line becomes 1
|
||||||
|
sh(repo, "commit", "-q", "-am", "changed two files")
|
||||||
|
|
||||||
|
newest, added, initial = git.recent_commits(repo, 3)
|
||||||
|
assert (newest.files, newest.insertions, newest.deletions) == (2, 2, 7)
|
||||||
|
# a pure addition: git omits the deletions clause entirely, which reads as zero, not unknown
|
||||||
|
assert (added.files, added.insertions, added.deletions) == (1, 2, 0)
|
||||||
|
assert initial.subject == "initial" and initial.files == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_commits_without_a_diffstat_report_no_totals(repo):
|
||||||
|
sh(repo, "commit", "-q", "--allow-empty", "-m", "empty")
|
||||||
|
sh(repo, "checkout", "-q", "-b", "side")
|
||||||
|
write(repo, "s.py", "s\n")
|
||||||
|
sh(repo, "add", "-A")
|
||||||
|
sh(repo, "commit", "-q", "-m", "on the side")
|
||||||
|
sh(repo, "checkout", "-q", "main")
|
||||||
|
sh(repo, "merge", "-q", "--no-ff", "side", "-m", "merge side")
|
||||||
|
|
||||||
|
totals = {c.subject: (c.files, c.insertions, c.deletions) for c in git.recent_commits(repo, 5)}
|
||||||
|
# git log prints no diffstat for either, so the totals stay unknown rather than a false zero
|
||||||
|
assert totals["merge side"] == (None, None, None)
|
||||||
|
assert totals["empty"] == (None, None, None)
|
||||||
|
assert totals["on the side"] == (1, 1, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_no_head_repo(tmp_path):
|
def test_no_head_repo(tmp_path):
|
||||||
repo = str(tmp_path)
|
repo = str(tmp_path)
|
||||||
sh(repo, "init", "-q")
|
sh(repo, "init", "-q")
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ STATUS_COLOURS = {
|
|||||||
}
|
}
|
||||||
COMMIT_KIND = (sublime.KIND_ID_COLOR_BLUISH, "c", "commit")
|
COMMIT_KIND = (sublime.KIND_ID_COLOR_BLUISH, "c", "commit")
|
||||||
|
|
||||||
|
# a quick-panel row draws the annotation hard right on the subject's line; past this budget the
|
||||||
|
# subject would run into it, so it is clipped. The floor keeps a readable stub on a narrow panel.
|
||||||
|
COMMIT_ROW_WIDTH = 120
|
||||||
|
MIN_SUBJECT_WIDTH = 24
|
||||||
|
|
||||||
# highlight-to-preview delay: holding an arrow key must not spawn a git process per row
|
# highlight-to-preview delay: holding an arrow key must not spawn a git process per row
|
||||||
PREVIEW_DELAY_MS = 60
|
PREVIEW_DELAY_MS = 60
|
||||||
|
|
||||||
@@ -118,6 +123,24 @@ def file_items(files):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def commit_items(commits):
|
||||||
|
"""Quick-panel rows: subject on the left, its totals and the hash right-aligned"""
|
||||||
|
items = []
|
||||||
|
for c in commits:
|
||||||
|
annotation = c.short
|
||||||
|
if c.files is not None:
|
||||||
|
# merges and empty commits have no diffstat, so they show the hash alone
|
||||||
|
annotation = "{} {} changed -{} +{} {}".format(
|
||||||
|
c.files, "file" if c.files == 1 else "files", c.deletions, c.insertions, c.short
|
||||||
|
)
|
||||||
|
subject = c.subject or "(no subject)"
|
||||||
|
budget = max(COMMIT_ROW_WIDTH - len(annotation), MIN_SUBJECT_WIDTH)
|
||||||
|
if len(subject) > budget:
|
||||||
|
subject = subject[: budget - 2] + ".."
|
||||||
|
items.append(sublime.QuickPanelItem(subject, "{} · {}".format(c.author, c.date), annotation, COMMIT_KIND))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
def show_files_panel(window, view, st, files, kept, on_cancel=None, placeholder="Changed files"):
|
def show_files_panel(window, view, st, files, kept, on_cancel=None, placeholder="Changed files"):
|
||||||
"""Quick panel over `files`; highlighting previews, Enter keeps, Esc restores `kept` (or runs on_cancel)"""
|
"""Quick panel over `files`; highlighting previews, Enter keeps, Esc restores `kept` (or runs on_cancel)"""
|
||||||
st.files = files
|
st.files = files
|
||||||
@@ -229,9 +252,7 @@ class VdiffCommitsCommand(sublime_plugin.WindowCommand):
|
|||||||
run_async(lambda: git.recent_commits(st.repo, count), done)
|
run_async(lambda: git.recent_commits(st.repo, count), done)
|
||||||
|
|
||||||
def show_commits(self, view, st, commits, selected):
|
def show_commits(self, view, st, commits, selected):
|
||||||
items = [
|
items = commit_items(commits)
|
||||||
sublime.QuickPanelItem(c.subject or "(no subject)", "{} · {}".format(c.author, c.date), c.short, COMMIT_KIND) for c in commits
|
|
||||||
]
|
|
||||||
for i, c in enumerate(commits):
|
for i, c in enumerate(commits):
|
||||||
if c.sha == st.sha:
|
if c.sha == st.sha:
|
||||||
selected = i
|
selected = i
|
||||||
|
|||||||
17
vdiff/git.py
17
vdiff/git.py
@@ -1,11 +1,13 @@
|
|||||||
""" Git access: every call is one subprocess; results are plain tuples, no sublime imports (testable) """
|
""" Git access: every call is one subprocess; results are plain tuples, no sublime imports (testable) """
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
# path, one-letter status (M A D R ?), staged flag, numstat counts (None for binary), rename source
|
# path, one-letter status (M A D R ?), staged flag, numstat counts (None for binary), rename source
|
||||||
FileChange = namedtuple("FileChange", "path status staged insertions deletions old_path")
|
FileChange = namedtuple("FileChange", "path status staged insertions deletions old_path")
|
||||||
Commit = namedtuple("Commit", "sha short author date subject")
|
# shortstat totals; all three are None when git prints no diffstat (merges, empty commits)
|
||||||
|
Commit = namedtuple("Commit", "sha short author date subject files insertions deletions")
|
||||||
CommitInfo = namedtuple("CommitInfo", "sha short author date subject body")
|
CommitInfo = namedtuple("CommitInfo", "sha short author date subject body")
|
||||||
|
|
||||||
# hash of the empty tree: the diff base for a repository without commits
|
# hash of the empty tree: the diff base for a repository without commits
|
||||||
@@ -31,6 +33,9 @@ STATUS_NAMES = {
|
|||||||
_INFO_FORMAT = "%H%x1f%h%x1f%an%x1f%ad%x1f%s%x1f%b"
|
_INFO_FORMAT = "%H%x1f%h%x1f%an%x1f%ad%x1f%s%x1f%b"
|
||||||
_DATE_FORMAT = "--date=format:%Y-%m-%d %H:%M"
|
_DATE_FORMAT = "--date=format:%Y-%m-%d %H:%M"
|
||||||
|
|
||||||
|
# " 3 files changed, 10 insertions(+), 2 deletions(-)" - either count clause is dropped when zero
|
||||||
|
_SHORTSTAT = re.compile(r"\s*(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?")
|
||||||
|
|
||||||
|
|
||||||
class GitError(Exception):
|
class GitError(Exception):
|
||||||
"""git exited with an unexpected code; str() is the first stderr line, `stderr` the whole of it"""
|
"""git exited with an unexpected code; str() is the first stderr line, `stderr` the whole of it"""
|
||||||
@@ -178,12 +183,18 @@ def recent_commits(repo, count):
|
|||||||
"""Last `count` commits, newest first; empty list without commits"""
|
"""Last `count` commits, newest first; empty list without commits"""
|
||||||
if not has_head(repo):
|
if not has_head(repo):
|
||||||
return []
|
return []
|
||||||
text = run(repo, ["log", "-n", str(count), "--format=%H%x1f%h%x1f%an%x1f%ar%x1f%s"])
|
# --shortstat rides along on the same call: one subprocess, totals for free
|
||||||
|
text = run(repo, ["log", "-n", str(count), "--format=%H%x1f%h%x1f%an%x1f%ar%x1f%s", "--shortstat"])
|
||||||
commits = []
|
commits = []
|
||||||
for line in text.split("\n"):
|
for line in text.split("\n"):
|
||||||
parts = line.split("\x1f")
|
parts = line.split("\x1f")
|
||||||
if len(parts) == 5:
|
if len(parts) == 5:
|
||||||
commits.append(Commit(*parts))
|
commits.append(Commit(*parts, files=None, insertions=None, deletions=None))
|
||||||
|
continue
|
||||||
|
stat = commits and commits[-1].files is None and _SHORTSTAT.match(line)
|
||||||
|
if stat:
|
||||||
|
files, ins, dele = (int(g or 0) for g in stat.groups())
|
||||||
|
commits[-1] = commits[-1]._replace(files=files, insertions=ins, deletions=dele)
|
||||||
return commits
|
return commits
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user