Rollback Permission-Only Changes in Git
The Problem
After restoring a backup with rsync, you run git status and see a wall of modified files. You open git diff and realize none of them have actual content changes — only file permission changes (e.g., mode change 100644 => 100755). rsync preserves the permissions from the backup source, which may differ from what git tracks — so every file whose mode changed during the restore shows up as "modified".
Committing these changes is noise. Rolling them back one by one is tedious.
The Script
rollback_permission_changes.sh automates the cleanup. It scans git diff, identifies every file whose only change is a permission/mode change, and restores them to what HEAD says they should be — leaving files with real content changes completely untouched.
bash rollback_permission_changes.sh
Run with --dry-run first to see exactly what would be rolled back:
bash rollback_permission_changes.sh --dry-run
How It Works
The script uses two git diff modes together:
git diff --numstat reports how many lines were added and deleted per file:
0 0 some-script.sh
5 2 another-file.js
A file with 0 additions and 0 deletions has no content change — only a possible mode change.
git diff --summary reports mode changes:
mode change 100644 => 100755 some-script.sh
The script combines both: a file only qualifies for rollback if --summary shows a mode change and --numstat shows zero line changes. If a file has both a permission change and content changes, it is skipped and reported as a warning — you decide what to do with those.
Once the list is built, the script runs:
git checkout HEAD -- <file1> <file2> ...
This restores the file's mode (and content) to exactly what HEAD tracks, discarding the unwanted permission diff.
Example Output
[INFO] Files with permission-only changes to roll back:
→ scripts/deploy.sh
→ bin/run.sh
[INFO] Successfully rolled back permission changes for 2 file(s).
If there are mixed files (content + permission change):
[WARN] Skipping files with BOTH permission AND content changes:
↷ src/index.ts
The Full Script
#!/usr/bin/env bash
# rollback_permission_changes.sh
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
error "Not inside a git repository."
exit 1
fi
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
warn "Dry-run mode enabled — no changes will be applied."
fi
permission_only=()
skipped_mixed=()
while IFS=$'\t' read -r added deleted file; do
if git diff --summary -- "$file" | grep -qE 'mode change'; then
if [[ ("$added" == "0" || "$added" == "-") && ("$deleted" == "0" || "$deleted" == "-") ]]; then
permission_only+=("$file")
else
skipped_mixed+=("$file")
fi
fi
done < <(git diff --numstat)
if [[ ${#skipped_mixed[@]} -gt 0 ]]; then
warn "Skipping files with BOTH permission AND content changes:"
for f in "${skipped_mixed[@]}"; do echo " ↷ $f"; done
fi
if [[ ${#permission_only[@]} -eq 0 ]]; then
info "No purely permission-only changed files to roll back."
exit 0
fi
info "Files with permission-only changes to roll back:"
for f in "${permission_only[@]}"; do echo " → $f"; done
echo ""
if $DRY_RUN; then
warn "Dry-run: would run: git checkout HEAD -- ${permission_only[*]}"
else
git checkout HEAD -- "${permission_only[@]}"
info "Successfully rolled back permission changes for ${#permission_only[@]} file(s)."
fi
When to Use This
- After running a tool that recursively sets executable bits (build systems, unzip, Docker volume mounts)
- When a CI environment applies different umask settings than your local machine
- Anytime
git diffis full of mode-only changes you did not intend