From b4f8e0eae27ccb7545019d4e529c6a86fe22a6cd Mon Sep 17 00:00:00 2001 From: Connor Olding Date: Tue, 5 Oct 2021 18:15:27 -0700 Subject: [PATCH] add `glug` utility for quickly diffing git logs (WIP) --- sh/glug | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 sh/glug diff --git a/sh/glug b/sh/glug new file mode 100644 index 0000000..633ed34 --- /dev/null +++ b/sh/glug @@ -0,0 +1,311 @@ +#!/usr/bin/env dash +# YES_ZSH +# YES_BASH +# YES_DASH +# YES_ASH +# fuck termcap, all my homies hate termcap. + +glug() ( # note the subshell syntax. this allows us to abuse globals like crazy. + [ $# = 0 ] || { printf '%s does not take arguments.\n' glug >&2; return 2; } + + _here="$PWD" + repo="$PWD" + i=0 + while ! [ -d .git ] 2>/dev/null; do + # no one has git repositories with directories deeper than 10 levels, right? + [ $((i+=1)) -lt 10 ] || { printf '%s: failed to find a .git directory\n' glug >&2; return 2; } + cd .. + repo="$PWD" + done + cd "$_here" + + if [ -z "$ZSH_VERSION" ]; then + # TODO: is `stty size` more portable? + COLUMNS="${COLUMNS:-$(tput cols)}" + LINES="${LINES:-$(tput lines)}" + fi + + if [ -n "$ZSH_VERSION" ] || [ -n "$BASH_VERSION" ]; then + esc=$'\e' + nl=$'\n' + cr=$'\r' + else + esc="$(printf '\033')" + # shells strip trailing newlines from captures, so work around that: + nl="$(printf '\nx')"; nl="${nl%x}" + cr="$(printf '\rx')"; cr="${cr%x}" + fi + + limit=200 # maximum number of git commits to look back on + choices=0 + selection=1 + + alt=0 + ret=0 + err= + + alt_on() { + printf '\033[?1049h' # alt screen on (smcup) + alt=1 + printf '\033[?1h\033=' # no idea but less does it so it must be important (smkx) + printf '\r' # move cursor to start of line (carriage return) + printf '\033[H' # move cursor to first line + } + + alt_off() { + printf '\r' # move cursor to start of line (carriage return) + #printf '\033[K' # erase everything before the cursor (el) + printf '\033[?1l\033>' # no idea but less does it so it must be important (rmkx) + printf '\033[?1049l' # alt screen off (rmcup) + alt=0 + } + + glug_enter() { + [ $alt = 1 ] || alt_on + } + + glug_exit() { + [ $alt = 0 ] || alt_off + [ -z "$err" ] || printf '%s\n' "$err" >&2 + exit $ret + } + + die() { + ret=$? + err="$@" + [ $? = 0 ] && exit 1 || exit $? + } + + read_byte() { + # NOTE: run this function in a subshell, else everything will blow up. + old="$(stty -g)" + trap 'stty "$old"' INT EXIT + stty -icanon -echo + dd ibs=1 count=1 2>/dev/null + echo _ # append an underscore so that newlines are preserved in captures + } \033[m \033[33m%s\033[m ' "$commit" + printf ' \0337 \033[7m>\033[m \033[33m%s\033[m ' "$commit" + else + printf ' \033[90m%s\033[m ' "$commit" + fi + + # TODO: truncate message as necessary. + printf '%s\n' "$message" + : $((linesleft-=1)) + + if [ $i = $selection ]; then + gds="$(git diff --stat "$commit~" "$commit")" + if [ $? = 0 ]; then + : $((linesleft-=2)) # reserve two lines for present_choices + format_stats + : $((linesleft+=2)) # ... + else + printf ' \033[91merror:\033[31m failed to run `git diff --stat`\n' + : $((linesleft-=1)) + fi + fi + + [ $linesleft -ge 2 ] || break + done + if [ $linesleft -ge 1 ]; then + printf ' \033[33m(\033[97m%s\033[33m/\033[97m%s\033[33m)\033[m ' "$selection" "$choices" + : $((linesleft-=1)) + fi + eval "commit=\$choice_$selection" # set $commit to something useful + } + + handle_input() { + # TODO: handle escape codes (especially arrow keys, page up/down) + # TODO: reset button that checks LINES and COLUMNS again. + printf '\0338' # restore cursor to prompt position (rc) + input="$(read_byte)" || return + printf '\033[K' # TODO: unnecessary? + if [ "${input%_}" = "$input" ]; then + # no magic underscore, something went wrong. + return 1 + else + input="${input%_}" + fi + while [ -n "$input" ]; do + # replace newlines with spaces for my own sanity. + # of course, this makes spaces act the same as newlines. + if [ "${input#$nl}" != "$input" ]; then + input=" ${input#$nl}" + fi + + if [ "${input#[! jkq]}" != "$input" ]; then + _seg="${input%%[ jkq]*}" + elif [ "${input# }" != "$input" ]; then + _seg=' ' + alt_off + git diff "$commit~" "$commit" || exit + alt_on + elif [ "${input#j}" != "$input" ]; then + _seg=j + [ $((selection+=1)) -le $choices ] || selection=$choices + elif [ "${input#k}" != "$input" ]; then + _seg=k + [ $((selection-=1)) -ge 1 ] || selection=1 + elif [ "${input#q}" != "$input" ]; then + _seg=q + return 1 + else + break # unreachable + fi + input="${input#$_seg}" + done + } + + trap glug_exit INT EXIT && glug_enter + + # TODO: invoke git with --no-config or whatever? + logs="$(git log --oneline -n $limit 2>&1)" || die "$logs" + + parse_logs + [ $choices -gt 0 ] || { printf '%s: no commits\n' >&2; return 2; } + + present_choices + while handle_input; do + printf '\r' # move cursor to start of line (carriage return) + printf '\033[H' # move cursor to first line + printf '\033[J' # clear the rest of the screen + present_choices + done + +) 1<>/dev/tty <&1 # ensure this interacts with a terminal instead of a pipe + +[ -n "${preload+-}" ] || glug "$@"