One useful feature of VCS (git or otherwise) is the ability to restore the state of a project to a previous point in time.
Here are some common mistakes and how to fix them.
Local changes
Changes you might want to undo before being pushed to a remote.
Commit
Commits can be undone using the git reset
command.
There are multiple ways to undo a commit, depending on what you want to do with the changes in it.
--hard
: Removes all changes from the removed commit--soft
: Puts all changes in the staging area--mixed
(default): Puts all changes in the working dir (unstaged)
We also need to tell git which commit we are resetting to. This is done by passing the hash of the commit prior to the one to be undone:
You can get the last 10 hashes with this command:
If only the last commit needs to be reset, HEAD~1
can be used instead of the hash to tell git to go to the commit before the current one:
Of course this allows for any number of commits to be undone, not just the last one.
Change
Let’s suppose, to keep things simple, that all changes to a file need to be undone.
If these changes have not yet been added to the staging area, git restore <file>
will remove those changes.
If instead they are already in the staging area, git restore --staged <file>
will unstage them, so that they can be either modified and restaged, or removed altogether using the previous command.
Of course if multiple files need to be handled a .
can be used instead of a list of file names.
Consider that this will apply to all files.
Similarly, if all uncommitted changes need to be fully discarded, running git reset --hard
with no commit hash will reset the state of the project to whatever is in the current commit, removing all other changes.
Merge
Undoing an in-progress merge is as simple as running git merge --abort
.
If however the merge has already been committed, the previously mentioned git reset --hard HEAD~1
will also work here. Of course, using the hash instead of HEAD~1
would work as expected.
Merges are ultimately just fancy commits.
The Nuclear option
Sometimes, the local work tree gets mangled by a combination of odd git abstractions and user error.
It might be easier to fully reset the local env to whatever is currently on the remote repo.
To do this, run these commands:
Here, the state of the remote repo is fetched, the state of the local repo is reset to the remote one, and all untracked files are cleaned recursively, leaving the working area with no changes.
Indeed, at this point one might consider rm -rf ./the_whole_project/ && git clone the_thing_again
.
This works, but also removes all branches and ignored files. Plus, big repos might take a while to fully clone. The commands described here should be more time-efficient.
Pushed changes
If the changes have already been pushed, using reset
like before will require a git push --force
, which will overwrite the remote repo with your current one (or more specifically, overwrite the conflicting changes).
This might not be an issue in a personal project but when working with other people it’s a bit no-no.
In fact, force pushes might be disabled altogether.
This makes sense, since changing the state of the remote while another person’s work depends on that (now overwritten) state can render their work useless or take a while to merge back together.
Revert
Apart from resetting a branch to a given commit, we can also revert a specific commit (or set of commits).
This way, instead of removing commits, we add new ones with the changes required to reset the state of the project to how it was before the commit to be reverted.
So given git log like this:
Resetting the last commit would require a force push, but git revert HEAD
will simply add a new commit that can be safely pushed:
Beware however, that if a revert is done on a commit previous to the last one, and the reverted changes are needed for the changes in newer commits to work, those commits might break (as in, the build might break, or the tests might fail).
In those cases you might need to revert multiple commits or introduce further ones to fix the issues.
This has no good solution, so consider reverting a commit as soon as possible and committing small changes at a time.
A commit that changes 200 files is bound to cause issues when reverted, while one that only modifies a function likely will not.
Merges
Perhaps surprisingly, using the previous revert command on a merge commit will fail:
That -m
flag takes a number that corresponds to the Main parent.
This makes sense, a merge by definition has two parent commits: the one you are on when running git merge
(1) and the one you are merging into it (2).
So if merging feature-branch
into master
, the former would be 2 and the latter would be 1.
The command:
Would create a revert commit restoring the state of master
.
Since more than two commits can be merged, the -m
flag takes an indefinite number.
In most cases, the expected behavior will be achieved passing 1 to it.