This article lists some experiences from working with Git on different projects.

Useful Git tips and tricks Image by Unsplash

Clean all updates on the current branch

I have worked on a project where due to how the project was setup, whenever I’d checkout a brand new branch, a couple of files would be automatically added. The .gitignore file was not setup to not track those files due to a number of reasons. Thus, the first thing I had to do when I’d checkout a new branch was to quickly remove add the tracked changes, so that I could have a clean slate to work with.

So, once you’ve checked out your brand new branch, the command to clean up all the automatic changes is actually really simple. Here goes:

git checkout .

Erase local Git branches that got merged into master

Sometimes doing a git branch locally will not be as useful as you’d expect, due to the fact that there are too many local branches checked out already. The following helpful code snippets makes it possible to erase “stale” local branches that got merged into master.

git branch --merged master | grep -v "\* master" | xargs -n 1 git branch -d

There’s a handy site that can help with understanding the above command; it’s called explainshell.com, and it allows us to type in the exact command that we used, and it will show us the meaning of each section of the bash command we typed in.

Combining several commits into one

Sometimes we need to combine several git commits into one, and for that we can use git squash.

Here’s a real-life example: I wrote a single database migration file and committed it into three different commits. Of course, when we run migrate up command, it’s not going to work, because it’s going to think that the migration already happened; i.e it’s not expected to have a migration file update for a single db migration spread across three files. So we’ll need to squash our commits.

Here’s how.

First, we’ll find our commits by running:

git log --oneline -10

This gives us an abbreviated overview of the last ten commits, on ten lines, with SHA stubs (unique ID stubs) for each of the commits.

Next, we’ll run git rebase -i HEAD~10. This will show us a list of 10 commits that we can squash (the 10 most recent commits before the HEAD commit).

On my machine, running the above code opens nano editor, with the following file:

/home/my-user-name/location/to/my/project/.git/rebase-merge/git-rebase-todo

The file looks like this:

pick SHA Commit message
pick SHA Commit message
pick SHA Commit message

There are 10 pick lines altogether here, after which there is the following message in the comments:

# Rebase SHA..SHA onto SHA (13 commands)
#
# Commands:
# p, pick = use commit
# r, reword = user 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
# d, drop = remove commit

# These lines can be re-ordered; they are execued 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

Also note that interestingly, the last commit on the list is the newest one.

After changing to squash on the last three commits, you’ll get an updated commit message, listing the commit messages that of the commits that were squashed. That way the history of all the squashed commits is preserved in the single commit message automatically. All you have to do is save the commit with what was already offered.

If you have already previously pushed commits onto remote, you will now need to run git push -f, or, alternatively, try git push --force-with-lease.

How to erase a git branch locally and pull again

I had a situation where I pulled the master branch, and then rebased it onto a local feature branch. However, another developer on my team updated the feature branch remotely. Since I rebased the master branch onto the local version of the feature branch, I couldn’t add any code to it, beacuse if I did, I could possibly force push the feature branch onto the remote, but that would also mean that I’d override all the updates from the remote feature branch that my co-worker added in the meantime.

So, I decided to just delete the local feature branch, then pull it again from remote.

Here’s how I did it.

First, I deleted my local feature branch:

git branch -d feature_branch

I got back the following message:

error: The branch 'feature_branch' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature_branch'

It’s logical that I’d get this error, because I rebased the master branch onto it. The message above is not an error, it’s a warning.

Since I know for a fact that I’ve only rebased the master branch onto my feature_branch (i.e that’s the only difference that causes the above git error message), I can delete it with -D. The -D flag skips the check for local un-pushed commits on the feature_branch branch.

So, without fear, I’ll just run:

git branch -D feature_branch

After that, to get the remote version of the feature_branch, I’ll run:

git checkout feature_branch

That’s it, I’ve got the updates that my colleague made, and I’ve only deleted my local changes.

Fix the Cannot update paths and switch to branch error

Sometimes when running the following command:

git checkout --track origin/<branch-name>

…you might get a weird error that goes like this:

fatal: Cannot update paths and switch to branch '<branch-name>' at the sam e time. Do you intend to checkout 'origin/<branch-name>' which can not be resolved as commit?

The solution is pretty easy.

First, as a verification step, make sure to inspect the remote with:

git remote -v

You’ll get back the remote, which looks something like:

origin git@........ .git (fetch)
origin git@........ .git (push)

Next, run the git fetch to update the branches. Likely, it will result in your <branch-name> being fetched:

	* [new branch]    <branch-name> -> origin/<branch-name>

Now you can re-run the command:

git checkout --track origin/<branch-name>

And this time,it should work: you should be switched to <branch-name> and it’s remote should also be set up accordingly.

Add only selected files to staging area

This is another scenario that I had at work recently. I had a file that needed updating, but there was no way to see the changes in the frontend.

Luckily, the functionality that I needed to add was not tightly coupled to that specific file, so instead of working on that file directly, I decided to work in a different file that I was able to verify on the frontend.

Let’s name our two files from this real-life example:

  1. unreachable.js
  2. reachable.js

So, my idea was to add code to reachable.js in order to test how the code will behave and what the result will be on the frontend. Then I could just copy those changes to unreachable.js.

Once I’ve writen code that worked as I wanted, I simply copied it over from reachable.js into unreachable.js.

However, I was facing an issue: while the code in unreachable.js was the code that I needed to commit, the changes in reachable.js - the changes that lead to the solution - were now redundant.

This now left me with the following question: How do I commit only the changes in one of these two files?

I was aware of one solution to the problem:

git add unreachable.js
git commit -m "Bugfix unreachable.js"
git push

The above commands in Git do this:

  1. The first command adds unreachable.js to the staging area
  2. The second command commits the staged unreachable.js with a commit message
  3. The third command pushes local changes to the remote repository

After running the above commands, I could just do git checkout . to “undo” the changes made to reachable.js, and it would all be great.

However, it was difficult to copy-paste the file path for the unreachable.js file, so it was a bit time-consuming to run git add unreachable.js (note that this is abbreviated; the path was very long).

The solution is the second approach to committing only specific files. This approach works if you only have a couple of files, or a few files at the most. Here it goes:

git add -p

The git add -p command does the following:

  1. It lists the file location and a single change in the file
  2. It asks the following question: Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

If you want to keep the change (“stage this hunk”), type the y letter. If you don’t want to keep the change, type the n letter.

Understanding git add --patch

The above command, git add -p, is just an alias for git add --patch. The --patch flag makes it possible for us to make more granular commits.

To understand all the options that are available to us, we can just type a question mark instead of a y or n that we listed above.

Here’s the full output:

Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? ?
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
e - manually edit the current hunk
? - print help

There is an option that we can use but is not listed: the s option.

The s stands for split the hunk. If the active hunk has a section of code that hasn’t been changed between edits, the s will split the hunk and thus make it possible to stage the edits separately.

Besides the story of why and how I used git add -p, there are other legitimate reasons to use the git add --patch:

  1. If you’ve written a lot of code, but forgot to spread it out over several commits, this is an alternative way to achieve the desired effect without much effort
  2. It makes it easy to go over your code hunk by hunk one more time before committing