How can I fix a corrupted Git repository?

As an alternative to Todd’s last option (Full Restores and Re-Initialization), if only the local repository is corrupted, and you know the URL to the remote, you can use this to reset your .git to match the remote (replacing ${url} with the remote URL):

mv -v .git .git_old &&            # Remove old Git files
git init &&                       # Initialise new repository
git remote add origin "${url}" && # Link to old repository
git fetch &&                      # Get old history
# Note that some repositories use 'master' in place of 'main'. Change the following line if your remote uses 'master'.
git reset origin/main --mixed     # Force update to old history.

This leaves your working tree intact, and only affects Git’s bookkeeping.

I also recently made a Bash script for this very purpose (Appendix A), which wraps a bit of safety around this operation.

Note:

  • If your repository has submodules, this process will mess them up somehow, and the only solution I’ve found so far is deleting them and then using git submodule update --init (or recloning the repository, but that seems too drastic).
  • This tries to determine the correct choice between ‘main’ and ‘master’ depending on local configuration settings, however there may be some issues if used on a repository that uses ‘master’, on a machine that has ‘main’ as the default branch.
  • This uses wget to check that the url is reachable before doing anything. This is not necessarily the best operation to determine that a site is reachable, and if you haven’t got wget available, this can likely be replaced with ping -c 1 "${url_base}" (linux), ping -n 1 "${url_base}" (windows), or curl -Is "${url_base}"

Appendix A – Full script

Also published as a gist, though it is now out of date.

#!/bin/bash

# Usage: fix-git [REMOTE-URL]
#   Must be run from the root directory of the repository.
#   If a remote is not supplied, it will be read from .git/config
#
# For when you have a corrupted local repo, but a trusted remote.
# This script replaces all your history with that of the remote.
# If there is a .git, it is backed up as .git_old, removing the last backup.
# This does not affect your working tree.
#
# This does not currently work with submodules!
# This will abort if a suspected submodule is found.
# You will have to delete them first
# and re-clone them after (with `git submodule update --init`)
#
# Error codes:
# 1: If a URL is not supplied, and one cannot be read from .git/config
# 4: If the URL cannot be reached
# 5: If a Git submodule is detected


if [[ "$(find -name .git -not -path ./.git | wc -l)" -gt 0 ]] ;
then
    echo "It looks like this repo uses submodules" >&2
    echo "You will need to remove them before this script can safely execute" >&2
    echo "Then use \`git submodule update --init\` to re-clone them" >&2
    exit 5
fi

if [[ $# -ge 1 ]] ;
then
    url="$1"
else
    if ! url="$(git config --local --get remote.origin.url)" ;
    then
        echo "Unable to find remote 'origin': missing in '.git/config'" >&2
        exit 1
    fi
fi

if ! branch_default="$(git config --get init.defaultBranch)" ;
then
    # if the defaultBranch config option isn't present, then it's likely an old version of git that uses "master" by default
    branch_default="master"
fi

url_base="$(echo "${url}" | sed -E 's;^([^/]*://)?([^/]*)(/.*)?$;\2;')"
echo "Attempting to access ${url_base} before continuing"
if ! wget -p "${url_base}" -O /dev/null -q --dns-timeout=5 --connect-timeout=5 ;
then
    echo "Unable to reach ${url_base}: Aborting before any damage is done" >&2
    exit 4
fi

echo
echo "This operation will replace the local repo with the remote at:"
echo "${url}"
echo
echo "This will completely rewrite history,"
echo "but will leave your working tree intact"
echo -n "Are you sure? (y/N): "

read confirm
if ! [ -t 0 ] ; # i'm open in a pipe
then
    # print the piped input
    echo "${confirm}"
fi
if echo "${confirm}"|grep -Eq "[Yy]+[EeSs]*" ; # it looks like a yes
then
    if [[ -e .git ]] ;
    then
        # remove old backup
        rm -vrf .git_old | tail -n 1 &&
        # backup .git iff it exists
        mv -v .git .git_old
    fi &&
    git init &&
    git remote add origin "${url}" &&
    git config --local --get remote.origin.url | sed 's/^/Added remote origin at /' &&
    git fetch &&
    git reset "origin/${branch_default}" --mixed
else
    echo "Aborting without doing anything"
fi

Leave a Comment