Skip to content

Instantly share code, notes, and snippets.

@mike-callahan
Last active October 17, 2025 17:43
Show Gist options
  • Save mike-callahan/e42969d2fe775d2be544b3ff15f898db to your computer and use it in GitHub Desktop.
Save mike-callahan/e42969d2fe775d2be544b3ff15f898db to your computer and use it in GitHub Desktop.
Git Save - A bash function that pushes your exact working state to a development branch without touching your staging area (its like control-S for git!)
git-save() { (
# Safe options scoped to subshell
set -e
set -u
set -o pipefail 2>/dev/null || true
# Configuration (add flags later if you want)
REMOTE="origin"
SNAPSHOT_BRANCH="snapshot/${USER:-dev}"
INCLUDE_IGNORED="false"
MESSAGE="Snapshot of working tree"
QUIET="true"
URL_FORMAT="https" # Options: "https" or "ssh"
# ============================================================================
# Core Functions
# ============================================================================
get_repo_context() {
# Gathers essential repository metadata and validates we're in a git repo.
# Sets global variables for repo name, current branch, HEAD commit, hostname,
# and timestamp. Also changes to the repository root directory.
git rev-parse --git-dir >/dev/null 2>&1 || { echo "❌ Not a git repo."; exit 1; }
TOP=$(git rev-parse --show-toplevel)
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "DETACHED")
PARENT_COMMIT=$(git rev-parse -q --verify HEAD 2>/dev/null || true)
REPO_NAME=$(basename "$TOP")
HOST=$(hostname -s 2>/dev/null || echo "host")
TS=$(date -u +%Y%m%dT%H%M%SZ)
cd "$TOP"
}
fetch_previous_snapshot_oid() {
# Fetches the remote snapshot branch into a temporary ref to get its commit ID.
# Uses a temp ref to avoid polluting the local branch namespace. Returns the
# commit SHA if it exists, or empty string if this is the first snapshot.
local snap_ref="refs/heads/${SNAPSHOT_BRANCH}"
local tmp_ref="refs/tmp/git-save-snap"
git update-ref -d "$tmp_ref" 2>/dev/null || true
git fetch --no-tags "$REMOTE" "+${snap_ref}:${tmp_ref}" >/dev/null 2>&1 || true
if git show-ref --verify --quiet "$tmp_ref"; then
git rev-parse "$tmp_ref"
else
echo ""
fi
}
build_snapshot_tree() {
# Creates a git tree object from the current working directory using a temporary index.
# Seeds the temp index from HEAD (helps with sparse checkouts), then adds all working
# tree files. Returns the tree SHA. This is the core magic that snapshots your work
# without touching your actual staging area.
local tmp_index
tmp_index="$(mktemp "${TMPDIR:-/tmp}/git-index.XXXXXX")"
trap 'rm -f "${tmp_index-}"' EXIT
export GIT_INDEX_FILE="$tmp_index"
# Seed from HEAD if it exists (helps sparse/unborn)
[[ -n "${PARENT_COMMIT:-}" ]] && git read-tree "$PARENT_COMMIT"
# Add working tree
if [[ "$INCLUDE_IGNORED" == "true" ]]; then
git add -A -f .
else
git add -A .
fi
# Write tree (loud failure)
local tree_id
if ! tree_id=$(git write-tree 2>._git_save_write_tree_err); then
echo "❌ git write-tree failed:" >&2
sed -n '1,120p' ._git_save_write_tree_err >&2 || true
rm -f ._git_save_write_tree_err
exit 1
fi
rm -f ._git_save_write_tree_err
if [[ -z "$tree_id" ]]; then
echo "❌ Empty tree generated. Debug (temp index):" >&2
git ls-files --stage >&2 || true
exit 1
fi
echo "$tree_id"
}
create_snapshot_commit() {
# Creates a commit from the given tree with two parents (when possible):
# Parent 1: previous snapshot (enables fast-forward pushes)
# Parent 2: current HEAD (shows provenance - what your code was based on)
# Returns the new commit SHA.
local tree_id="$1"
local prev_snapshot="$2"
local commit_msg="$MESSAGE
Repo: $REPO_NAME
When: $TS (UTC)
Host: $HOST
From branch: $CURRENT_BRANCH
Parent (HEAD): ${PARENT_COMMIT:-<none>}
Snapshot branch: $SNAPSHOT_BRANCH
Includes: working tree (untracked included$([[ "$INCLUDE_IGNORED" == "true" ]] && echo ", ignored included"))"
# Parents: prev snapshot (for FF), then HEAD (provenance)
local parents=()
[[ -n "$prev_snapshot" ]] && parents+=(-p "$prev_snapshot")
[[ -n "${PARENT_COMMIT:-}" ]] && parents+=(-p "$PARENT_COMMIT")
if [[ ${#parents[@]} -eq 0 ]]; then
printf "%s\n" "$commit_msg" | git commit-tree "$tree_id"
else
printf "%s\n" "$commit_msg" | git commit-tree "$tree_id" "${parents[@]}"
fi
}
update_local_ref() {
# Updates the local snapshot branch ref to point to the new commit.
# Uses update-ref with the old value for atomic compare-and-swap safety.
# This moves the branch pointer without doing a checkout.
local new_commit="$1"
local prev_snapshot="$2"
local snap_ref="refs/heads/${SNAPSHOT_BRANCH}"
if [[ -n "$prev_snapshot" ]]; then
git update-ref "$snap_ref" "$new_commit" "$prev_snapshot"
else
git update-ref "$snap_ref" "$new_commit"
fi
}
push_snapshot() {
# Pushes the snapshot branch to the remote, creating it if it doesn't exist.
# Respects QUIET mode - shows git progress when false, suppresses when true.
local redirect=""
[[ "$QUIET" == "true" ]] && redirect=">/dev/null 2>&1"
# Create the branch remotely if it doesn't exist
if ! git ls-remote --exit-code "$REMOTE" "refs/heads/${SNAPSHOT_BRANCH}" >/dev/null 2>&1; then
eval git push -u "$REMOTE" "${SNAPSHOT_BRANCH}:${SNAPSHOT_BRANCH}" $redirect
else
eval git push "$REMOTE" "${SNAPSHOT_BRANCH}:${SNAPSHOT_BRANCH}" $redirect
fi
}
build_commit_url() {
# Constructs a web-friendly URL pointing to the commit on the remote host.
# Normalizes URLs based on URL_FORMAT setting (https or ssh).
# For https: converts SSH URLs ([email protected]:user/repo) to HTTPS format
# For ssh: converts HTTPS URLs to SSH format ([email protected]:user/repo.git)
# Strips/adds .git extensions as appropriate.
local commit="$1"
local remote_url
remote_url=$(git remote get-url "$REMOTE")
if [[ "$URL_FORMAT" == "ssh" ]]; then
# Convert HTTPS to SSH if needed
if [[ "$remote_url" =~ ^https://([^/]+)/(.+)$ ]]; then
local host="${BASH_REMATCH[1]}"
local path="${BASH_REMATCH[2]}"
# Strip .git if present, we'll add it back
path="${path%.git}"
echo "git@${host}:${path}.git#${commit}"
elif [[ "$remote_url" =~ ^git@([^:]+):(.+)$ ]]; then
# Already SSH format
local path="${BASH_REMATCH[2]}"
path="${path%.git}"
echo "${remote_url%.git}.git#${commit}"
else
# Fallback
echo "${remote_url}#${commit}"
fi
else
# Convert to HTTPS (default)
remote_url="${remote_url%.git}"
if [[ "$remote_url" =~ ^git@([^:]+):(.+)$ ]]; then
remote_url="https://${BASH_REMATCH[1]}/${BASH_REMATCH[2]}"
fi
echo "${remote_url}/commit/${commit}"
fi
}
# ============================================================================
# Main Flow
# ============================================================================
get_repo_context
PREV_SNAPSHOT=$(fetch_previous_snapshot_oid)
TREE_ID=$(build_snapshot_tree)
NEW_COMMIT=$(create_snapshot_commit "$TREE_ID" "$PREV_SNAPSHOT")
update_local_ref "$NEW_COMMIT" "$PREV_SNAPSHOT"
push_snapshot
COMMIT_URL=$(build_commit_url "$NEW_COMMIT")
if [[ "$QUIET" == "false" ]]; then
echo "✅ Snapshot pushed!"
echo " Branch: ${SNAPSHOT_BRANCH}"
echo " Commit: ${NEW_COMMIT}"
echo " Remote: ${REMOTE}"
echo " URL: ${COMMIT_URL}"
fi
echo "$COMMIT_URL"
); }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment