A bash script to zip uncommitted files in a local git repository
Want to back up your work without pushing it to the remote repository due to the precommit hooks?
Run this script within a local git repository. All modified and new files in the repository are zipped to ~/.git-backups/<repo-name>-<branch-name>/<repo-name>-<branch-name>-<timestamp>.zip
Warning: This script ignores files that have been committed regardless of whether they’ve been pushed to the remote repository. If it’s not output by git status
, it won’t be included in the archive.
This script runs fine on MacOS (which always requires extra work!) and it *should* run on Linux but I haven’t tried it.
Preparation
- MacOS:
brew install zip
- Copy the script below to (for example)
~/gitzip
chmod +x ~/gitzip
#!/bin/bash
# Run this script within a local git repository.
# All modified and new files in the repository are zipped to
# ~/.git-backups/<repository>-<branch>/<repository>-<branch>-<timestamp>.zip
# Exit immediately if a command exits with a non-zero status
set -e
# Function to print error messages to stderr and exit
error() {
echo "Error: $1" >&2
exit 1
}
# Function to generate a timestamp in the format YYYYMMDD-HHMMSS
generate_timestamp() {
date +"%Y%m%d-%H%M%S"
}
# Function to sanitize strings for use in file and directory names
sanitize() {
echo "$1" | tr ' /\\:*?"<>|' '----------'
}
# Change to the root of the Git repository
git_root=$(git rev-parse --show-toplevel 2>/dev/null) || error "Not inside a Git repository."
cd "$git_root" || error "Failed to change directory to Git root."
# Retrieve the repository name using the basename of the Git root directory
repo_name=$(basename "$git_root")
repo_name_sanitized=$(sanitize "$repo_name")
# Get the current Git branch name
branch_name=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || error "Failed to retrieve current Git branch."
branch_name_sanitized=$(sanitize "$branch_name")
# Get the list of changed files (staged and unstaged)
changed_files=$(git diff --name-only HEAD)
# Get the list of untracked files
untracked_files=$(git ls-files --others --exclude-standard)
# Combine changed and untracked files, ensuring no duplicates
combined_files=$(printf "%s\n%s" "$changed_files" "$untracked_files" | sort -u)
# Check if there are any files to back up
if [ -z "$combined_files" ]; then
echo "No changed or untracked files found."
exit 0
fi
# Create a temporary directory for copying changed and untracked files
temp_dir=$(mktemp -d) || error "Failed to create temporary directory."
# Ensure the temporary directory is removed on script exit
trap 'rm -rf "$temp_dir"' EXIT
# Copy changed and untracked files to the temporary directory, preserving directory structure
while IFS= read -r file; do
# Ensure the file exists (it might have been deleted)
if [ -e "$file" ]; then
# Create the target directory inside temp_dir
target_dir="$temp_dir/$(dirname "$file")"
mkdir -p "$target_dir" || error "Failed to create directory structure for $file."
# Copy the file to the target directory
cp "$file" "$target_dir/" || error "Failed to copy file: $file"
else
echo "Skipping deleted file: $file"
fi
done <<< "$combined_files"
# Create the zip file with the repository and branch name
zip_file="${repo_name_sanitized}-${branch_name_sanitized}-$(generate_timestamp).zip"
(
cd "$temp_dir" || error "Failed to change directory to temporary directory."
zip -r "$zip_file" ./* >/dev/null || error "Failed to create zip archive."
)
# Define the backup directory path
backup_dir="$HOME/.git-backups/${repo_name_sanitized}-${branch_name_sanitized}"
# Create the backup directory
mkdir -p "$backup_dir" || error "Failed to create backup directory: $backup_dir"
# Move the zip file to the backup directory
mv "$temp_dir/$zip_file" "$backup_dir/" || error "Failed to move zip file."
# Inform the user of the backup location
echo "Changed and untracked files have been zipped to $backup_dir/$zip_file"