DEV Community

loading...

10 Common Git Problems and How to Fix Them

citizen428 profile image Michael Kohl Originally published at citizen428.net Updated on ・7 min read

I originally wrote this article for Codementor in October 2014. It should have something for everyone, from fairly new git users to experienced developers.

1. Discard local file modifications

Sometimes the best way to get a feel for a problem is diving in and playing around with the code. Unfortunately, the changes made in the process sometimes turn out to be less than optimal, in which case reverting the file to its original state can be the fastest and easiest solution:

git checkout -- Gemfile # reset specified path 
git checkout -- lib bin # also works with multiple arguments
Enter fullscreen mode Exit fullscreen mode

In case you’re wondering, the double dash (--) is a common way for command line utilities to signify the end of command options.

2. Undo local commits

Alas, sometimes it takes us a bit longer to realize that we are on the wrong track, and by that time one or more changes may already have been committed locally. This is when git reset comes in handy:

git reset HEAD~2        # undo last two commits, keep changes
git reset --hard HEAD~2 # undo last two commits, discard changes  
Enter fullscreen mode Exit fullscreen mode

Be careful with the --hard option! It resets your working tree as well as the index, so all your modifications will be lost for good.

3. Remove a file from git without removing it from your file system

If you are not careful during a git add, you may end up adding files that you didn’t want to commit. However, git rm will remove it from both your staging area, as well as your file system, which may not be what you want. In that case make sure you only remove the staged version, and add the file to your .gitignore to avoid making the same mistake a second time:

git reset filename          # or git remove --cached filename
echo filename >> .gitignore # add it to .gitignore to avoid re-adding it
Enter fullscreen mode Exit fullscreen mode

4. Edit a commit message

Typos happen, but luckily in the case of commit messages, it is very easy to fix them:

git commit --amend                  # start $EDITOR to edit the message
git commit --amend -m "New message" # set the new message directly
Enter fullscreen mode Exit fullscreen mode

But that’s not all git-amend can do for you. Did you forget to add a file? Just add it and amend the previous commit!

git add forgotten_file 
git commit --amend
Enter fullscreen mode Exit fullscreen mode

Please keep in mind that --amend actually will create a new commit which replaces the previous one, so don’t use it for modifying commits which already have been pushed to a central repository. An exception to this rule can be made if you are absolutely sure that no other developer has already checked out the previous version and based their own work on it, in which case a forced push (git push --force) may still be ok. The --force option is necessary here since the tree’s history was locally modified which means the push will be rejected by the remote server since no fast-forward merge is possible.

5. Clean up local commits before pushing

While --amend is very useful, it doesn’t help if the commit you want to reword is not the last one. In that case an interactive rebase comes in handy:

git rebase --interactive 
# if you didn't specify any tracking information for this branch 
# you will have to add upstream and remote branch information: 
git rebase --interactive origin branch
Enter fullscreen mode Exit fullscreen mode

This will open your configured editor and present you with the following menu:

pick 8a20121 Upgrade Ruby version to 2.1.3 
pick 22dcc45 Add some fancy library 
# Rebase fcb7d7c..22dcc45 onto fcb7d7c 
# 
# Commands: # p, pick = use commit 
# r, reword = use commit, but edit the commit message 
# e, edit = use commit, but stop for amending 
# s, squash = use commit, but meld into previous commit 
# f, fixup = like "squash", but discard this commit's log message 
# x, exec = run command (the rest of the line) using shell 
# 
# These lines can be re-ordered; they are executed from top to bottom. 
# 
# If you remove a line here THAT COMMIT WILL BE LOST. 
# 
# However, if you remove everything, the rebase will be aborted. 
#
# Note that empty commits are commented out
Enter fullscreen mode Exit fullscreen mode

On top you’ll see a list of local commits, followed by an explanation of the available commands. Just pick the commit(s) you want to update, change pick to reword (or r for short), and you will be taken to a new view where you can edit the message.

However, as can be seen from the above listing, interactive rebases offer a lot more than simple commit message editing: you can completely remove commits by deleting them from the list, as well as edit, reorder, and squash them. Squashing allows you to merge several commits into one, which is something I like to do on feature branches before pushing them to the remote. No more “Add forgotten file” and “Fix typo” commits recorded for eternity!

6. Reverting pushed commits

Despite the fixes demonstrated in the previous tips, faulty commits do occasionally make it into the central repository. Still this is no reason to despair, since git offers an easy way to revert single or multiple commits:

 git revert c761f5c              # reverts the commit with the specified id
 git revert HEAD^                # reverts the second to last commit
 git revert develop~4..develop~2 # reverts a whole range of commits
Enter fullscreen mode Exit fullscreen mode

In case you don’t want to create additional revert commits but only apply the necessary changes to your working tree, you can use the --no-commit/-n option.

# undo the last commit, but don't create a revert commit 
git revert -n HEAD
Enter fullscreen mode Exit fullscreen mode

The manual page at man 1 git-revert list further options and provides some additional examples.

7. Avoid repeated merge conflicts

As every developer knows, fixing merge conflicts can be tedious, but solving the exact same conflict repeatedly (e.g. in long running feature branches) is outright annoying. If you’ve suffered from this in the past, you’ll be happy to learn about the underused reuse recorded resolution feature. Add it to your global config to enable it for all projects:

git config --global rerere.enabled true
Enter fullscreen mode Exit fullscreen mode

Alternatively you can enable it on a per-project basis by manually creating the directory .git/rr-cache.

This sure isn’t a feature for everyone, but for people who need it, it can be real time saver. Imagine your team is working on various feature branches at the same time. Now you want to merge all of them together into one testable pre-release branch. As expected, there are several merge conflicts, which you resolve. Unfortunately it turns out that one of the branches isn’t quite there yet, so you decide to un-merge it again. Several days (or weeks) later when the branch is finally ready you merge it again, but thanks to the recorded resolutions, you won’t have to resolve the same merge conflicts again.

The man page (man git-rerere) has more information on further use cases and commands (git rerere status, git rerere diff, etc).

8. Find the commit that broke something after a merge

Tracking down the commit that introduced a bug after a big merge can be quite time consuming. Luckily git offers a great binary search facility in the form of git-bisect. First you have to perform the initial setup:

 git bisect start         # starts the bisecting session
 git bisect bad           # marks the current revision as bad
 git bisect good revision # marks the last known good revision

Enter fullscreen mode Exit fullscreen mode

After this git will automatically checkout a revision halfway between the known “good” and “bad” versions. You can now run your specs again and mark the commit as “good” or “bad” accordingly.

git bisect good # or git bisec bad
Enter fullscreen mode Exit fullscreen mode

This process continues until you get to the commit that introduced the bug.

9. Avoid common mistakes with git hooks

Some mistakes happen repeatedly, but would be easy to avoid by running certain checks or cleanup tasks at a defined stage of the git workflow. This is exactly the scenario that hooks were designed for. To create a new hook, add an executable file to .git/hooks. The name of the script has to correspond to one of the available hooks, a full list of which is available in the manual page (man githooks). You can also define global hooks to use in all your projects by creating a template directory that git will use when initializing a new repository (see man git-init for further information). Here’s how the relevant entry in ~/.gitconfig and an example template directory look like:

[init]
    templatedir = ~/.git_template

  → tree .git_template
  .git_template
  └── hooks
      └── pre-commit
Enter fullscreen mode Exit fullscreen mode

When you initialize a new repository, files in the template directory will be copied to the corresponding location in your project’s .git directory.

What follows is a slightly contrived example commit-msg hook, which will ensure that every commit message references a ticket number like “#123“.

ruby
  #!/usr/bin/env ruby
  message = File.read(ARGV[0])

  unless message =~ /\s*#\d+/
    puts "[POLICY] Your message did not reference a ticket."
    exit 1
  end
Enter fullscreen mode Exit fullscreen mode

10. When all else fails

So far we covered quite a lot of ground on how to fix common errors when working with git. Most of them have easy enough solutions, however there are times when one has to get out the big guns and rewrite the history of an entire branch. One common use case for this is removing sensitive data (e.g. login credentials for production systems) that were committed to a public repository:

git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch secrets.txt' \
  --prune-empty --tag-name-filter cat -- --all
Enter fullscreen mode Exit fullscreen mode

This will remove the file secrets.txt from every branch and tag. It will also remove any commits that would be empty as a result of the above operation. Keep in mind that this will rewrite your project’s entire history, which can be very disruptive in a distributed workflow. Also while the file in question has now been removed, the credentials it contained should still be considered compromised!

GitHub has a very good tutorial on removing sensitive data and man git-filter-branch has all details on the various available filters and their options.

Discussion (24)

pic
Editor guide
Collapse
hoelzro profile image
Rob Hoelz

One obscure trick that I've had to use a few times in the past is that until you prune objects from .git via git-gc or git-prune, they're still under .git. So if you git-add a file and accidentally git reset --hard, you can still recover it through some Git object database spelunking - even if you didn't commit!

Collapse
mauriciodeveloper profile image
Maurício

Yep, git keep the files for 30 days at the trash area

Collapse
tomlincoln93 profile image
Gáspár Tamás

Whoa, thx for that, going to be life saver tomorrow.

Collapse
danaloni profile image
Dan Aloni

I strongly recommend to all careful developers to never use rerere.

Resolving a conflict does not sure that you resolved it correctly, as some bugs are only discovered quite a while after the conflict resolution is done. Therefore, you certainly would not want Git to remember conflict resolutions and replay them automatically. Even though it is possible to purge the rerere DB, nothing guarantees that you would remember to do so when re-doing merges.

It is such as badly designed "feature", that if rerere.enable is not specified to any value, and the current repository has a rerere DB, it will be enabled in conflict resolution. Therefore, it is best to have:

git config --global rerere.enabled false
Enter fullscreen mode Exit fullscreen mode

I prefer a rule of thumb, that if you find yourself repeating the same conflict resolutions over and over, then your development process could be wrong and you need to rethink the process.

Collapse
tmr232 profile image
Tamir Bahar

I have to agree on this one. I've been burned by rerere on multiple occasions. You merge, there are seemingly no conflicts, and then everything breaks.
I'd much rather repeat the same resolution over and over again and not get a bad automated resolution.

Collapse
bhilburn profile image
Ben Hilburn

Great list, thanks so much for sharing!

I especially appreciate that your heading for rebase included the phrase before pushing - trying to rebase pushed branches and then force-pushing is such a common mistake in shared repositories, hah. Nice work!

Collapse
rafaacioly profile image
Rafael Acioly

Nice post!

git commit --amend --no-edit is a good one too.

Collapse
zenulabidin profile image
Ali Sherief

It's an old post but I have something worth adding.

One useful git command I use if my local branch becomes really messy or if I want to undo an accidental merge or git reset --hard that I made more commits to is git reflog. It gives you a list of HEAD identifiers for each git operation you did and you can use git reset --hard HEAD@{NUMBER} to rewind the branch to that stage.

For example from a repo I'm working on:

$ git reflog
ba51eec69 (HEAD -> master, origin/master, origin/HEAD) HEAD@{0}: checkout: moving from master to master
ba51eec69 (HEAD -> master, origin/master, origin/HEAD) HEAD@{1}: commit: [youtube] Fix #29 YoutubePlaylistsIE
1b39e948f HEAD@{2}: reset: moving to origin/master
d6748de2d HEAD@{3}: checkout: moving from 1b39e948fbe01a130ce0c3c7dbcf2cf1840ae4b2 to master
1b39e948f HEAD@{4}: checkout: moving from master to upstream/master
d6748de2d HEAD@{5}: merge upstream/master: Merge made by the 'recursive' strategy.
e0479dd2b HEAD@{6}: reset: moving to HEAD
Enter fullscreen mode Exit fullscreen mode

I can undo the merge and everything I did after it using git reset --hard HEAD@{6}.

Collapse
jnareb profile image
Jakub Narębski

However, git rm will remove it from both your staging area, as well as your file system, which may not be what you want.

You can use git rm --cached, which would remove file only from the staging area (the index), while leaving it on filesystem. If you don't remember the command, git status would write it down for you!

Note also that git rm --cached works any time, even after many commits -- it would always make Git no longer track the file, and no longer have it in commits / in the repository, while git reset <file> would work only after you git add-ed the file but before you have created new commit with this new file -- it simply overwrites the state in the staging area (the index) with the state from the latest commit (HEAD); thus if addition was not committed, it would untrack the file (and only then).

Collapse
nickytonline profile image
Nick Taylor (he/him)

Git hooks are great. I really enjoy using husky and lint-staged for a great pre-commit workflow for Node/front-end dev.

Collapse
nickytonline profile image
Collapse
mteheran profile image
Miguel Teheran

git-init
git-reset

perfect combination when "pull" is not working fine jojoj

Collapse
valeth profile image
Valeth

Nice guide, pretty useful

Just one small typo in point 3:
.gitingore

Collapse
citizen428 profile image
Michael Kohl Author

Thanks, will fix!

Collapse
sublimegeek profile image
Jonathan Irvin

5 can be dangerous. Just avoid doing it to already pushed code. If it's only on your local, then it's fair game IMO.

Collapse
citizen428 profile image
Michael Kohl Author

Nothing. The original post is very old though, so it (a) didn't work back then or (b) I wasn't aware of it.

Collapse
ohad24 profile image
ohad24

thanks man

Collapse
crisz profile image
crisz

I just noticed how many commands are just mnemonic and they're not well designed. By the way, nice post!

Collapse
zx1986 profile image
張旭

useful post! thank you!

Collapse
pumbaacave profile image
pumbaacave

useful, thx

Collapse
sagar profile image
Sagar

Wow. Thanks buddy for git tips

Collapse
andrekelvin profile image