Last active
June 26, 2025 14:59
-
-
Save Johnpc123/41fc897ef53d79190cc191db5b6b472d to your computer and use it in GitHub Desktop.
Purges all git history of a folder, but keeps its current state in the latest commit. Used for clearing accidentaly commiting unencrypted secrets.
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
| #!/bin/bash | |
| # purge-git-path-history.sh | |
| # Purges all git history of a file or folder, but keeps its current state in the latest commit. | |
| # Useful for when you've accidentally commited sensitive or confidential information and must purge it from commmit history | |
| # with post-operation guidance, safety checks, and logging. | |
| set -e | |
| usage() { | |
| echo "Usage: $0 <relative-folder-path> [--dry-run]" | |
| echo | |
| echo "This script will:" | |
| echo " - Remove all history of <relative-folder-path> from your git repository." | |
| echo " - Restore the current contents of the folder in a new commit." | |
| echo | |
| echo "WARNING:" | |
| echo " - This operation rewrites git history and is DESTRUCTIVE." | |
| echo " - You should make a backup of your repository before proceeding." | |
| echo " - All collaborators will need to re-clone or reset after this change." | |
| echo " - You must have git-filter-repo installed." | |
| echo | |
| echo "Options:" | |
| echo " --dry-run Only print what would be done, do not perform any actions." | |
| echo | |
| echo "Example:" | |
| echo " $0 myfolder" | |
| exit 1 | |
| } | |
| if [ $# -lt 1 ]; then | |
| usage | |
| fi | |
| FOLDER="$1" | |
| DRY_RUN=0 | |
| LOG_FILE="purge-git-folder-history.log" | |
| if [[ "$2" == "--dry-run" ]]; then | |
| DRY_RUN=1 | |
| fi | |
| if [[ "$FOLDER" == *".."* ]]; then | |
| echo "Error: Invalid path component '..' found in '$FOLDER'" | |
| exit 2 | |
| fi | |
| if [ ! -d "$FOLDER" ]; then | |
| echo "Error: Folder '$FOLDER' does not exist in the current directory." | |
| exit 3 | |
| fi | |
| if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then | |
| echo "Error: This script must be run from the root of a git repository." | |
| exit 4 | |
| fi | |
| if ! command -v git-filter-repo > /dev/null 2>&1; then | |
| echo "Error: git-filter-repo is not installed." | |
| echo "Install with: pip install git-filter-repo" | |
| exit 5 | |
| fi | |
| # Check for uncommitted changes | |
| if ! git diff-index --quiet HEAD --; then | |
| echo "Warning: You have uncommitted changes. Please commit or stash them before running this script." | |
| exit 11 | |
| fi | |
| # Check if 'origin' remote exists before filtering | |
| ORIGIN_URL=$(git remote get-url origin 2>/dev/null || echo "") | |
| # Print backup locations | |
| REPO_BACKUP="../$(basename "$PWD")-backup" | |
| FOLDER_BACKUP="/tmp/$(basename "$FOLDER")-backup" | |
| # Warn if backup folder exists | |
| if [ -e "$FOLDER_BACKUP" ]; then | |
| echo "Warning: Backup path $FOLDER_BACKUP already exists. Aborting to prevent overwrite." | |
| exit 12 | |
| fi | |
| # Confirm script is being run from repo root | |
| REPO_ROOT=$(git rev-parse --show-toplevel) | |
| if [ "$PWD" != "$REPO_ROOT" ]; then | |
| echo "Warning: It is recommended to run this script from the root of your git repository ($REPO_ROOT)." | |
| read -p "Continue anyway? (yes/NO): " CONFIRM_DIR | |
| if [[ "$CONFIRM_DIR" != "yes" ]]; then | |
| echo "Aborted." | |
| exit 13 | |
| fi | |
| fi | |
| # Confirmation prompt | |
| echo "WARNING: This will rewrite git history and cannot be undone without a backup." | |
| echo "Backups will be made at:" | |
| echo " Repo: $REPO_BACKUP" | |
| echo " Folder: $FOLDER_BACKUP" | |
| read -p "Are you sure you want to continue? (yes/NO): " CONFIRM | |
| if [[ "$CONFIRM" != "yes" ]]; then | |
| echo "Aborted." | |
| exit 10 | |
| fi | |
| # Dry run mode | |
| if [ $DRY_RUN -eq 1 ]; then | |
| echo "[DRY RUN] Would back up repo to: $REPO_BACKUP" | |
| echo "[DRY RUN] Would back up folder to: $FOLDER_BACKUP" | |
| echo "[DRY RUN] Would run: git filter-repo --path \"$FOLDER\" --invert-paths" | |
| echo "[DRY RUN] Would restore folder and commit." | |
| exit 0 | |
| fi | |
| # Start logging | |
| exec > >(tee -a "$LOG_FILE") 2>&1 | |
| echo "==> Backing up current repository to $REPO_BACKUP" | |
| cd .. | |
| cp -r "$(basename "$OLDPWD")" "$(basename "$OLDPWD")-backup" | |
| cd "$OLDPWD" | |
| echo "==> Backing up folder '$FOLDER' to $FOLDER_BACKUP" | |
| cp -r "$FOLDER" "$FOLDER_BACKUP" | |
| # Branch name check | |
| CURRENT_BRANCH=$(git symbolic-ref --short HEAD) | |
| if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then | |
| echo "Notice: You are on branch '$CURRENT_BRANCH', not 'main' or 'master'." | |
| read -p "Proceed with branch '$CURRENT_BRANCH'? (yes/NO): " CONFIRM_BRANCH | |
| if [[ "$CONFIRM_BRANCH" != "yes" ]]; then | |
| echo "Aborted." | |
| exit 14 | |
| fi | |
| fi | |
| echo "==> Purging history of '$FOLDER' from git repository..." | |
| git filter-repo --path "$FOLDER" --invert-paths | |
| echo "==> Restoring folder '$FOLDER' to working directory" | |
| cp -r "$FOLDER_BACKUP" "$FOLDER" | |
| git add "$FOLDER" | |
| git commit -m "Restore $FOLDER after purging its history" | |
| echo | |
| echo "==> DONE." | |
| echo "You have purged all history of '$FOLDER' and restored its current state in a new commit." | |
| echo | |
| # Post-operation guidance for remotes and tracking | |
| HAS_ORIGIN=$(git remote | grep '^origin$' || true) | |
| if [ -z "$HAS_ORIGIN" ]; then | |
| echo "NOTICE: The 'origin' remote was removed by git-filter-repo for safety." | |
| if [ -n "$ORIGIN_URL" ]; then | |
| echo "To re-add it, run:" | |
| echo " git remote add origin $ORIGIN_URL" | |
| else | |
| echo "To re-add your remote, run:" | |
| echo " git remote add origin <your-repo-url>" | |
| fi | |
| echo | |
| echo "After re-adding the remote, set the upstream tracking branch with:" | |
| echo " git branch --set-upstream-to=origin/$CURRENT_BRANCH $CURRENT_BRANCH" | |
| echo | |
| echo "Then, force-push your changes with:" | |
| echo " git push --force origin $CURRENT_BRANCH" | |
| echo | |
| echo "Replace <your-repo-url> and <$CURRENT_BRANCH> with your actual remote URL and branch name." | |
| else | |
| UPSTREAM=$(git rev-parse --abbrev-ref "$CURRENT_BRANCH@{upstream}" 2>/dev/null || true) | |
| if [ -z "$UPSTREAM" ]; then | |
| echo "NOTICE: Your local branch '$CURRENT_BRANCH' is not set to track a remote branch." | |
| echo "To set tracking, run:" | |
| echo " git branch --set-upstream-to=origin/$CURRENT_BRANCH $CURRENT_BRANCH" | |
| echo | |
| fi | |
| echo "To force-push your rewritten history, run:" | |
| echo " git push --force origin $CURRENT_BRANCH" | |
| echo | |
| fi | |
| echo "All collaborators will need to re-clone or hard reset after this change." | |
| # Post-operation summary | |
| echo | |
| echo "Summary:" | |
| echo " - Purged history for folder: $FOLDER" | |
| echo " - Repo backup: $REPO_BACKUP" | |
| echo " - Folder backup: $FOLDER_BACKUP" | |
| echo " - Log file: $LOG_FILE" | |
| echo " - Current branch: $CURRENT_BRANCH" | |
| echo " - Next steps: review changes, re-add remote if needed, set tracking, and force-push." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment