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

Useful Git tips and tricks Image by Unsplash

1. 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 .

2. 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.

3. 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.

4. 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

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

5. How to delete remote branches in Git

Contrary to the previous tip, we don’t use “git branch”; instead, we use “git push” with the “–delete” flag:

git push origin --delete feature_branch

6. 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.

7. 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

8. How to preview an earlier commit

Let’s say you’re working on a webapp and you’d like to inspect the frontend of your app as it was 5 or 6 commits ago. You’re not trying to revert the changes; you only want to see what the app looked like at some point in the past.

Doing this includes four steps:

  1. list the last several commits using git log --oneline -N (where N is the number of commits in the past)
  2. get the SHA-1 of the specific commit (the unique hash in front of each line obtained in the previous step; the hash looks like, for example: abcdef123)
  3. go back to the previous state of your app with this command: git checkout <unique SHA-1 hash of the commit>; once you do that, you will see a message that reads: “You are in ‘detached HEAD’ state”, along with some additional info. This detached state means you are not checked out in any specific branch anymore.
  4. Once you’ve looked around and are ready to go back to the newest commit (the HEAD of the current branch), use this command: git checkout <current branch name>

9. How to solve the warning “your branch and origin have divered”

Working in a team with Git can be tricky, or not, depending on the situation.

Recently, I’ve checked out a branch I’ve worked on, only to discover someone else in the team rebased the origin branch. That’s what caused the following notification on my local machine when I’ve checked out the branch again:

Switched to branch 'xyz'
Your branch and 'origin/xyz' have diverged, and have 4 and 9 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

Basically, this is why the above message appears:

  1. You submit a pull request in GitHub, or GitLab, or BitBucket
  2. A team member sees your pull request, and thinks, “Oh, I can rebase the merge request!”. Then, they push the “rebase” button and that adds more changes on the remote.
  3. Now we need to rebase our local branch as well, so that both the local branch and the remote are in sync.

Here’s the fix.

First, we check if we already have this solution set up in our .git/config local project file.

nano .git/config

Then, inside the nano editor, go to the very bottom, and look for these lines:

[pull]
	rebase = true

If that line does not exist, go back to the console and run:

git config pull.rebase true

Now you can confirm that it has been added to the .git/config file by again running:

nano .git/config

The reason why this works: with the config update, every git pull will also do a rebase - i.e. it will make our local branch always rebase, just like the remote branch gets rebased by other team members.

Now, all you have to do on your local branch, is run: git pull. Once you do, this is what happens:

First, rewinding head to replay your work on top of it...

And now, if we do a git status on our local branch, we’ll get the expected message:

On branch xyz
Your branch is up to date with 'origin/xyz'.

nothing to commit, working tree clean

The above message means that we’ve successfully resolved our issue.

10. How to see the actual changes in code between two commits without using SHA stubs

Let’s say the commit history looks a bit like this:

abcdef05 commit 05
abcdef04 commit 04
abcdef03 commit 03
abcdef02 commit 02
abcdef01 commit 01

And let’s say that we would like to compare the changes between the current HEAD and the commit before it. Here’s a simple way to do it, without using the SHA unique IDs of commits.

git diff HEAD HEAD~1

The HEAD "tilde" 1 argument above means “one commit before the HEAD”. If we wanted to go two commits in the past, we’d do:

git diff HEAD HEAD~2

Additionally, to quickly inspect the number of affected files and the number of changed lines in those files, we can do:

git diff --stat HEAD HEAD~1

11. Finding the parent branch that a new branch was checked out from

It seems that this is not as straightforward as one would expect. However, there is a simple command that might help:

git reflog

With this command, we’ll get a list of all the commits on the current branch, followed by commits from the parent branch, etc.

12. Removing the most recent commit from remote branch and local branch

Scenario: You had two files altered locally, but you wanted to push only one. Well, it was late at night, and your concentration was down, so instead of doing git add file-one.xyz, you did git add --all, and then merrily pushed your changes to the remote with a nice commit message. Oops.

So what to do now? If you are happy with removing the commit both on the remote and on the local branch, you can simply do this:

git reset HEAD^

That removes the commit locally. Now we can force-push the update:

git push origin +HEAD

Note: make sure that no one else checked out your branch before you did this correction, otherwise their history and yours will be in conflict.

13. Use git reset to undo the last commit

If we run the git reset command with the --soft flag, that will keep the current uncommitted changes in our project, but it will remove the most recent commit from git. That means that the HEAD will now be one commit in the past, and the previous, more recent HEAD position will be gone. The command is pretty easy:

git reset --soft HEAD~1

The git reset command is practically the reversal of the git add command.

14. Quickly switch to the branch you were previously on

This handy tip will save you some typing.

Let’s say you’re currently checked out on the-branch-with-a-very-long-name.

For some reason you need to switch to the master branch, so you go:

git checkout master

Now you’d like to switch to the previously checked out the-branch-with-a-very-long-name.

Here’s a shortcut way to doing just that:

git checkout @{-1}

Because the above command saves us some typing, we’ll get just a bit faster. Getting faster means being more productive, and that’s always good.

15. Delete a git branch from local and remote

In this article, we’ve already discussed how to delete a local git branch.

We’ll continue discussing branch deletion here, where we’ll examine how to delete a branch on the remote repo too.

This step comes after running either git branch -d feature_branch or git branch -D feature_branch.

As a reminder, the lowercase -d flag deletes a local branch only when it’s already been pushed and merged with remote, while the capital -D forces deletion regardless.

To delete a remote branch, we use the following syntax:

git push <remote> --delete <feature_branch>

Since most remotes are by default called origin, the command will like look as follows:

git push origin --delete feature_branch

When working in a team, it’s possible that someone else deletes a branch before us, in which case we’ll get this message:

error: unable to push to unqualified destination: feature_branch
The destination refspec neither matches an existing ref on the remote nor begins with refs/,
and we are unable to guess a prefix based on the source ref. 
error: failed to push some refs to 'git@<repository_name>'

This means we need to sync the list of branches with:

git fetch -p

Here, the -p flag stands for prune, meaning that local branches that can’t be found on the remote, will get deleted.

16. How to pull from remote a branch that I deleted locally

I tried to do something on the local branch, and severely messed it up, to the point where the only thing I could do to fix it was to delete the local branch and pull it from the remote.

Deleting it was easy, we’ve already covered it:

git branch -D feature_branch_name

But how to get it from the remote?

Simple. First we get the newest updates with git fetch or git pull, then we run:

git checkout feature_branch_name

That’s all there is to it.

17. Add an ssh key to your git profile

This instruction works for Linux OS.

First, check for existing keys with:

ls -al ~/.ssh

If there are any keys already there, they’ll have the file extension of .pub, and the file names of id_*.

Second, generate a new SSH key with:

ssh-keygen -t ed25519 -C "your_email@example.com"

For older systems, use:

ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

Press the ENTER key to accept defaults and again to skip the file password setup.

Third, add your ssh key to the ssh agent:

ssh-add ~/.ssh/id_ed25519

The success confirmation reads: Identity added <path/to/ssh/key> <your_email@example.com>.

Four, install xclip and copy the data inside the id_ed25519.pub file to your clipboard:

sudo apt-get install xclip
xclip -selection clipboard < ~/.ssh/id_ed25519.pub

Alternatively, simply open the id_ed25519.pub with less and copy the file contents.

Five, log into your account at github.com, click the profile image, and click the settings link in the dropdown. Then, find the SSH and GPG keys link on the settings page, and click the New SSH key button.

Paste in the data from the local ssh file, and click the Add SSH key button; a password prompt might pop up and you might need to confirm your password to complete the key addition.

Six, test drive your new key by trying to push to one of your repositories.

If you accessed your repo using a password, and now you’ve updated your local machine and want to use SSH for auth, you have to flip from using HTTPS access to SSH access, like this:

git remote set-url origin git@github.com:<username>/<repo>.git

18. List all git branches

When you clone a new respository locally, from, for example an existing project on GitHub, you’ll get the entire project on your hard drive.

If that repository has multiple branches, you can inspect them by running:

git branch -a

19. Undoing a git add command

Sometimes you run git add, perhaps with the --all flag, only to see that you’ve made a mistake, and you shouldn’t commit the code you’ve just staged to commit.

What can you do? You don’t want to lose the changes; you only want to remove the added code from the staging area.

In that case, there’s a very simple fix:

git reset

That’s it. Really simple.

20. A confusing scenario with “detached HEAD” state

After working for hours on end, a person is prone to errors. One such situation happened to me. I was trying to locally check out a branch like this:

git checkout remotes/origin/<branch-name>

And I was getting this unexpected message:

Note: checkout out 'remotes/origin/<branch-name>

You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example:

	git checkout -b <new-branch-name>

HEAD is now at 1234abc <Commit Message>

This was really confusing; What’s going on?, I thought. Until I realized that I was silly in trying to checkout a remote branch instead of simply doing this:

git checkout <branch-name>

But now that I was in this detached state, what was I to do?

The most important thing to note: this is not an error. You’re just currently “in-between branches”. In other words, you’re not on any branch at the moment. That’s why you got the suggestion that you might want to check out a new branch.

In other words, whenever you checkout a branch, you are “attached” if you’re on the tip of that branch, i.e on the HEAD. If you check out some other commit, you are “detached”, or in a “detached state”.

The solution? Just check out the branch you originally wanted to check out, which in my case, means, just run this:

git checkout <branch-name>

21. How to delete a file that is already in a merge request (on remote)?

Recently I had a situation where some cached file, a style.css and a tailwind-output.css files were not properly handled by the CI/CD, so they ended up being committed in one of my merge requests.

Before the MR was approved and merged on remote, the CI/CD issue was resolved, and now I was left with the situation where I needed to remove these two files that were already committed, and the commit was pushed to the remote, and furthermore, the pushed commit was part of a feature branch whose functionality was ready to be merged, so I submitted a merge request.

Now I needed to remove these two style.css and tailwind-output.css files, so as to be able to prevent future possible conflicts, and obviously, because these two files were to be handled by our CI process.

Here’s what I did to delete these two files from my pull request.

I started by reviewing the feature branch (I was already checked out on it).

git diff --stat HEAD~1 HEAD

Once I got the listing of files, I could copy the full path for the two offending files. The paths looked something like:

folder/subfolder/etc/.../styles.css
another-folder/etc/.../tailwind-output.css

Then I ran:

git rm --cached <first-full-file-name-with-the-full-path>
git rm --cached <second-full-file-name-with-the-full-path>

Next, I got back this ouput after running git status:

Changes to be committed:
	(use "git reset HEAD <file>..." to unstage)

	deleted: folder/subfolder/etc/.../styles.css
	deleted: another-folder/etc/.../tailwind-output.css

Finally, since the deleted files were obviously already staged for commit, I ran:

git commit -m "Remove cached files from the MR"

Next I pushed the changes with git push and that was it.