Skip to content

Instantly share code, notes, and snippets.

@nottrobin
Created November 8, 2016 09:57
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nottrobin/89bbfd5085fb31e2287858c02506325c to your computer and use it in GitHub Desktop.
Save nottrobin/89bbfd5085fb31e2287858c02506325c to your computer and use it in GitHub Desktop.
git force-clone: clone a repository; if it already exists locally, reset it to a clone of the remote.
#! /usr/bin/env bash
set -euo pipefail
_usage() {
echo "
Usage:
git-force-clone -b branch remote_url destination_path
Example:
git-force-clone -b master git@github.com:me/repo.git ./repo_dir
Provides the basic functionality of 'git clone', but if the destination git
repository already exists it will force-reset it to resemble a clone of the
remote.
Because it doesn't actually delete the directory, it is usually significantly
faster than the alternative of deleting the directory and cloning the
repository from scratch.
**CAUTION**: If the repository exists, this will destroy *all* local work:
changed files will be reset, local branches and other remotes will be removed.
OPTIONS:
-b, --branch The branch to pull from the remote (default: master)
-h, --help Display this help message
"
}
_check() {
if [ -z "$1" ]; then
echo "Error: Missing ${2}"
_usage
exit 1
fi
}
main() {
while [[ -n "${1:-}" ]] && [[ "${1:0:1}" == "-" ]]; do
case $1 in
-b | --branch )
branch=${2:-}
shift
;;
-h | --help )
_usage
exit 0
;;
* )
echo "Error: Invalid option: $1" >>/dev/stderr
_usage
exit 1
;;
esac
shift
done
remote_url=${1:-}
destination_path=${2:-}
_check "${remote_url}" "remote_url"
_check "${destination_path}" "destination_path"
if [ -d "${destination_path}/.git" ]; then
(
cd ${destination_path}
# Delete all remotes
for remote in `git remote`; do
git remote rm ${remote}
done
# Add origin
git remote add origin ${remote_url}
git fetch origin
# Set default branch
if [ -z "${branch:-}" ]; then
branch=`LC_ALL=C git remote show origin | grep -oP '(?<=HEAD branch: )[^ ]+$'`
git remote set-head origin ${branch}
else
git remote set-head origin -a
fi
# Make sure current branch is clean
git clean -fd
git reset --hard HEAD
# Get on the desired branch
git checkout ${branch}
git reset --hard origin/${branch}
# Delete all other branches
branches=`git branch | grep -v \* | xargs`
if [ -n "${branches}" ]; then
git branch -D ${branches}
fi
)
elif [ -n "${branch:-}" ]; then
git clone -b ${branch} ${remote_url} ${destination_path}
else
git clone ${remote_url} ${destination_path}
fi
}
main "$@"
exit 0
@lotyp
Copy link

lotyp commented Mar 26, 2023

Did some refactoring:

#! /usr/bin/env bash

#
# Original script by @nottrobin:
# https://gist.github.com/nottrobin/89bbfd5085fb31e2287858c02506325c
#

set -euo pipefail

usage() {
    cat <<EOF
Usage:
  git-force-clone -b branch remote_url destination_path
Example:
  git-force-clone -b master git@github.com:me/repo.git ./repo_dir

Provides the basic functionality of 'git clone', but if the destination git
repository already exists it will force-reset it to resemble a clone of the
remote.

Because it doesn't actually delete the directory, it is usually significantly
faster than the alternative of deleting the directory and cloning the
repository from scratch.

**CAUTION**: If the repository exists, this will destroy *all* local work:
changed files will be reset, local branches and other remotes will be removed.

OPTIONS:
  -b, --branch    The branch to pull from the remote (default: master)
  -h, --help      Display this help message
EOF
}

_check_arg() {
    if [ -z "$1" ]; then
        echo "Error: Missing ${2}" >&2
        usage
        exit 1
    fi
}

_get_default_branch() {
    local remote_url=$1
    git ls-remote --symref "$remote_url" HEAD | awk -F'/' '/ref:/{print $NF}' | awk '{print $NF}' | tr -d '\n'
}

main() {
    local branch=""

    while [[ $# -gt 0 ]]; do
        case $1 in
        -b | --branch)
            branch=$2
            shift
            shift
            ;;
        -h | --help)
            usage
            exit 0
            ;;
        -*)
            echo "Error: Invalid option: $1" >>/dev/stderr
            usage
            exit 1
            ;;
        *)
            break
            ;;
        esac
    done

    local remote_url=${1:-}
    local destination_path=${2:-}

    _check_arg "${remote_url}" "remote_url"
    _check_arg "${destination_path}" "destination_path"

    if [[ -d "${destination_path}/.git" ]]; then
        (
            cd "${destination_path}"

            # Delete all remotes
            for remote in $(git remote); do
                git remote rm "${remote}"
            done

            # Add origin
            git remote add origin "${remote_url}"
            git fetch origin

            # Set default branch
            if [[ -z "${branch}" ]]; then
                branch=$(_get_default_branch "${remote_url}")
                git remote set-head origin "${branch}"
            else
                git remote set-head origin -a
            fi

            # Make sure current branch is clean
            git clean -fd
            git reset --hard HEAD

            # Get on the desired branch
            git checkout "${branch}"
            git reset --hard "origin/${branch}"

            # Delete all other branches
            branches=$(git branch --format='%(refname:short)' | grep -v -E '^(main|master|\*)$' || true)
            if [[ -n "${branches}" ]]; then
                git branch -D "${branches}"
            fi
        )
    else
        git clone --branch "${branch:-master}" "${remote_url}" "${destination_path}"
    fi
}

main "$@"
  • Does not exit with error code 1, if script successfully completed
  • The check function is renamed to check_arg for better readability
  • The remote_url and destination_path variables are now declared locally

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment