Last active
January 19, 2026 13:16
-
-
Save the-solipsist/38fa498c0cdc9c858ed104cbb10a0338 to your computer and use it in GitHub Desktop.
hledger-jj converted from ysh to bash by Gemini 3.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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