If you have ever accidentally committed code under the wrong git.user/git.email, you should know you can rewrite the git log to change commit authors using git filter-branch.

Git filter-repo plugin

Install the git-filter-repo plugin for Git with Python:

1
pip install git-filter-repo

This plugin is required to run the git filter-repo command. It is ok to install this using “system Python.”

If you are running Linux, you can install it with apt install -y git-filter-repo (or whatever package manager your distribution uses, i.e. dnf for RedHat/Fedora).

Steps

Clone the repository that has the history you want to rewrite using the --bare flag:

1
git clone --bare git@github.com:user/repo.git

Run the following command to replace all instances of the old/wrong name & email with a different Git user:

1
2
3
4
5
6
7
8
git filter-branch --env-filter '
if [ "$GIT_COMMITTER_NAME" = "Old Name" ] && [ "$GIT_COMMITTER_EMAIL" = "old.email@example.com" ]; then
    GIT_COMMITTER_NAME="New Name"
    GIT_COMMITTER_EMAIL="new.email@example.com"
    GIT_AUTHOR_NAME="New Name"
    GIT_AUTHOR_EMAIL="new.email@example.com"
fi
' --tag-name-filter cat -- --branches --tags

Info

Replace Old Name with the old git config user.name, old.email@example.com with the old git config user.email, and do the same for New Name and new.email@example.com.

Clean the reflog and run Git garbage collection to remove any cached history with the old Git user:

1
2
git reflog expire --expire=now --all
git gc --prune=now

Finally, force push the changes back to the remote:

1
2
git push --force --all
git push --force --tags

Tip

If you have branch protection rules that prevent force pushing, you will need to turn them off temporarily to run the force push.

Verify the rewrite succeeded by cloning the repository to a new path and search the log for the old user:

1
2
3
4
mkdir ~/tmp
git clone git@github.com:user/repo.git ~/tmp/repo
cd ~/tmp/repo
git log --author="Old Name"

You should not see any results; if you do, look back through the history of your commands to see if there were any errors during the process.

Bash script

On a Linux or Mac system, you can use this Bash script to automate the steps above. Run the script with --help to see the usage menu.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#!/usr/bin/env bash
set -euo pipefail

## Find a Python interpreter for pip installs
PYTHON_BIN=""
for bin in python3 python py py3 python; do
  if command -v "$bin" >/dev/null 2>&1; then
    PYTHON_BIN=$bin
    break
  fi
done

## Ensure git-filter-repo is installed (try uv first, then pip)
if ! command -v git-filter-repo >/dev/null 2>&1; then
  echo "git-filter-repo not found."

  ## Install with uv, if available
  if command -v uv >/dev/null 2>&1; then
    echo "uv found. Installing git-filter-repo as a tool..."
    uv tool install git-filter-repo
    export PATH="$HOME/.local/bin:$PATH"
  fi

  if [[ -z "$PYTHON_BIN" ]]; then
    echo "No Python interpreter found. Please install Python or uv."
    exit 1
  fi

  ## Fallback to Python
  echo "Using $PYTHON_BIN to install git-filter-repo via pip..."
  "$PYTHON_BIN" -m pip install --user git-filter-repo
  export PATH="$HOME/.local/bin:$PATH"
fi

## Test git-filter-repo was installed correctly
if ! command -v git-filter-repo >/dev/null 2>&1; then
  echo "git-filter-repo still not found after installation attempts."
  exit 1
fi

## Function to print help menu/usage
usage() {
  echo ""
  echo "Usage: $0 [--force] \\"
  echo "          --repo-url git@github.com:user/repo.git \\"
  echo "          --source-email 'old.email@example.com' \\"
  echo "          --target-email 'new.email@example.com' \\"
  echo "          [--source-name 'Old Name'] \\"
  echo "          [--target-name 'New Name']"
  echo ""

  exit 1
}

## Default vars
REPO_URL=""
SRC_EMAIL=""
TGT_EMAIL=""
SRC_NAME=""
TGT_NAME=""
FORCE_PUSH=""

## Parse arguments
while [[ $# -gt 0 ]]; do
  case $1 in
  --repo-url)
    REPO_URL="$2"
    shift 2
    ;;
  --source-email)
    SRC_EMAIL="$2"
    shift 2
    ;;
  --target-email)
    TGT_EMAIL="$2"
    shift 2
    ;;
  --source-name)
    SRC_NAME="$2"
    shift 2
    ;;
  --target-name)
    TGT_NAME="$2"
    shift 2
    ;;
  --force)
    FORCE_PUSH=1
    shift
    ;;
  -h | --help)
    usage
    ;;
  *)
    echo "Invalid argument: $1"
    usage
    ;;
  esac
done

if [[ -z "$REPO_URL" || -z "$SRC_EMAIL" || -z "$TGT_EMAIL" ]]; then
  echo "Missing required arguments."
  usage
fi

echo "Repo URL: $REPO_URL"
echo "Replacing source email <$SRC_EMAIL> with target email <$TGT_EMAIL>"

if [[ -n "$SRC_NAME" ]]; then
  echo "Source name: $SRC_NAME"
fi
if [[ -n "$TGT_NAME" ]]; then
  echo "Target name: $TGT_NAME"
fi

## Create temporary directory to clone repo into
TMP_DIR=$(mktemp -d)
echo "Mirror cloning repository into temporary directory: $TMP_DIR"
git clone --mirror "$REPO_URL" "$TMP_DIR/repo"
cd "$TMP_DIR/repo"

echo "Rewriting commit history emails with git-filter-repo..."
## Read history with source username/email, replace with target
COMMIT_CALLBACK="
if commit.author_email.decode('utf-8') == '$SRC_EMAIL':
    commit.author_email = b'$TGT_EMAIL'
"
if [[ -n "$TGT_NAME" ]]; then
    COMMIT_CALLBACK+="
    commit.author_name = b'$TGT_NAME'
"
fi

COMMIT_CALLBACK+="
if commit.committer_email.decode('utf-8') == '$SRC_EMAIL':
    commit.committer_email = b'$TGT_EMAIL'
"
if [[ -n "$TGT_NAME" ]]; then
    COMMIT_CALLBACK+="
    commit.committer_name = b'$TGT_NAME'
"
fi

echo "Generated callback:"
echo "$COMMIT_CALLBACK"

## Rewrite history
git filter-repo --force --commit-callback "$COMMIT_CALLBACK"

echo "git filter-repo completed successfully"
echo "Verifying first few commits after rewrite..."
git log --all --pretty=format:"%h %ad %an <%ae>" --date=iso -5

echo "Removing backup refs..."
##  Remove all refs with old data
git for-each-ref --format='%(refname)' refs/original | xargs -r git update-ref -d
git for-each-ref --format='%(refname)' refs/backup | xargs -r git update-ref -d

echo "Expiring reflogs and pruning unreachable objects..."
## Expire old data locally before pushing
git reflog expire --expire=now --all
git gc --prune=now --aggressive

echo "Verifying no lingering commits with source email anywhere..."

## Get list of commits with source email
SOURCE_COMMITS=$(git for-each-ref --format='%(refname)' | while read -r ref; do
  git log "$ref" --pretty=format:"%H%x09%ad%x09%an%x09%ae%x09%cN%x09%cE" --date=iso |
    awk -v src_email="$SRC_EMAIL" '
        $4 == src_email || $6 == src_email {
            print FILENAME "\t" $0
        }' FILENAME="$ref"
done)

## If any commits remain with source email, exit
if [[ -n "$SOURCE_COMMITS" ]]; then
  echo ""
  echo "[ERROR] Some commits still contain the source email <$SRC_EMAIL>:"
  echo ""
  echo -e "Ref\tCommit\tDate\tAuthorName\tAuthorEmail\tCommitterName\tCommitterEmail"
  echo "$SOURCE_COMMITS"
  echo ""
  echo "Aborting push."

  exit 2
fi

echo "Removing local refs under refs/merge-requests/ to avoid push errors..."
## Remove all refs under refs/merge-requests/
git for-each-ref --format='%(refname)' refs/merge-requests | xargs -r -n 1 git update-ref -d || true

echo "Adding remote origin after filter-repo cleanup..."
## Re-add origin (git-filter-repo removes it)
git remote add origin "$REPO_URL"

echo "Pushing all branches and tags to origin forcibly..."
## Push rewritten histories back up
if [[ -n "${FORCE_PUSH:-}" ]]; then
  if ! git push origin --force --all; then
    echo ""
    echo "[ERROR] Failed to push rewritten commits to origin."

    exit 1
  fi

  if ! git push origin --force --tags; then
    echo ""
    echo "[ERROR] Failed to push rewritten commits to origin."

    exit 1
  fi

else
  if ! git push origin --force --all; then
    echo ""
    echo "[ERROR] Failed to push rewritten commits to origin."

    exit 1
  fi

  if ! git push origin --force --tags; then
    echo ""
    echo "[ERROR] Failed to push rewritten commits to origin."

    exit 1
  fi
fi

echo "Successfully rewrote all commits and pushed to remote."
echo "Temporary repo location: $TMP_DIR/repo"

exit 0