Can I make git reset --hard safer or disable it?

392 Views Asked by At

I have gotten into the bad habit of using git reset --hard regularly.

I now discovered that there is git reset --keep, and if I really need to delete something I could even do git stash && git stash drop.

So I'd like to either disable hard resets, or make them ask before they delete uncommitted changes irrecoverably, or at least create a backup. Can this be done without wrapping git in a shell function?

3

There are 3 best solutions below

3
On

git does not intrinsically support what you're asking for.

You could write a script or Bash function called git and put that ahead of git in your PATH. The script or function would just check for "forbidden commands" and otherwise run the regular git program (e.g. /usr/bin/git).

4
On

Git is notorious for not warning the user when performing a potentially dangerous operation. And in any case, opinions can vary over what constitutes danger. Basically, when you use Git at the command line, you are saying you're a power user and you know what you're doing. There's no way to make Git disabuse you of that notion.

Instead, you could use a GUI, such as Sourcetree. It knows that reset --hard is potentially dangerous, and puts up an alert that forces you stop and think hard about what you're doing.

0
On

The question asks if there is a way to do this and ends by adding "is there a way to do this without a shell script". I take that to mean that the OP wants a way to do it that is preferably without a wrapper if possible. Since it is not possible without a wrapper, here is what I did to add some extra checks on my use of git. It involves writing a wrapper script which I alias to git in my shell startup files. The script makes the checks and exits with a message if I did something I'm trying not to do anymore otherwise it calls command git "$@".

The following script disables "git reset --hard" if there are uncommitted (staged or unstaged) changes to the repo.

function has_arg(){
    local arg=$1; shift
    for a in "$@" ; do
        if [[ "${a}" == "${arg}" ]] ; then
            return 0
        fi
    done
    return 1
}

function repo_is_clean(){
    git diff --no-ext-diff --quiet 2>/dev/null \
    && git diff --no-ext-diff --cached --quiet 2>/dev/null
}

# Copied straight from git-completion.bash to determine the git command
# with minor adaptations (using ${!c} to get the c-th argument instead
# of ${COMP_WORDS[c]} and some stuff removed).
declare i c=1 git_command
while [ $c -le ${#} ]; do
    i=${!c}
    case "$i" in
    --git-dir=*)  ;;
    --git-dir)   ((c++)) ;;
    --bare) ;;
    --help) git_command="help"; break ;;
    -c|--work-tree|--namespace) ((c++)) ;;
    -C) ((c++)) ;;
    -*) ;;
    *) git_command="$i"; break ;;
    esac
    ((c++))
done

if [[ ${git_command} == reset ]] && has_arg --hard "$@" && ! repo_is_clean ; then
    echo "PHIL: You have uncommitted changes, use stash to get rid of them"
    echo "      and git stash drop later which will allow you an extra"
    echo "      opportunity to realize if you made a mistake"
    exit 1
fi

command git "$@"

Some notes about how it works:

  • has_arg checks if its first argument is present in the list of following arguments so has_arg --hard "$@" checks if the arguments passed to the script contain --hard.
  • repo_is_clean:The git diff --no-exit-diff --quiet exists with 0 if there are no unstaged changes and the one with --cached does the same for staged changes. So if the function fails (returns non-zero), there are some uncommitted changes.
  • The while loop is from git-completion.bash, I found the code that it uses to determine what the git command is and stripped down what was unnecessary for this script's purposes.
  • One of the checks: if the command is reset and --hard is present and the repo has uncommitted changes, print a message and exit.
  • If we make it to the end of the script, delegate to the actual git command forwarding all arguments.

To use this, make this script executable and create an alias for it in your shell startup files

alias git=<your-script>

Breaking habits

Because the question mentions breaking habits, here is something a bit more "intense" that I did to break a different habit.

For reasons explained below, I wanted to break the habit of doing git commit -m and use the editor instead by doing git commit (without -m):

if [[ "${git_command}" == commit ]] && has_arg -m "$@"; then
    echo "PHIL: Don't use -m for commits"
    exit 1
fi

For a while, I was still always doing git commit -m "message", then getting the message, then doing it in the editor. I wanted that to change, so I made it more painful by replacing the simple echo with the following:

if [[ "${git_command}" == commit ]] && has_arg -m "$@"; then
    trap 'n=0; echo "sorry, you gotta wait! setting counter back to 0"' SIGINT
    echo "To help break the habit of using git commit -m, please endure"
    echo "uh, I mean 'enjoy' this 5 second uninterruptible sleep"
    for((n=1;n<=5;n++)); do
        sleep 1
        printf "\r${n}"
    done
    echo ""
    exit 1
fi

The trap on SIGINT causes the echo -n ... to be run when the user presses Ctrl-C in the shell to attempt to get out of the 5 second penalty.

Note that there is nothing wrong with using git commit -m as long as you write properly formatted commit messages. But there are people who write one-super-long-line commit messages and I want to be able to encourage them to always use the editor, and for that I have to do it myself.

And there are other reasons why I like using the editor all the time now but that's not relevant to the question and my "punishment" system barely is anyway.

Alternative to git reset --hard

While git reset --hard can be used to move where a branch points, the fact that it is a habit for you and that you mentioned git stash && git stash drop leads me to believe that you use it to get rid of unwanted uncommitted changes with the potential of realizing too late that you got rid of some changes that you wanted to keep.

In that case, the git checkout -p command is useful. It is like git add -p in that it shows you hunks of changes interactively and asks you to decide something. You press 'y' to discard the hunk