Skip to content

Instantly share code, notes, and snippets.

@sdkks
Last active July 19, 2026 16:01
Show Gist options
  • Select an option

  • Save sdkks/0d81e593aa9268c6bc24df0a299c76ea to your computer and use it in GitHub Desktop.

Select an option

Save sdkks/0d81e593aa9268c6bc24df0a299c76ea to your computer and use it in GitHub Desktop.
git-loc alias for smart display of your git changes with color codes

Smart Git Diff with LOC Info

image

See the git diff output in a more meaningful way.

These aliases summarize line changes and show each file's current worktree size. They are useful for a quick view of whether a change is growing or shrinking the codebase and whether a changed file may be getting large.

Commands

Command Scope
git cloc Cached (staged) changes
git dloc Unstaged changes to tracked files
git uloc Untracked files, excluding files ignored by Git
git loc All three scopes, with labeled sections and a grand total

git loc prints the sections in this order:

DIFF
...

DIFF CACHED
...

UNTRACKED FILES
...

GRAND TOTAL +1 (+3/-2) GROWING

Each section ends with its own TOTAL line. GRAND TOTAL combines the additions and deletions from all three sections.

Reading the output

A text-file row has this exact shape:

<signed-net> (+adds/-deletes) file LOC:current-lines

For example:

+3 (+5/-2) src/parser.sh LOC:248
0 (+1/-1) README.md LOC:86
-12 (+0/-12) lib/old.rb LOC:0
TOTAL -9 (+6/-15) SHRINKING
  • Signed net is additions minus deletions. Positive values include a leading +, negative values include -, and zero is printed as 0.
  • +adds/-deletes preserves the underlying addition and deletion counts. A zero net can therefore still represent a replacement, such as 0 (+1/-1).
  • LOC:current-lines is the file's line count in the current worktree—not its size in the index. This is also true in git cloc, so staged change counts can appear beside a LOC value that includes later unstaged edits. A final line without a trailing newline still counts as one line.
  • GROWING, SHRINKING, and UNCHANGED describe the sign of a section or grand total's net change. UNCHANGED means the net is zero; additions and deletions may still be nonzero.

Totals sum additions and deletions, then derive the signed net and direction. They do not sum or display the current LOC values.

Special file behavior

Binary files are reported without numeric change or LOC fields:

BINARY assets/logo.bin

They do not contribute to section or grand totals. A missing or deleted file has no current regular-file content and is shown with LOC:0. Symlinks also show LOC:0; the counter deliberately does not follow their targets. Empty regular files likewise have zero current lines.

Limit the report with pathspecs

All four aliases accept optional Git pathspecs:

git dloc -- src/
git cloc -- '*.sh'
git uloc docs/
git loc -- src/ tests/

The separating -- is optional, but it is useful when a path could be mistaken for an option. Relative pathspecs retain the calling directory's semantics. For example, from src/, git dloc parser.sh selects src/parser.sh.

Untracked-file selection uses Git's standard exclusions, so git uloc and the untracked section of git loc omit ignored files.

Colors

Colors are enabled only when standard output is attached to a terminal or pseudo-terminal (TTY/PTY). Piped and redirected output contains no ANSI escape sequences, making it safe for scripts and text files.

Element Color
Positive net and additions Green
Negative net and deletions Red
Zero net Neutral
Unstaged tracked filename (DIFF) Amber
Cached filename (DIFF CACHED) Green
Untracked filename Red
LOC:0 Red
LOC:1LOC:499 Green
LOC:500LOC:999 Amber
LOC:1000 and above Red

Filename colors make the three change sources distinguishable at a glance: amber for unstaged work, green for staged work, and red for files Git is not yet tracking.

The LOC palette is an attention heuristic:

  • Zero current lines deserves attention because the file may be intentionally empty, newly scaffolded, deleted, missing, or a symlink.
  • A file with 1–499 lines is usually small enough to navigate and reason about comfortably.
  • A file with 500–999 lines may be accumulating complexity and deserves caution.
  • A file with 1,000 lines or more is a strong complexity or refactoring signal.

These colors are prompts for review, not quality verdicts. File purpose, generated content, language, and project conventions still matter.

Piping and automation

Redirect a plain report to a file:

git loc >loc-report.txt

Or inspect only totals without having to strip color codes:

git loc | grep -E '^(TOTAL|GRAND TOTAL)'

Because the pipe makes stdout non-TTY, both examples produce plain text with no ANSI color sequences.

Implementation references

  • common.conf defines the cloc, dloc, uloc, and loc aliases. Append this to your global git-config or use the include directive.
  • git_loc.sh selects files, counts changes and current worktree lines, formats totals, and applies terminal colors.
  • test_git_loc.sh covers empty sections, net directions, combined totals, pathspecs, special filenames, binary files, LOC thresholds, unterminated final lines, symlinks, and TTY versus redirected output.

LICENSE

MIT

[alias]
# Count staged additions and deletions by file.
cloc = "!sh -c 'exec \"$HOME/.dotfiles/data/git/git_loc.sh\" cached \"$@\"' --"
# Count unstaged tracked additions and deletions by file.
dloc = "!sh -c 'exec \"$HOME/.dotfiles/data/git/git_loc.sh\" diff \"$@\"' --"
# Count untracked lines by file.
uloc = "!sh -c 'exec \"$HOME/.dotfiles/data/git/git_loc.sh\" untracked \"$@\"' --"
# Show unstaged, staged, and untracked line counts in labeled sections.
loc = "!sh -c 'exec \"$HOME/.dotfiles/data/git/git_loc.sh\" all \"$@\"' --"
#!/bin/sh
set -u
terminate() {
signal_status=$1
trap - 1 2 3 15
exit "$signal_status"
}
cleanup_worker() {
status=$?
trap - 0 1 2 3 15
rm -rf "$WORKER_TMP"
exit "$status"
}
worker_error() {
if [ -s "$WORKER_ERROR" ]; then
cat "$WORKER_ERROR" >&2
fi
printf '%s' 'git_loc.sh: unable to count untracked file: ' >&2
cat "$WORKER_DISPLAY" >&2
exit 255
}
untracked_worker() {
[ "$#" -eq 2 ] || exit 255
worker_root=$1
file=$2
WORKER_TMP=$(mktemp -d "$worker_root/worker.XXXXXX") || exit 255
WORKER_STAT="$WORKER_TMP/stat"
WORKER_ERROR="$WORKER_TMP/error"
WORKER_COUNTS="$WORKER_TMP/counts"
WORKER_DISPLAY="$WORKER_TMP/display"
trap cleanup_worker 0
trap 'terminate 129' 1
trap 'terminate 130' 2
trap 'terminate 131' 3
trap 'terminate 143' 15
# Produce display text separately from the lossless pathname argument.
git -c core.quotePath=true --literal-pathspecs ls-files \
--others --exclude-standard -- "$file" >"$WORKER_DISPLAY" || exit 255
awk 'END { exit(NR == 1 ? 0 : 1) }' "$WORKER_DISPLAY" || {
printf '%s\n' 'git_loc.sh: untracked file disappeared before it could be counted' >&2
exit 255
}
: >"$WORKER_ERROR"
git --no-pager diff --no-ext-diff --no-index --numstat -- \
/dev/null "$file" >"$WORKER_STAT" 2>"$WORKER_ERROR"
status=$?
case "$status" in
0)
# A valid empty file has no diff and therefore no numstat record.
if [ -s "$WORKER_STAT" ] || [ -s "$WORKER_ERROR" ]; then
worker_error
fi
additions=0
deletions=0
;;
1)
# Status 1 is valid only when Git produced one well-formed record.
if [ -s "$WORKER_ERROR" ] || ! awk -F '\t' '
NR == 1 && NF >= 3 &&
(($1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/) ||
($1 == "-" && $2 == "-")) {
print $1 "\t" $2
valid = 1
next
}
{ valid = 0 }
END { exit(NR == 1 && valid ? 0 : 1) }
' "$WORKER_STAT" >"$WORKER_COUNTS"; then
worker_error
fi
tab=$(printf '\t')
counts=$(cat "$WORKER_COUNTS")
additions=${counts%%"$tab"*}
deletions=${counts#*"$tab"}
;;
*)
worker_error
;;
esac
printf '%s\t%s\t' "$additions" "$deletions"
cat "$WORKER_DISPLAY"
}
loc_worker() {
[ "$#" -eq 2 ] || exit 255
loc_root=$1
loc_file=$2
loc_path=$loc_root/$loc_file
# A missing/deleted file and a symlink both have no current regular-file
# lines. In particular, do not let the line counter follow a symlink.
if [ -L "$loc_path" ] || [ ! -e "$loc_path" ] || [ ! -f "$loc_path" ]; then
printf '0\n'
return
fi
# awk counts a final unterminated record, unlike wc -l.
awk 'END { print NR }' "$loc_path"
}
if [ "${1-}" = --untracked-worker ]; then
shift
untracked_worker "$@"
exit
fi
if [ "${1-}" = --loc-worker ]; then
shift
loc_worker "$@"
exit
fi
usage() {
echo "usage: git_loc.sh {diff|cached|untracked|all} [pathspec ...]" >&2
exit 2
}
MODE=${1-}
[ -n "$MODE" ] || usage
shift
# A leading -- is optional for callers that prefer to separate pathspecs.
if [ "${1-}" = "--" ]; then
shift
fi
SELF=$(command -v "$0") || exit 1
case "$SELF" in
/*) ;;
*) SELF=$PWD/${SELF#./} ;;
esac
ROOT=$(git rev-parse --show-toplevel) || exit 1
# With no pathspec, use repository scope. Explicit relative pathspecs retain the
# caller's directory semantics even though Git shell aliases start at the root.
if [ "$#" -eq 0 ]; then
cd "$ROOT" || exit 1
elif [ -n "${GIT_PREFIX-}" ]; then
cd "$ROOT/${GIT_PREFIX%/}" || exit 1
fi
TMP_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/git-loc.XXXXXX") || exit 1
NUMSTAT="$TMP_ROOT/numstat"
FILE_LIST="$TMP_ROOT/files"
LOC_FILE_LIST="$TMP_ROOT/loc-files"
LOC_COUNTS="$TMP_ROOT/loc-counts"
FORMATTED="$TMP_ROOT/formatted"
GRAND_NUMSTAT="$TMP_ROOT/grand-numstat"
COLLECT_GRAND=0
if [ -t 1 ]; then
COLOR_GREEN=$(printf '\033[32m')
COLOR_RED=$(printf '\033[31m')
COLOR_AMBER=$(printf '\033[38;5;214m')
COLOR_RESET=$(printf '\033[0m')
else
COLOR_GREEN=
COLOR_RED=
COLOR_AMBER=
COLOR_RESET=
fi
cleanup() {
status=$?
trap - 0 1 2 3 15
rm -rf "$TMP_ROOT"
exit "$status"
}
trap cleanup 0
trap 'terminate 129' 1
trap 'terminate 130' 2
trap 'terminate 131' 3
trap 'terminate 143' 15
count_current_locs() {
: >"$LOC_COUNTS" || return
if [ -s "$LOC_FILE_LIST" ]; then
xargs -0 -n 1 "$SELF" --loc-worker "$ROOT" \
<"$LOC_FILE_LIST" >"$LOC_COUNTS" || return
fi
}
format_numstat() {
if ! awk -F '\t' \
-v green="$COLOR_GREEN" \
-v red="$COLOR_RED" \
-v amber="$COLOR_AMBER" \
-v filename_color="$2" \
-v loc_file="$LOC_COUNTS" \
-v reset="$COLOR_RESET" '
function set_net_style(net) {
if (net > 0) {
signed_net = "+" net
net_color = green
direction = "GROWING"
} else if (net < 0) {
signed_net = net
net_color = red
direction = "SHRINKING"
} else {
signed_net = "0"
net_color = ""
direction = "UNCHANGED"
}
}
{
if ((getline loc < loc_file) != 1 || loc !~ /^[0-9]+$/) {
invalid_locs = 1
next
}
additions = $1
deletions = $2
file = $0
sub(/^[^\t]*\t[^\t]*\t/, "", file)
if (additions == "-" || deletions == "-") {
printf "BINARY %s%s%s\n", filename_color, file, reset
next
}
net = additions - deletions
set_net_style(net)
if (loc == 0) {
loc_color = red
} else if (loc < 500) {
loc_color = green
} else if (loc < 1000) {
loc_color = amber
} else {
loc_color = red
}
total_additions += additions
total_deletions += deletions
printf "%s%s%s (%s+%d%s/%s-%d%s) %s%s%s LOC:%s%d%s\n",
net_color, signed_net, (net_color == "" ? "" : reset),
green, additions, reset,
red, deletions, reset,
filename_color, file, reset,
loc_color, loc, reset
}
END {
extra_locs = getline extra < loc_file
if (invalid_locs || extra_locs != 0) {
exit 1
}
net = total_additions - total_deletions
set_net_style(net)
printf "TOTAL %s%s%s (%s+%d%s/%s-%d%s) %s\n",
net_color, signed_net, (net_color == "" ? "" : reset),
green, total_additions + 0, reset,
red, total_deletions + 0, reset,
direction
}
' "$1" >"$FORMATTED"; then
return 1
fi
cat "$FORMATTED"
}
format_grand_total() {
awk -F '\t' \
-v green="$COLOR_GREEN" \
-v red="$COLOR_RED" \
-v reset="$COLOR_RESET" '
$1 != "-" && $2 != "-" {
additions += $1
deletions += $2
}
END {
net = additions - deletions
if (net > 0) {
direction = "GROWING"
signed_net = "+" net
net_color = green
} else if (net < 0) {
direction = "SHRINKING"
signed_net = net
net_color = red
} else {
direction = "UNCHANGED"
signed_net = "0"
net_color = ""
}
printf "GRAND TOTAL %s%s%s (%s+%d%s/%s-%d%s) %s\n",
net_color, signed_net, (net_color == "" ? "" : reset),
green, additions + 0, reset,
red, deletions + 0, reset,
direction
}
' "$GRAND_NUMSTAT"
}
collect_grand_numstat() {
if [ "$COLLECT_GRAND" -eq 1 ]; then
cat "$NUMSTAT" >>"$GRAND_NUMSTAT"
fi
}
report_diff() {
diff_kind=$1
shift
if [ "$diff_kind" = cached ]; then
git --no-pager diff --cached --numstat -- "$@" >"$NUMSTAT" || return
git --no-pager diff --cached --name-only -z -- "$@" >"$LOC_FILE_LIST" || return
else
git --no-pager diff --numstat -- "$@" >"$NUMSTAT" || return
git --no-pager diff --name-only -z -- "$@" >"$LOC_FILE_LIST" || return
fi
count_current_locs || return
if [ "$diff_kind" = cached ]; then
filename_color=$COLOR_GREEN
else
filename_color=$COLOR_AMBER
fi
format_numstat "$NUMSTAT" "$filename_color" || return
collect_grand_numstat
}
report_untracked() {
: >"$NUMSTAT"
git ls-files -z --others --exclude-standard -- "$@" >"$FILE_LIST" || return
# Keep caller-relative names for the diff worker: it runs from the caller's
# directory and also produces the displayed pathname. Count LOC from a
# separate root-relative list because loc_worker joins names to ROOT.
git ls-files --full-name -z --others --exclude-standard -- "$@" \
>"$LOC_FILE_LIST" || return
# macOS /usr/bin/xargs supports NUL records and transports each pathname as
# one opaque argument to the internal worker without shell interpolation.
if [ -s "$FILE_LIST" ]; then
xargs -0 -n 1 "$SELF" --untracked-worker "$TMP_ROOT" \
<"$FILE_LIST" >"$NUMSTAT" || return
fi
count_current_locs || return
format_numstat "$NUMSTAT" "$COLOR_RED" || return
collect_grand_numstat
}
case "$MODE" in
diff)
report_diff diff "$@"
;;
cached)
report_diff cached "$@"
;;
untracked)
report_untracked "$@"
;;
all)
COLLECT_GRAND=1
: >"$GRAND_NUMSTAT" || exit
printf '%s\n' 'DIFF'
report_diff diff "$@" || exit
printf '\n%s\n' 'DIFF CACHED'
report_diff cached "$@" || exit
printf '\n%s\n' 'UNTRACKED FILES'
report_untracked "$@" || exit
printf '\n'
format_grand_total
;;
*)
usage
;;
esac
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
GIT_LOC="$SCRIPT_DIR/git_loc.sh"
TEST_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/git-loc-test.XXXXXX")
TEST_COUNT=0
cleanup() {
status=$?
trap - 0 1 2 3 15
rm -rf "$TEST_ROOT"
exit "$status"
}
trap cleanup 0
trap 'exit 129' 1
trap 'exit 130' 2
trap 'exit 131' 3
trap 'exit 143' 15
fail() {
printf 'not ok %d - %s\n' "$TEST_COUNT" "$1" >&2
if [ "$#" -ge 2 ]; then
printf '%s\n' "$2" >&2
fi
exit 1
}
begin_test() {
TEST_COUNT=$((TEST_COUNT + 1))
TEST_NAME=$1
}
pass() {
printf 'ok %d - %s\n' "$TEST_COUNT" "$TEST_NAME"
}
assert_equal() {
ASSERT_EXPECTED=$1
ASSERT_ACTUAL=$2
ASSERT_MESSAGE=$3
if [ "$ASSERT_ACTUAL" != "$ASSERT_EXPECTED" ]; then
fail "$TEST_NAME: $ASSERT_MESSAGE" "expected:
$ASSERT_EXPECTED
actual:
$ASSERT_ACTUAL"
fi
}
assert_line() {
ASSERT_LINE=$1
ASSERT_OUTPUT=$2
ASSERT_MESSAGE=$3
printf '%s\n' "$ASSERT_OUTPUT" | grep -F -x -- "$ASSERT_LINE" >/dev/null ||
fail "$TEST_NAME: $ASSERT_MESSAGE" "missing line: $ASSERT_LINE
output:
$ASSERT_OUTPUT"
}
assert_contains() {
ASSERT_FRAGMENT=$1
ASSERT_OUTPUT=$2
ASSERT_MESSAGE=$3
case "$ASSERT_OUTPUT" in
*"$ASSERT_FRAGMENT"*) ;;
*)
fail "$TEST_NAME: $ASSERT_MESSAGE" "missing fragment: $ASSERT_FRAGMENT
output:
$ASSERT_OUTPUT"
;;
esac
}
assert_no_ansi() {
ASSERT_OUTPUT=$1
ASSERT_MESSAGE=$2
ASSERT_ESCAPE=$(printf '\033')
case "$ASSERT_OUTPUT" in
*"$ASSERT_ESCAPE"*) fail "$TEST_NAME: $ASSERT_MESSAGE" ;;
esac
}
run_tty_all() {
TTY_REPO=$1
TTY_CAPTURE=$2
case $(uname -s) in
Darwin)
(cd "$TTY_REPO" && GIT_LOC_UNDER_TEST=$GIT_LOC \
script -q "$TTY_CAPTURE" sh -c \
'exec "$GIT_LOC_UNDER_TEST" all' >/dev/null)
;;
*)
(cd "$TTY_REPO" && GIT_LOC_UNDER_TEST=$GIT_LOC \
script -q -e -c \
'exec "$GIT_LOC_UNDER_TEST" all' "$TTY_CAPTURE" >/dev/null)
;;
esac
}
new_repo() {
repo=$TEST_ROOT/repo-$TEST_COUNT
mkdir "$repo"
git -C "$repo" init -q
git -C "$repo" config user.name 'git-loc test'
git -C "$repo" config user.email 'git-loc@example.invalid'
printf '%s\n' "$repo"
}
commit_all() {
git -C "$1" add -A
git -C "$1" commit -q -m baseline
}
make_lines() {
MAKE_LINE_COUNT=$1
MAKE_LINE_FILE=$2
awk -v count="$MAKE_LINE_COUNT" 'BEGIN {
for (line = 1; line <= count; line++) print "line " line
}' >"$MAKE_LINE_FILE"
}
replace_first_line() {
REPLACE_FILE=$1
REPLACE_TMP=$REPLACE_FILE.tmp
{
printf 'changed\n'
awk 'NR > 1' "$REPLACE_FILE"
} >"$REPLACE_TMP"
mv "$REPLACE_TMP" "$REPLACE_FILE"
}
printf '1..9\n'
begin_test 'empty repository and empty sections'
repo=$(new_repo)
actual=$(cd "$repo" && "$GIT_LOC" all)
expected='DIFF
TOTAL 0 (+0/-0) UNCHANGED
DIFF CACHED
TOTAL 0 (+0/-0) UNCHANGED
UNTRACKED FILES
TOTAL 0 (+0/-0) UNCHANGED
GRAND TOTAL 0 (+0/-0) UNCHANGED'
assert_equal "$expected" "$actual" 'empty all-mode output differs'
assert_equal 'TOTAL 0 (+0/-0) UNCHANGED' "$(cd "$repo" && "$GIT_LOC" diff)" 'empty diff output differs'
assert_equal 'TOTAL 0 (+0/-0) UNCHANGED' "$(cd "$repo" && "$GIT_LOC" cached)" 'empty cached output differs'
assert_equal 'TOTAL 0 (+0/-0) UNCHANGED' "$(cd "$repo" && "$GIT_LOC" untracked)" 'empty untracked output differs'
pass
begin_test 'growing working tree'
repo=$(new_repo)
printf 'one\ntwo\nthree\n' >"$repo/growth.txt"
actual=$(cd "$repo" && "$GIT_LOC" all)
assert_line '+3 (+3/-0) growth.txt LOC:3' "$actual" 'untracked file counts differ'
assert_line 'TOTAL +3 (+3/-0) GROWING' "$actual" 'untracked total differs'
assert_line 'GRAND TOTAL +3 (+3/-0) GROWING' "$actual" 'growth grand total differs'
pass
begin_test 'shrinking working tree'
repo=$(new_repo)
printf 'one\ntwo\nthree\n' >"$repo/shrink.txt"
commit_all "$repo"
: >"$repo/shrink.txt"
actual=$(cd "$repo" && "$GIT_LOC" all)
assert_line '-3 (+0/-3) shrink.txt LOC:0' "$actual" 'deletion or current LOC count differs'
assert_line 'TOTAL -3 (+0/-3) SHRINKING' "$actual" 'diff total differs'
assert_line 'GRAND TOTAL -3 (+0/-3) SHRINKING' "$actual" 'shrinkage grand total differs'
pass
begin_test 'unchanged net working tree'
repo=$(new_repo)
printf 'before\n' >"$repo/unchanged.txt"
commit_all "$repo"
printf 'after\n' >"$repo/unchanged.txt"
actual=$(cd "$repo" && "$GIT_LOC" all)
assert_line '0 (+1/-1) unchanged.txt LOC:1' "$actual" 'replacement or current LOC count differs'
assert_line 'GRAND TOTAL 0 (+1/-1) UNCHANGED' "$actual" 'unchanged grand total differs'
pass
begin_test 'combined unstaged, staged, and untracked totals'
repo=$(new_repo)
printf 'u1\nu2\nu3\n' >"$repo/unstaged.txt"
printf 's1\ns2\n' >"$repo/staged.txt"
commit_all "$repo"
printf 'u1\n' >"$repo/unstaged.txt"
printf 's1\nnew\ns2\n' >"$repo/staged.txt"
git -C "$repo" add staged.txt
printf 'n1\nn2\n' >"$repo/untracked.txt"
actual=$(cd "$repo" && "$GIT_LOC" all)
assert_no_ansi "$actual" 'non-TTY output contained ANSI escapes'
assert_line '-2 (+0/-2) unstaged.txt LOC:1' "$actual" 'unstaged counts differ'
assert_line '+1 (+1/-0) staged.txt LOC:3' "$actual" 'staged counts differ'
assert_line '+2 (+2/-0) untracked.txt LOC:2' "$actual" 'untracked counts differ'
assert_line 'GRAND TOTAL +1 (+3/-2) GROWING' "$actual" 'combined grand total differs'
pass
begin_test 'special filenames, empty files, and binary exclusion'
repo=$(new_repo)
printf 'space\n' >"$repo/a file.txt"
printf 'dash\n' >"$repo/-leading.txt"
printf 'tab\n' >"$repo/tab name.txt"
printf 'newline\n' >"$repo/newline
name.txt"
: >"$repo/empty file.txt"
printf '\000\001\002' >"$repo/binary file.bin"
actual=$(cd "$repo" && "$GIT_LOC" untracked)
assert_line 'TOTAL +4 (+4/-0) GROWING' "$actual" 'special-name total differs'
binary_count=$(printf '%s\n' "$actual" | grep -c '^BINARY ' || :)
assert_equal '1' "$binary_count" 'binary file was not reported exactly once'
numeric_count=$(printf '%s\n' "$actual" | grep -c '^+1 (+1/-0) .* LOC:1$' || :)
assert_equal '4' "$numeric_count" 'special text files or LOC counts differ'
zero_count=$(printf '%s\n' "$actual" | grep -c '^0 (+0/-0) .* LOC:0$' || :)
assert_equal '1' "$zero_count" 'empty file was not counted as zero'
all_output=$(cd "$repo" && "$GIT_LOC" all)
assert_line 'GRAND TOTAL +4 (+4/-0) GROWING' "$all_output" \
'binary or empty file incorrectly affected the grand total'
pass
begin_test 'current LOC thresholds, final non-newline, and symlink safety'
repo=$(new_repo)
make_lines 499 "$repo/loc499.txt"
make_lines 500 "$repo/loc500.txt"
make_lines 999 "$repo/loc999.txt"
make_lines 1000 "$repo/loc1000.txt"
printf 'first\nsecond' >"$repo/final-no-newline.txt"
ln -s loc1000.txt "$repo/linked.txt"
actual=$(cd "$repo" && "$GIT_LOC" untracked)
assert_no_ansi "$actual" 'plain threshold output contained ANSI escapes'
assert_line '+499 (+499/-0) loc499.txt LOC:499' "$actual" 'LOC 499 output differs'
assert_line '+500 (+500/-0) loc500.txt LOC:500' "$actual" 'LOC 500 output differs'
assert_line '+999 (+999/-0) loc999.txt LOC:999' "$actual" 'LOC 999 output differs'
assert_line '+1000 (+1000/-0) loc1000.txt LOC:1000' "$actual" 'LOC 1000 output differs'
assert_line '+2 (+2/-0) final-no-newline.txt LOC:2' "$actual" \
'unterminated final line was not counted'
assert_line '+1 (+1/-0) linked.txt LOC:0' "$actual" \
'symlink target was followed while counting LOC'
pass
begin_test 'caller-relative pathspec and invalid mode failure'
repo=$(new_repo)
mkdir "$repo/sub"
printf 'old\n' >"$repo/sub/tracked.txt"
printf 'outside\n' >"$repo/outside.txt"
commit_all "$repo"
printf 'new\n' >"$repo/sub/tracked.txt"
printf 'changed\n' >"$repo/outside.txt"
printf 'first\nsecond' >"$repo/sub/untracked.txt"
printf 'outside untracked\n' >"$repo/outside-untracked.txt"
actual=$(cd "$repo" && GIT_PREFIX='sub/' "$GIT_LOC" diff tracked.txt)
expected='0 (+1/-1) sub/tracked.txt LOC:1
TOTAL 0 (+1/-1) UNCHANGED'
assert_equal "$expected" "$actual" 'caller-relative pathspec selected the wrong tracked files'
actual=$(cd "$repo" && GIT_PREFIX='sub/' "$GIT_LOC" untracked -- untracked.txt)
expected='+2 (+2/-0) untracked.txt LOC:2
TOTAL +2 (+2/-0) GROWING'
assert_equal "$expected" "$actual" \
'caller-relative untracked pathspec used the wrong LOC path or selected extra files'
actual=$(cd "$repo" && GIT_PREFIX='sub/' "$GIT_LOC" all -- untracked.txt)
assert_line '+2 (+2/-0) untracked.txt LOC:2' "$actual" \
'all mode used the wrong LOC path for caller-relative untracked selection'
assert_line 'GRAND TOTAL +2 (+2/-0) GROWING' "$actual" \
'all mode caller-relative grand total differs'
if (cd "$repo" && "$GIT_LOC" invalid >/dev/null 2>&1); then
fail "$TEST_NAME: invalid mode unexpectedly succeeded"
fi
pass
begin_test 'TTY colors for nets, splits, filenames, and LOC thresholds'
if ! command -v script >/dev/null 2>&1; then
printf 'ok %d - %s # SKIP script utility unavailable\n' "$TEST_COUNT" "$TEST_NAME"
else
repo=$(new_repo)
make_lines 499 "$repo/loc499.txt"
make_lines 500 "$repo/loc500.txt"
make_lines 999 "$repo/loc999.txt"
make_lines 1000 "$repo/loc1000.txt"
printf 's1\ns2\n' >"$repo/staged.txt"
commit_all "$repo"
replace_first_line "$repo/loc499.txt"
replace_first_line "$repo/loc500.txt"
replace_first_line "$repo/loc999.txt"
replace_first_line "$repo/loc1000.txt"
printf 's1\n' >"$repo/staged.txt"
git -C "$repo" add staged.txt
printf 'n1\nn2\n' >"$repo/untracked.txt"
: >"$repo/empty.txt"
printf '\000\001\002' >"$repo/binary file.bin"
tty_capture=$TEST_ROOT/tty-$TEST_COUNT
run_tty_all "$repo" "$tty_capture"
actual=$(tr -d '\r' <"$tty_capture")
escape=$(printf '\033')
green=${escape}'[32m'
red=${escape}'[31m'
amber=${escape}'[38;5;214m'
reset=${escape}'[0m'
assert_contains \
"0 (${green}+1${reset}/${red}-1${reset}) ${amber}loc499.txt${reset} LOC:${green}499${reset}" \
"$actual" 'LOC 499 color or zero net differs'
assert_contains \
"0 (${green}+1${reset}/${red}-1${reset}) ${amber}loc500.txt${reset} LOC:${amber}500${reset}" \
"$actual" 'LOC 500 color differs'
assert_contains \
"0 (${green}+1${reset}/${red}-1${reset}) ${amber}loc999.txt${reset} LOC:${amber}999${reset}" \
"$actual" 'LOC 999 color differs'
assert_contains \
"0 (${green}+1${reset}/${red}-1${reset}) ${amber}loc1000.txt${reset} LOC:${red}1000${reset}" \
"$actual" 'LOC 1000 color differs'
assert_contains \
"${red}-1${reset} (${green}+0${reset}/${red}-1${reset}) ${green}staged.txt${reset} LOC:${green}1${reset}" \
"$actual" 'negative net or cached filename color differs'
assert_contains \
"${green}+2${reset} (${green}+2${reset}/${red}-0${reset}) ${red}untracked.txt${reset} LOC:${green}2${reset}" \
"$actual" 'positive net or untracked filename color differs'
assert_contains \
"0 (${green}+0${reset}/${red}-0${reset}) ${red}empty.txt${reset} LOC:${red}0${reset}" \
"$actual" 'empty-file LOC zero was not red'
assert_contains "BINARY ${red}binary file.bin${reset}" "$actual" \
'binary filename color differs'
assert_contains \
"TOTAL 0 (${green}+4${reset}/${red}-4${reset}) UNCHANGED" \
"$actual" 'section total component colors differ'
assert_contains \
"GRAND TOTAL ${green}+1${reset} (${green}+6${reset}/${red}-5${reset}) GROWING" \
"$actual" 'grand total colors differ'
plain_capture=$TEST_ROOT/plain-$TEST_COUNT
(cd "$repo" && "$GIT_LOC" all >"$plain_capture")
plain_output=$(cat "$plain_capture")
assert_no_ansi "$plain_output" 'redirected output contained ANSI escapes'
assert_line '0 (+1/-1) loc500.txt LOC:500' "$plain_output" \
'redirected plain file output differs'
assert_line 'GRAND TOTAL +1 (+6/-5) GROWING' "$plain_output" \
'redirected plain grand total differs'
pass
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment