Skip to content

Instantly share code, notes, and snippets.

@the-solipsist
Last active January 19, 2026 13:16
Show Gist options
  • Select an option

  • Save the-solipsist/38fa498c0cdc9c858ed104cbb10a0338 to your computer and use it in GitHub Desktop.

Select an option

Save the-solipsist/38fa498c0cdc9c858ed104cbb10a0338 to your computer and use it in GitHub Desktop.
hledger-jj converted from ysh to bash by Gemini 3.0
#!/usr/bin/env bash
# -*- sh -*-
# hledger-jj v2.0
#
# A bash wrapper for using Jujutsu (jj) with hledger journals.
#
# Credits & Version History:
# v2.0 - Added -f support, improved architecture, and fixed exit codes by Gemini 3.0 Pro.
# v1.0 - Ported from the original YSH script to Bash by Gemini 2.0.
# Original Idea & YSH Script by Simon Michael.
# --- 1. Argument Parsing ---
# We parse args first to allow -f to override the environment variable.
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
-f|--file)
LEDGER_FILE="$2"
shift 2 # Shift past the flag and the value
;;
-h|--help)
# Handle help early so we don't fail on missing LEDGER_FILE
SHOW_HELP=true
shift
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
# Restore positional parameters (the commands like 'commit', 'status')
set -- "${POSITIONAL_ARGS[@]}"
# --- 2. Help Message ---
HELP=$(cat <<'EOF'
-------------------------------------------------------------------------------
hledger-jj [OPTIONS] [COMMAND [OPTS]] - easy version control for hledger journals
An easy CLI for keeping your data in version control, using jj and a git repo.
A repo will be created if needed, in the journal file's directory.
You can run this tool from any directory. Commands may be abbreviated.
Options are passed to jj; you may need to write -- first.
Global Options:
-f, --file FILE Use FILE as the journal (overrides LEDGER_FILE env var)
Commands:
help Show help message
status [OPTS] Show status of journal files
diff [OPTS] Show unrecorded changes in journal files
commit [MSG] Record changes to journal files
log [OPTS] List recorded changes to journal files
Examples:
hledger-jj status
hledger-jj -f ./2024.journal diff
hledger-jj commit "Added rent transaction"
hledger-jj log -n 5 --stat
EOF
)
help() {
echo -e "$HELP"
}
# Check if help was requested via flag
if [[ "$SHOW_HELP" == "true" ]]; then
help
exit 0
fi
# --- 3. Environment & Dependency Validation ---
if [[ -z "$LEDGER_FILE" ]]; then
echo "Error: LEDGER_FILE is not set and no -f flag provided." >&2
echo "Usage: hledger-jj [-f file] command" >&2
exit 1
fi
FILE1="${LEDGER_FILE}"
DIR="$(dirname "$FILE1")"
if [[ ! -d "$DIR" ]]; then
echo "Error: Directory '$DIR' (derived from journal file) does not exist." >&2
exit 1
fi
# Check if hledger is installed
if ! command -v hledger >/dev/null 2>&1; then
echo "Error: hledger is not installed or not in PATH." >&2
exit 1
fi
# Check if jj is installed
if ! command -v jj >/dev/null 2>&1; then
echo "Error: jj (Jujutsu) is not installed or not in PATH." >&2
echo "Please install it: https://jj-vcs.github.io" >&2
exit 1
fi
# Get journal files and handle potential hledger errors
# Note: We explicitly pass -f to hledger here to ensure consistency
FILES_STR=$(hledger -f "$FILE1" files 2>/dev/null)
if [[ -z "$FILES_STR" ]]; then
echo "Error: hledger files command failed or returned no files." >&2
echo "Checked file: $FILE1" >&2
exit 1
fi
IFS=$'\n' read -r -d '' -a FILES <<< "$FILES_STR"
unset IFS
# --- 4. Function Definitions ---
setup() {
ensure_journal_repo
}
ensure_journal_repo() {
pushd "$DIR" > /dev/null || {
echo "Error: Could not change directory to '$DIR'." >&2
exit 1
}
trap 'popd > /dev/null' EXIT
if ! jj status >/dev/null 2>&1; then
if ! jj git init --colocate; then
echo "Error: Failed to initialize jj/git repo in '$DIR'." >&2
exit 1
fi
echo "Created new colocated jj/git repo in $DIR"
fi
}
status() {
pushd "$DIR" > /dev/null || { echo "Error: Could not change directory to '$DIR'." >&2; exit 1; }
trap 'popd > /dev/null' EXIT
jj status --color=always "$@" "${FILES[@]}" | grep -vE '^Untracked paths:|\?'
}
diff() {
pushd "$DIR" > /dev/null || { echo "Error: Could not change directory to '$DIR'." >&2; exit 1; }
trap 'popd > /dev/null' EXIT
jj diff "$@" "${FILES[@]}"
}
commit() {
pushd "$DIR" > /dev/null || { echo "Error: Could not change directory to '$DIR'." >&2; exit 1; }
trap 'popd > /dev/null' EXIT
if ! jj file track "${FILES[@]}"; then
echo "Error: Failed to track files using jj." >&2
exit 1
fi
# Basic check to avoid empty commits if desired, though jj handles empty commits gracefully usually
# Use $@ to pass other args like -m
msg="${2:-$(date +'%Y-%m-%d %H:%M')}"
# Note: logic slightly adjusted to handle args correctly
# If $2 is set, it's the message. If not, we generate date.
# We need to construct the jj command carefully.
if [[ -n "$2" ]]; then
# Message provided as argument
shift 2 # remove command and message from args
if ! jj commit -m "$msg" "$@" "${FILES[@]}"; then
echo "Error: jj commit command failed." >&2
exit 1
fi
else
# No message provided
shift 1 # remove command
if ! jj commit -m "$msg" "$@" "${FILES[@]}"; then
echo "Error: jj commit command failed." >&2
exit 1
fi
fi
}
log() {
pushd "$DIR" > /dev/null || { echo "Error: Could not change directory to '$DIR'." >&2; exit 1; }
trap 'popd > /dev/null' EXIT
jj log "$@" "${FILES[@]}"
}
# --- 5. Main Execution ---
if [ "$#" -lt 1 ]; then
help
exit 0
else
command="$1"
args=("${@:2}")
case "$command" in
help | -h | --help ) help ;;
status* | s | st ) setup; status "${args[@]}" ;;
diff* | d ) setup; diff "${args[@]}" ;;
commit* | c ) setup; commit "$@" ;; # Pass full $@ to handle message logic inside function
log* | l ) setup; log "${args[@]}" ;;
* )
echo "Unknown command: $command"
exit 1 # Fixed: exit instead of return
;;
esac
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment