-
-
Save izackp/dad8090f25ed0d3cd3b2a15fdcb14dac to your computer and use it in GitHub Desktop.
gist_sync.sh
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 | |
| # gist: https://gist.github.com/izackp/dad8090f25ed0d3cd3b2a15fdcb14dac | |
| # gist_sync.sh — bidirectional file <-> GitHub gist sync helper. | |
| # See `gist_sync.sh help` for full usage. | |
| # | |
| # Version: 0.1.0 | |
| # Updated: 2026/05/26 | |
| # Copyright: (c) 2026 Isaac Paul | |
| # License: https://gist.github.com/izackp/5eba3008d3311e897c7486f3dee5d604 | |
| # | |
| # Changelog: | |
| # 0.1.0 (2026/05/26) — initial release. | |
| set -euo pipefail | |
| GIST_SYNC_VERSION="0.1.0" | |
| # --------------------------------------------------------------------------- | |
| # Help text | |
| # --------------------------------------------------------------------------- | |
| HELP_TEXT=$(cat <<'HELP' | |
| gist_sync.sh — bidirectional file <-> GitHub gist sync | |
| SYNOPSIS | |
| gist_sync.sh <command> [args] | |
| COMMANDS | |
| create [-p] <file> | |
| Create a new gist from <file>. Default is secret; -p makes it public. | |
| Inserts gist URL into <file> as comment and pushes stamped version | |
| back to gist. Refuses if <file> already carries a gist URL comment. | |
| If stamp-push fails the gist + local are left mismatched and the | |
| command exits 1 — rerun `up <file>` to reconcile. | |
| up [-f] <file> | |
| Push <file> to its gist (URL read from header comment). Refuses if | |
| gist diverged from cached baseline (remote has changes you have not | |
| pulled). Use -f to overwrite. Skips push when local == gist. | |
| down [-f] <file> | |
| Pull gist into <file>. Refuses if local diverged from cached baseline | |
| (you have unpushed edits). Use -f to discard local. Backup written | |
| to <file>.bak before overwrite. Skips overwrite when local == gist. | |
| sync | |
| Iterate every path in cache, pick `up`, `down`, or `conflict` per | |
| file by comparing three hashes (cached baseline, local, gist). | |
| Per-file errors do not abort the loop; summary printed at end. | |
| help, --help, -h | |
| Print this text. Also printed when no command is given. | |
| version, --version, -V | |
| Print version and exit. | |
| CACHE | |
| Location: ${GIST_SYNC_HOME:-$HOME/.gist_sync}/cache.tsv | |
| Format: <absolute_path>\t<sha256_of_last_synced_content> | |
| TSV chosen over CSV because paths commonly contain commas. Paths with | |
| literal tabs or newlines are refused. | |
| URL COMMENT PLACEMENT | |
| URL lands on line 2. | |
| - Shebang on line 1: | |
| line 1: #!/usr/bin/env bash (untouched) | |
| line 2: # gist: <url> (inserted) | |
| - No shebang: | |
| line 1: (blank, inserted) | |
| line 2: # gist: <url> (inserted) | |
| Comment marker by extension / filename: | |
| # .sh .py .rb .yml .yaml .toml .gitignore .tf .hcl .conf .cfg .ini | |
| Makefile Dockerfile .bashrc .zshrc .profile .bash_profile .env | |
| // .kt .swift .c .h .cpp .java .js .ts .jsx .tsx .rs .go .scala .cs | |
| <!-- ... --> .md .html .xml .svg | |
| .json is refused (no comment syntax). Other unknown extensions refused. | |
| SYNC DECISION TABLE | |
| Given local_hash, gist_hash, cached_hash: | |
| local == cache && gist == cache -> unchanged (noop) | |
| local != cache && gist == cache -> up | |
| local == cache && gist != cache -> down | |
| local != cache && gist != cache && | |
| local == gist -> converged (cache refreshed) | |
| local != cache && gist != cache && | |
| local != gist -> conflict (no transfer) | |
| CONCURRENCY | |
| A directory-based lock at $CACHE_DIR/lock.d serializes the script. Stale | |
| lock can be removed with: rmdir $CACHE_DIR/lock.d | |
| ENVIRONMENT | |
| GIST_SYNC_HOME Override cache directory (default $HOME/.gist_sync). | |
| EXIT CODES | |
| 0 ok | |
| 1 validation / runtime error | |
| 2 usage error (bad args, missing gh, unknown comment style) | |
| HELP | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Constants + tmp/lock lifecycle | |
| # --------------------------------------------------------------------------- | |
| CACHE_DIR="${GIST_SYNC_HOME:-$HOME/.gist_sync}" | |
| CACHE_TSV="$CACHE_DIR/cache.tsv" | |
| LOCK_DIR="$CACHE_DIR/lock.d" | |
| TMP_FILES=() | |
| LOCK_HELD=0 | |
| cleanup_exit() { | |
| local f | |
| if [ "${#TMP_FILES[@]}" -gt 0 ]; then | |
| for f in "${TMP_FILES[@]}"; do | |
| [ -n "$f" ] && rm -f -- "$f" | |
| done | |
| fi | |
| if [ "$LOCK_HELD" = "1" ]; then | |
| rmdir -- "$LOCK_DIR" 2>/dev/null || true | |
| fi | |
| } | |
| trap cleanup_exit EXIT | |
| mktmp() { | |
| local f | |
| f=$(mktemp) | |
| TMP_FILES+=("$f") | |
| printf '%s\n' "$f" | |
| } | |
| acquire_lock() { | |
| local tries=0 | |
| while ! mkdir -- "$LOCK_DIR" 2>/dev/null; do | |
| tries=$((tries + 1)) | |
| if [ "$tries" -ge 50 ]; then | |
| echo "gist_sync: cache lock $LOCK_DIR held; remove if stale: rmdir $LOCK_DIR" >&2 | |
| exit 1 | |
| fi | |
| sleep 0.1 | |
| done | |
| LOCK_HELD=1 | |
| } | |
| ensure_cache() { | |
| mkdir -p -- "$CACHE_DIR" | |
| [ -e "$CACHE_TSV" ] || : >"$CACHE_TSV" | |
| } | |
| require_gh() { | |
| command -v gh >/dev/null 2>&1 || { | |
| echo "gist_sync: gh CLI not found on PATH" >&2 | |
| exit 2 | |
| } | |
| } | |
| # --------------------------------------------------------------------------- | |
| # gh retry wrappers (handle transient network errors) | |
| # --------------------------------------------------------------------------- | |
| # Run `gh <args>` up to 3 times with exponential backoff, redirecting stdout | |
| # to <outfile>. The outfile is truncated before each attempt. | |
| gh_retry_to_file() { | |
| local outfile=$1; shift | |
| local attempts=3 delay=1 i=1 rc=0 | |
| while [ "$i" -le "$attempts" ]; do | |
| : >"$outfile" | |
| if gh "$@" >"$outfile" 2>/dev/null; then | |
| return 0 | |
| fi | |
| rc=$? | |
| if [ "$i" -lt "$attempts" ]; then | |
| sleep "$delay" | |
| delay=$((delay * 2)) | |
| fi | |
| i=$((i + 1)) | |
| done | |
| return "$rc" | |
| } | |
| # Run `gh <args>` up to 3 times. On success print captured stdout+stderr. | |
| # On final failure print captured output to stderr and return non-zero. | |
| gh_retry_capture() { | |
| local attempts=3 delay=1 i=1 rc=0 | |
| local out="" | |
| while [ "$i" -le "$attempts" ]; do | |
| if out=$(gh "$@" 2>&1); then | |
| printf '%s' "$out" | |
| return 0 | |
| fi | |
| rc=$? | |
| if [ "$i" -lt "$attempts" ]; then | |
| sleep "$delay" | |
| delay=$((delay * 2)) | |
| fi | |
| i=$((i + 1)) | |
| done | |
| printf '%s' "$out" >&2 | |
| return "$rc" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Path + validation helpers | |
| # --------------------------------------------------------------------------- | |
| abs_path() { | |
| local f=$1 | |
| (cd -- "$(dirname -- "$f")" && printf '%s/%s\n' "$PWD" "$(basename -- "$f")") | |
| } | |
| validate_path() { | |
| case "$1" in | |
| *$'\t'*|*$'\n'*) | |
| echo "gist_sync: path contains tab/newline, cannot store in cache: '$1'" >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Comment-style detection | |
| # --------------------------------------------------------------------------- | |
| detect_comment() { | |
| local file=$1 | |
| local base ext | |
| base=$(basename -- "$file") | |
| ext="${base##*.}" | |
| # Filename-based matches (no extension or special). | |
| case "$base" in | |
| Makefile|makefile|GNUmakefile|Dockerfile) echo "#"; return ;; | |
| .bashrc|.zshrc|.profile|.bash_profile|.env) echo "#"; return ;; | |
| .env.*) echo "#"; return ;; | |
| esac | |
| # If no real extension (no dot), refuse below. | |
| if [ "$ext" = "$base" ]; then | |
| echo "gist_sync: no extension on '$base' — add a rule in detect_comment()" >&2 | |
| exit 2 | |
| fi | |
| case "$ext" in | |
| sh|py|rb|yml|yaml|toml|gitignore|tf|hcl|conf|cfg|ini) echo "#" ;; | |
| kt|swift|c|h|cpp|java|js|ts|jsx|tsx|rs|go|scala|cs) echo "//" ;; | |
| md|html|xml|svg) echo "html" ;; | |
| json) | |
| echo "gist_sync: JSON has no comment syntax — cannot stamp URL into '$file'" >&2 | |
| exit 2 | |
| ;; | |
| *) | |
| echo "gist_sync: unknown comment style for extension '.$ext' — add to detect_comment()" >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| } | |
| format_comment_line() { | |
| local style=$1 url=$2 | |
| case "$style" in | |
| "#") printf '# gist: %s' "$url" ;; | |
| "//") printf '// gist: %s' "$url" ;; | |
| "html") printf '<!-- gist: %s -->' "$url" ;; | |
| esac | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Gist URL extraction / id parsing | |
| # --------------------------------------------------------------------------- | |
| extract_gist_url() { | |
| local file=$1 | |
| { head -n 5 -- "$file" 2>/dev/null \ | |
| | grep -oE 'https://gist\.github\.com/[^[:space:]]+' \ | |
| | sed -E 's/[->[:space:]]+$//' \ | |
| | head -n 1; } || true | |
| } | |
| gist_id_from_url() { | |
| local url=$1 id | |
| url="${url%/}" | |
| id="${url##*/}" | |
| if [ -z "$id" ] || [ "$id" = "$url" ]; then | |
| echo "gist_sync: malformed gist URL '$1'" >&2 | |
| exit 1 | |
| fi | |
| printf '%s\n' "$id" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Cache operations (TSV) | |
| # --------------------------------------------------------------------------- | |
| cache_get_hash() { | |
| local path=$1 | |
| awk -F'\t' -v p="$path" '$1==p {print $2; exit}' "$CACHE_TSV" 2>/dev/null || true | |
| } | |
| cache_upsert() { | |
| local path=$1 hash=$2 | |
| validate_path "$path" | |
| # mktemp inside CACHE_DIR so the final `mv` is same-filesystem and atomic. | |
| local tmp | |
| tmp=$(mktemp -- "$CACHE_TSV.XXXXXX") | |
| TMP_FILES+=("$tmp") | |
| awk -F'\t' -v p="$path" '$1!=p' "$CACHE_TSV" >"$tmp" | |
| printf '%s\t%s\n' "$path" "$hash" >>"$tmp" | |
| mv -- "$tmp" "$CACHE_TSV" | |
| } | |
| cache_rows() { | |
| awk -F'\t' '/^[[:space:]]*#/ {next} NF>=2 {printf "%s\t%s\n", $1, $2}' "$CACHE_TSV" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Hash + diff helpers | |
| # --------------------------------------------------------------------------- | |
| sha_file() { | |
| shasum -a 256 -- "$1" | awk '{print $1}' | |
| } | |
| print_diff() { | |
| local lb=$1 fb=$2 la=$3 fa=$4 | |
| diff -u --label "$lb" --label "$la" "$fb" "$fa" || true | |
| } | |
| # --------------------------------------------------------------------------- | |
| # URL comment insertion | |
| # --------------------------------------------------------------------------- | |
| insert_url_comment() { | |
| local file=$1 url=$2 | |
| local style first_line comment_line tmp | |
| style=$(detect_comment "$file") | |
| comment_line=$(format_comment_line "$style" "$url") | |
| first_line=$(head -n 1 -- "$file" 2>/dev/null || true) | |
| tmp=$(mktmp) | |
| case "$first_line" in | |
| "#!"*) | |
| printf '%s\n%s\n' "$first_line" "$comment_line" >"$tmp" | |
| tail -n +2 -- "$file" >>"$tmp" | |
| ;; | |
| *) | |
| printf '\n%s\n' "$comment_line" >"$tmp" | |
| cat -- "$file" >>"$tmp" | |
| ;; | |
| esac | |
| # Overwrite content into existing inode so mode + ownership survive. | |
| cp -- "$tmp" "$file" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Subcommands | |
| # --------------------------------------------------------------------------- | |
| cmd_help() { | |
| printf '%s\n' "$HELP_TEXT" | |
| } | |
| cmd_create() { | |
| local public=0 | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| -p) public=1; shift ;; | |
| --) shift; break ;; | |
| -*) echo "gist_sync: unknown flag '$1'" >&2; exit 2 ;; | |
| *) break ;; | |
| esac | |
| done | |
| [ $# -eq 1 ] || { echo "usage: gist_sync.sh create [-p] <file>" >&2; exit 2; } | |
| local file=$1 | |
| [ -r "$file" ] || { echo "gist_sync: cannot read '$file'" >&2; exit 1; } | |
| local existing | |
| existing=$(extract_gist_url "$file") | |
| if [ -n "$existing" ]; then | |
| echo "gist_sync: file already linked to $existing" >&2 | |
| echo "gist_sync: use 'up' to push local changes, 'down' to pull" >&2 | |
| exit 1 | |
| fi | |
| local abs | |
| abs=$(abs_path "$file") | |
| validate_path "$abs" | |
| # Validate comment style BEFORE creating remote gist (so we don't leak gists | |
| # for files we couldn't stamp anyway). | |
| detect_comment "$file" >/dev/null | |
| local create_args=(-d "$(basename -- "$file")") | |
| [ "$public" -eq 1 ] && create_args+=(-p) | |
| local url | |
| if ! url=$(gh_retry_capture gist create "${create_args[@]}" -- "$file"); then | |
| echo "gist_sync: gh gist create failed" >&2 | |
| exit 1 | |
| fi | |
| url=$(printf '%s\n' "$url" | awk 'NF{u=$0} END{print u}') | |
| case "$url" in | |
| https://gist.github.com/*) ;; | |
| *) echo "gist_sync: did not get a gist URL from gh, got: $url" >&2; exit 1 ;; | |
| esac | |
| local before | |
| before=$(mktmp) | |
| cp -- "$file" "$before" | |
| insert_url_comment "$file" "$url" | |
| local gist_id | |
| gist_id=$(gist_id_from_url "$url") | |
| # Stamp-push MUST succeed or local + gist diverge and a later `sync` would | |
| # silently restore the unstamped version. | |
| if ! gh_retry_capture gist edit "$gist_id" -- "$file" >/dev/null; then | |
| echo "gist_sync: gh gist edit $gist_id failed after stamping; gist '$url' has unstamped content, local has stamp" >&2 | |
| echo "gist_sync: rerun once network recovers: gist_sync.sh up '$file'" >&2 | |
| exit 1 | |
| fi | |
| cache_upsert "$abs" "$(sha_file "$file")" | |
| printf 'created: %s\n' "$url" | |
| print_diff "before/$file" "$before" "after/$file" "$file" | |
| } | |
| # do_up <file> <force:0|1> | |
| # Returns 0 on success, non-zero on failure (does NOT exit so callers like | |
| # cmd_sync can keep iterating). | |
| do_up() { | |
| local file=$1 force=${2:-0} | |
| local url gist_id abs | |
| url=$(extract_gist_url "$file") | |
| [ -n "$url" ] || { echo "gist_sync: no gist URL in '$file' (run 'create' first)" >&2; return 1; } | |
| gist_id=$(gist_id_from_url "$url") || return 1 | |
| abs=$(abs_path "$file") | |
| validate_path "$abs" | |
| local gist_tmp | |
| gist_tmp=$(mktmp) | |
| if ! gh_retry_to_file "$gist_tmp" gist view "$gist_id" -f "$(basename -- "$file")" -r; then | |
| echo "gist_sync: gh gist view $gist_id failed (or file '$(basename -- "$file")' not in gist)" >&2 | |
| return 1 | |
| fi | |
| local local_hash gist_hash cached_hash | |
| local_hash=$(sha_file "$file") | |
| gist_hash=$(sha_file "$gist_tmp") | |
| cached_hash=$(cache_get_hash "$abs") | |
| if [ "$local_hash" = "$gist_hash" ]; then | |
| cache_upsert "$abs" "$local_hash" | |
| printf 'unchanged: %s (local == gist)\n' "$url" | |
| return 0 | |
| fi | |
| # Safety: gist diverged from cached baseline means remote has changes the | |
| # local copy never saw. Force-flag required to clobber. | |
| if [ -n "$cached_hash" ] && [ "$gist_hash" != "$cached_hash" ] && [ "$force" != "1" ]; then | |
| echo "gist_sync: refuse 'up' — gist $gist_id differs from cached baseline (remote changed)" >&2 | |
| echo "gist_sync: run 'sync' for 3-way handling, 'down -f' to discard local, or 'up -f' to overwrite gist" >&2 | |
| return 1 | |
| fi | |
| print_diff "gist/$gist_id" "$gist_tmp" "local/$file" "$file" | |
| if ! gh_retry_capture gist edit "$gist_id" -- "$file" >/dev/null; then | |
| echo "gist_sync: gh gist edit $gist_id failed" >&2 | |
| return 1 | |
| fi | |
| cache_upsert "$abs" "$local_hash" | |
| printf 'up: %s\n' "$url" | |
| } | |
| # do_down <file> <force:0|1> | |
| do_down() { | |
| local file=$1 force=${2:-0} | |
| local url gist_id abs | |
| url=$(extract_gist_url "$file") | |
| [ -n "$url" ] || { echo "gist_sync: no gist URL in '$file' (run 'create' first)" >&2; return 1; } | |
| gist_id=$(gist_id_from_url "$url") || return 1 | |
| abs=$(abs_path "$file") | |
| validate_path "$abs" | |
| local gist_tmp | |
| gist_tmp=$(mktmp) | |
| if ! gh_retry_to_file "$gist_tmp" gist view "$gist_id" -f "$(basename -- "$file")" -r; then | |
| echo "gist_sync: gh gist view $gist_id failed (or file '$(basename -- "$file")' not in gist)" >&2 | |
| return 1 | |
| fi | |
| local local_hash gist_hash cached_hash | |
| local_hash=$(sha_file "$file") | |
| gist_hash=$(sha_file "$gist_tmp") | |
| cached_hash=$(cache_get_hash "$abs") | |
| if [ "$local_hash" = "$gist_hash" ]; then | |
| cache_upsert "$abs" "$local_hash" | |
| printf 'unchanged: %s (local == gist)\n' "$url" | |
| return 0 | |
| fi | |
| # Safety: local diverged from cached baseline means user has unpushed edits. | |
| if [ -n "$cached_hash" ] && [ "$local_hash" != "$cached_hash" ] && [ "$force" != "1" ]; then | |
| echo "gist_sync: refuse 'down' — local '$file' differs from cached baseline (you have unpushed edits)" >&2 | |
| echo "gist_sync: run 'sync' for 3-way handling, 'up -f' to push local, or 'down -f' to discard local" >&2 | |
| return 1 | |
| fi | |
| print_diff "local/$file" "$file" "gist/$gist_id" "$gist_tmp" | |
| # Always back up local before overwrite — undo safety net. | |
| cp -- "$file" "$file.bak" | |
| cp -- "$gist_tmp" "$file" | |
| cache_upsert "$abs" "$gist_hash" | |
| printf 'down: %s (backup at %s.bak)\n' "$url" "$file" | |
| } | |
| cmd_up() { | |
| local force=0 | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| -f|--force) force=1; shift ;; | |
| --) shift; break ;; | |
| -*) echo "gist_sync: unknown flag '$1'" >&2; exit 2 ;; | |
| *) break ;; | |
| esac | |
| done | |
| [ $# -eq 1 ] || { echo "usage: gist_sync.sh up [-f] <file>" >&2; exit 2; } | |
| [ -r "$1" ] || { echo "gist_sync: cannot read '$1'" >&2; exit 1; } | |
| do_up "$1" "$force" || exit 1 | |
| } | |
| cmd_down() { | |
| local force=0 | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| -f|--force) force=1; shift ;; | |
| --) shift; break ;; | |
| -*) echo "gist_sync: unknown flag '$1'" >&2; exit 2 ;; | |
| *) break ;; | |
| esac | |
| done | |
| [ $# -eq 1 ] || { echo "usage: gist_sync.sh down [-f] <file>" >&2; exit 2; } | |
| [ -e "$1" ] || { echo "gist_sync: '$1' does not exist (cannot determine gist; restore the file first)" >&2; exit 1; } | |
| do_down "$1" "$force" || exit 1 | |
| } | |
| cmd_sync() { | |
| [ $# -eq 0 ] || { echo "usage: gist_sync.sh sync" >&2; exit 2; } | |
| local up_n=0 down_n=0 unchanged_n=0 converged_n=0 skip_n=0 conflict_n=0 err_n=0 | |
| local rows=() | |
| while IFS=$'\t' read -r path cached_hash; do | |
| [ -n "$path" ] && rows+=("$path"$'\t'"$cached_hash") | |
| done < <(cache_rows) | |
| local entry path cached_hash | |
| if [ "${#rows[@]}" -eq 0 ]; then | |
| printf 'summary: up=0 down=0 unchanged=0 converged=0 skipped=0 conflicts=0 errors=0\n' | |
| return 0 | |
| fi | |
| for entry in "${rows[@]}"; do | |
| path="${entry%%$'\t'*}" | |
| cached_hash="${entry#*$'\t'}" | |
| printf -- '--- %s ---\n' "$path" | |
| if [ ! -e "$path" ]; then | |
| printf 'skip: %s (missing)\n' "$path" | |
| skip_n=$((skip_n + 1)) | |
| continue | |
| fi | |
| local url gist_id | |
| url=$(extract_gist_url "$path") | |
| if [ -z "$url" ]; then | |
| printf 'error: %s (no gist url)\n' "$path" | |
| err_n=$((err_n + 1)) | |
| continue | |
| fi | |
| if ! gist_id=$(gist_id_from_url "$url" 2>/dev/null); then | |
| printf 'error: %s (malformed url %s)\n' "$path" "$url" | |
| err_n=$((err_n + 1)) | |
| continue | |
| fi | |
| local local_hash gist_tmp gist_hash | |
| local_hash=$(sha_file "$path") | |
| gist_tmp=$(mktmp) | |
| if ! gh_retry_to_file "$gist_tmp" gist view "$gist_id" -f "$(basename -- "$path")" -r; then | |
| printf 'error: %s (gist %s unreachable or file missing)\n' "$path" "$gist_id" | |
| err_n=$((err_n + 1)) | |
| continue | |
| fi | |
| gist_hash=$(sha_file "$gist_tmp") | |
| if [ "$local_hash" = "$cached_hash" ] && [ "$gist_hash" = "$cached_hash" ]; then | |
| printf 'unchanged: %s\n' "$path" | |
| unchanged_n=$((unchanged_n + 1)) | |
| continue | |
| fi | |
| if [ "$local_hash" != "$cached_hash" ] && [ "$gist_hash" = "$cached_hash" ]; then | |
| if do_up "$path" 1; then | |
| up_n=$((up_n + 1)) | |
| else | |
| err_n=$((err_n + 1)) | |
| fi | |
| continue | |
| fi | |
| if [ "$local_hash" = "$cached_hash" ] && [ "$gist_hash" != "$cached_hash" ]; then | |
| if do_down "$path" 1; then | |
| down_n=$((down_n + 1)) | |
| else | |
| err_n=$((err_n + 1)) | |
| fi | |
| continue | |
| fi | |
| # Both differ from cache. | |
| if [ "$local_hash" = "$gist_hash" ]; then | |
| cache_upsert "$path" "$local_hash" | |
| printf 'converged: %s\n' "$path" | |
| converged_n=$((converged_n + 1)) | |
| continue | |
| fi | |
| # True conflict — print both diffs and skip. | |
| local cache_tmp | |
| cache_tmp=$(mktmp) | |
| if [ -n "$cached_hash" ]; then | |
| printf '(cached baseline hash: %s — content not retained)\n' "$cached_hash" >"$cache_tmp" | |
| else | |
| printf '(no cached baseline)\n' >"$cache_tmp" | |
| fi | |
| printf 'conflict: %s (local and gist both diverged from baseline)\n' "$path" | |
| printf ' local diff:\n' | |
| print_diff "cache" "$cache_tmp" "local" "$path" | |
| printf ' gist diff:\n' | |
| print_diff "cache" "$cache_tmp" "gist" "$gist_tmp" | |
| conflict_n=$((conflict_n + 1)) | |
| done | |
| printf 'summary: up=%d down=%d unchanged=%d converged=%d skipped=%d conflicts=%d errors=%d\n' \ | |
| "$up_n" "$down_n" "$unchanged_n" "$converged_n" "$skip_n" "$conflict_n" "$err_n" | |
| if [ "$conflict_n" -gt 0 ] || [ "$err_n" -gt 0 ]; then | |
| exit 1 | |
| fi | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Dispatcher | |
| # --------------------------------------------------------------------------- | |
| main() { | |
| if [ $# -eq 0 ]; then | |
| cmd_help | |
| exit 0 | |
| fi | |
| local cmd=$1 | |
| shift | |
| # help / version need no setup | |
| case "$cmd" in | |
| help|--help|-h) cmd_help; exit 0 ;; | |
| version|--version|-V) printf 'gist_sync.sh %s\n' "$GIST_SYNC_VERSION"; exit 0 ;; | |
| esac | |
| require_gh | |
| ensure_cache | |
| acquire_lock | |
| case "$cmd" in | |
| create) cmd_create "$@" ;; | |
| up) cmd_up "$@" ;; | |
| down) cmd_down "$@" ;; | |
| sync) cmd_sync "$@" ;; | |
| *) | |
| echo "gist_sync: unknown command '$cmd'" >&2 | |
| echo "Try: gist_sync.sh help" >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment