How to fix the most common mistakes in Git - learn it by breaking it

We all know how awesome Git is in tracking changes, simplifying collaboration between multiple developers and streamlining DevOps operations and it does all of that in the simplest ways.

However, not everything in Git is so simple or at least doesn't look so simple until you know how to use Git to deal with them.

In this tutorial, we take a very different approach, assuming you know the basics of Git, we try to see how to fix very common mistakes we might make working on a project version controlled by Git.

I describe each scenario and then I'll show you how to fix the issue.

Before we move on, remember you can implement your websites, web apps, APIs, landing pages, and much more, in no time with DoTenX for free. Make sure to check it out and even nominate your work to be showcased. DoTenX is open-source and you can find the repository here: github.com/dotenx/dotenx.


Let's get started.

Pushing changes to the wrong branch

You've made some changes, perhaps even deleted a few files, and likely added new files, commit the changes and push. All of a sudden you realise you had checked out your colleague's branch to check something out! Oops!

Let's fix it.

Let's say we had these files on John's branch:

And you added a line to b.txt and removed a.txt.

First, use the git log command to find the commit we want to undo:

$ git log
19f2583 (HEAD -> john-branch, origin/john-branch) bad commit
a0cd48e add hoy
701ae1d (origin/main, main) initial commit

We want to undo the commit with sha 19f2583, so we use git revert:

$ git revert 19f2583

In your terminal, you'll be prompted with the details of the commit and you can just quit it with :wq and push the commit generated by revert with git push.

So far you made John happy but there is a problem, which is you lost all your precious work! or maybe not.

Take a deep breath and switch to your branch and , use the git cherry-pick command to apply the changes from the commit you reverted to your current branch:

$ git cherry-pick 19f2583

Things might get a bit complicated here but it's just our friendly conflict! Depending on if you had previous changes in your own branch or not you might get one or more conflicts.

In my case, it shows the file b.txt is deleted, while we just added a line to that file. Let's see the changes using git status:

$ git status
On branch my-branch
You are currently cherry-picking commit 19f2583.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
        deleted:    a.txt

Unmerged paths:
  (use "git add/rm <file>..." as appropriate to mark resolution)
        deleted by us:   b.txt

For resolving the merge conflict in such cases I've found two simple rules:

  • If a file is edited and you see conflict marks in the file, simply pick the changes you want

  • If it's showing a file is deleted and it's causing the conflict, just listen to the message in git status: (use "git add/rm <file>..." as appropriate to mark resolution)

I just run git add b.txt and the conflicts are resolved.

$ git add b.txt
$ git cherry-pick --continue

bad commit

# Conflicts:
#       b.txt
#
# It looks like you may be committing a cherry-pick.
# If this is not correct, please run
#       git update-ref -d CHERRY_PICK_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
#
# On branch my-branch
# You are currently cherry-picking commit 19f2583.
#

Accept the message or change it (it's just a Vim doc opened in your terminal) and quit editing with :wq.

Don't forget to run git push after this.

Forgetting to commit staged changes before switching branches

You make some changes and stage them, and accidentally forcefully switch to another branch! Yes, it happens when you rely too much on commands' history.

First use git fsck to find the change:

$ git fsck --lost-found
Checking object directories: 100% (256/256), done.
dangling blob 98a727d446286e784a6d64a82e719ea6b626d53e

Switch back to your branch and use git show <id> to see the content you've lost:

$ git switch my-branch
$ git show 98a727d446286e784a6d64a82e719ea6b626d53e
hoy
This is a cool change by me

something

Now you have your changes and you can put them back where they belong to.

Undoing a faulty merge conflict

You have merged one branch into another one, one or more lines have changed in the same files in both branches, and you've faced merge conflicts. So far so good, and you resolve the issues by keeping the changes however you want, commit the merge and push it. The code is deployed and you realise that you weren't meant to merge the changes (maybe your branch was buggy or caused some issues).

Let's simulate the situation first.

git log in branch1:

[branch1]$ git log
ec0cf4e (HEAD -> branch1) add bye
701ae1d (origin/main, main, branch2) initial commit

git log in branch2:

[branch2]$ git log
073e46d (HEAD -> branch2) add yo
701ae1d (origin/main, main) initial commit

Now we merge them and 💥 :

$ git checkout branch1
$ git merge branch1
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
Automatic merge failed; fix conflicts and then commit the result.

We accept some changes and we pretend it's not what we wanted.

Let's fix it now.

First, let's find the commit history using git log:

$ git log --oneline
4bf3368 (HEAD -> branch2) Merge branch 'branch1' into branch2
073e46d add yo
ec0cf4e (branch1) add bye
701ae1d (origin/main, main) initial commit

Now, we might be facing two different situations:

  1. There aren't any changes merged into the target branch after the merge

  2. There are other changes merged into the target branch after the merge

If you're lucky enough to be in situation 1, simply use git reset (not revert) to rewrite the history and move the Head to the commit you want:

$ git reset --Hard 073e46d

After this you have undone the changes with a clean history:

$ git log --oneline
073e46d (HEAD -> branch2) add yo
701ae1d (origin/main, main) initial commit

This is particularly helpful if you continue the work on your branch to make sure it's in good shape to merge it again (although Git will see it as the first time you're merging them).

If you're not so lucky and there are some changes after your change, you need revert your changes (git rever -m 1 <merge commit sha>):

$ git revert -m 1 4e5278e

(* I merged the branches again, that's why the merge commit sha changed)

After committing the change, the history will look like this:

$ git log --oneline
0d49d3d (HEAD -> branch2) Revert "Merge branch 'branch1' into branch2"
4e5278e Merge branch 'branch1' into branch2
073e46d add yo
ec0cf4e (branch1) add bye
701ae1d (origin/main, main) initial commit

Now everything is back to normal, the only problem is that if you want to carry on the work on your branch and fix it and later merge it again, you have to revert this revert commit.

Accidentally deleting a branch

Yes, people do that! Actually, if you're reading this you might have already deleted your branch, so it happens.

Let's simulate the situation by deleting branch2 I created before.

First run git reflog command to find the commit that contains the lost work:

$ git reflog
701ae1d (HEAD -> main, origin/main) HEAD@{0}: checkout: moving from branch2 to main
0d49d3d HEAD@{1}: commit: Revert "Merge branch 'branch1' into branch2"
4e5278e HEAD@{2}: commit (merge): Merge branch 'branch1' into branch2
073e46d HEAD@{3}: reset: moving to 073e46d
4bf3368 HEAD@{4}: reset: moving to 4bf3368
4bf3368 HEAD@{5}: commit (merge): Merge branch 'branch1' into branch2
073e46d HEAD@{6}: checkout: moving from branch2 to branch2
073e46d HEAD@{7}: commit: add yo
701ae1d (HEAD -> main, origin/main) HEAD@{8}: checkout: moving from branch1 to branch2
...

Now use git cherry-pick with whatever commit that contains your lost work.

$ git cherry-pick 0d49d3d

Finish the cherry-pick as described earlier and you'll recover the lost changes.

Reverting to an old version of your code that you didn't mean to

For any reason, you revert your code, but it's not the commit you wanted to bring back. Now there are two situations:

  1. You haven't pushed the revert commit

  2. You have pushed the revert commit

First, let's simulate the situation. I've created a branch with this history:

$ git log --oneline
e9f0ccf (HEAD -> bad-revert) Revert "Add yo"
d7b0df1 Add yo
8929a21 Add hoy
701ae1d (origin/main, main, branch3) initial commit

Now, if you're in the first situation, simply run git reset:

$ git reset --hard Head^

The history is clean if the revert never happened.

$ git log 
d7b0df1 (HEAD -> bad-revert) Add yo
8929a21 Add hoy
701ae1d (origin/main, main, branch3) initial commit

Remember that Head^ means the immediate parent and is short for Head^1.

Now, if you have pushed the commit after reverting, you can just revert the revert with git revert <revert commit sha> or use git cherry-pick <commit before bad revert> for a cleaner history IMO:

$ git revert c92103a
2e74087 (HEAD -> bad-revert) Revert "Revert "Add yo""
c92103a Revert "Add yo"
d7b0df1 Add yo
8929a21 Add hoy
701ae1d (origin/main, main, branch3) initial commit

(* I reset the change and now try it with cherry-pick)

$ git cherry-pick cb174c0
bf188c6 (HEAD -> bad-revert) Add yo
cb174c0 Revert "Add yo"
d7b0df1 Add yo
8929a21 Add hoy
701ae1d (origin/main, main, branch3) initial commit

Accidentally committing sensitive information (e.g. passwords, personal data)

Oops!

Did you know that this is one of the most common/effective ways hackers use to attack organisations?

Alright, you did what you were not supposed to do, let's postpone writing a resignation letter, and fix the issue.

By the way, it's really a good idea to use something like git secrets to avoid committing sensitive information in the first place. It's not that hard to set up and use but let me know in the comments if you want me to write a tutorial about it.

Let's simulate the situation first.

$ git checkout -b b1
$ echo secrets > .env.production
$ echo randomchange >> a.txt
$ git add .
$ git commit -m "some good and bad changes"
$ git checkout main
$ git merge b1
$ git checkout -b b2
$ echo secrets2 >> .env.production
$ echo randomchange2 >> a.txt
$ git add .
$ git commit -m "again some good and bad changes"
$ git checkout main
$ git merge b2

This is what the history on the main branch looks like:

$ git log --oneline
f136e55 (HEAD -> main, b2) again some good and bad changes
659b0c0 (b1) some good and bad changes
701ae1d (origin/main, branch3) initial commit

Now let's fix it using the git filter-branch command to remove the sensitive information from the repository's history:

$ git filter-branch --force --index-filter \                                                 
  "git rm --cached --ignore-unmatch .env.production" \       
  --prune-empty --tag-name-filter cat -- --all

WARNING: git-filter-branch has a glut of gotchas generating mangled history
         rewrites.  Hit Ctrl-C before proceeding to abort, then use an
         alternative filtering tool such as 'git filter-repo'
         (https://github.com/newren/git-filter-repo/) instead.  See the
         filter-branch manual page for more details; to squelch this warning,
         set FILTER_BRANCH_SQUELCH_WARNING=1.
Proceeding with filter-branch...

Rewrite 659b0c01f0295d64ba199cc33e9fd2af8862e088 (11/13) (4 seconds passed, remaining 0 predicted)    rm '.env.production'
Rewrite f136e557fbc7359444f2154457c2bef54ea5d489 (11/13) (4 seconds passed, remaining 0 predicted)    rm '.env.production'

Ref 'refs/heads/b1' was rewritten
Ref 'refs/heads/b2' was rewritten
WARNING: Ref 'refs/heads/bad-revert' is unchanged
WARNING: Ref 'refs/heads/branch3' is unchanged
WARNING: Ref 'refs/heads/john-branch' is unchanged
Ref 'refs/heads/main' was rewritten
WARNING: Ref 'refs/heads/my-branch' is unchanged
WARNING: Ref 'refs/remotes/origin/john-branch' is unchanged
WARNING: Ref 'refs/remotes/origin/main' is unchanged
WARNING: Ref 'refs/remotes/origin/my-branch' is unchanged
Ref 'refs/stash' was rewritten

This command applies a filter to all commits in the repository, and removes the specified file from the history. We use --force flag to overwrite the existing history, and the --prune-empty flag to remove empty commits that are created as a result of the filter.

Let's see how it changed the history:

$ git log --oneline
87c7e80 (HEAD -> main, b2) again some good and bad changes
c772ec0 (b1) some good and bad changes
701ae1d (origin/main, branch3) initial commit

As you can see not only the secret file was deleted while keeping everything else (in all the branches), the history is not modified too.

Don't forget to push the changes with git push.


Git is the best friend of a developer and while it protects us from many problems, making mistakes is inevitable. Working as the maintainer of an open-source project, DoTenX, I have to deal with different challenges related to Git every now and then, but at the end of the day, unless you really mess up, Git always has a solution for you.

Finally, I invite you again to check DoTenX and its repository, use it as a modern alternative for Wordpress based on serverless architecture, with isolated front-end and back-end. Remember supporting open-source is free and starts with adding a ⭐️ to the projects you want to grow.