5 Git commands that will make your work smarter

For those unfamiliar with Git, it’s a tool that makes managing source code simple, especially when it’s being developed by a collaborating team dispersed around the globe. Files are stored in a repository and checked out to a working directory when a developer wants to work on a file and then committed back to the repository when they have finished. When I started my first project with Git, I learned how to use the basic Git commands that are listed in any tutorial. And I stuck with those for some time, because what else can you use Git for?

5 Git commands that will make your work smarter
Contributor Photo - Łukasz Mitusiński

Łukasz Mitusiński

Java Developer

This is a common problem - a lack of detailed knowledge of how a solution can be used. I spent too much time trying to fix up failed merges, checking a list of parameters for some Git commands over and over again, and in long debug sessions that would be a lot easier and shorter with Git.

I’ve made a list of 5 Git commands that are real life savers and I hope that some of them will help you to save your precious time when you’re managing your code in a Git repository.

See also:

Quick recap: basic git commands

git diff compares the working directory with the index

     - all unstaged changes

git diff HEAD compares the working directory with the local repository

     - all changes since last commit

git diff –cached compares the index with the local repository

                                    - all changes between last commit and next commit

git diff <file> compares a file in the working directory with the index

git diff HEAD <file> compares a file in the working directory with the local repository

git diff –cached <file> compares a file in the index with the local repository

1. Bisect to the rescue!

Once in a while, you will find yourself in an annoying situation when one of the features in your app no longer works. Furthermore, after a brief investigation you are sure that none of the related code in your repository has been changed so that isn’t the cause of the malfunction. To make the situation even worse, the most popular commit message is “fix” and you can’t even identify which files were changed in repository. Your next option is probably a tedious debugging process, dealing with tons of code that may not even be related to the problem.

The good news is that Git offers a great debugging tool for this specific situation: Git bisect.

It simply finds the particular commit that introduced the error. Our hero is called “bisect” because it carries out a binary tree search. Your role in the whole process is to check whether the commit does contain an error or if the code is working fine.

Debugging with Git

At the beginning, you need to mark the current commit as “bad” and mark the last commit that resulted in a fully functional feature as “good”. After that Git will pick a commit between these two and it’s up to you to check whether this one is buggy or contains a working feature. It will recursively narrow the range as long as there is more than one commit left – the one you’re looking for.

Of course, it’s not only a valuable tool for tracing bugs, but will also help you to find the exact commit for any change in general.

Start bisect and mark current commit as “bad”:

git bisect start    
git bisect bad

Find the latest commit that was bug-free, for example you can use git log with the –grep parameter to search for a commit by commit message. After that you can mark the last commit that was working fine using its hash:

git log --oneline --all --grep='commit without bug'    
    d151996 commit without bug
git bisect good d151996
    Bisecting: 3 revisions left to test after this (roughly 2 steps)
    \[15cfda61715d2d901ab7540078643e146b443dc6] some commit 6

Git is now in bisect mode, at this moment you are working on the commit selected by Git from the range between commits marked as good and bad. Also, you will be provided with short info about how many commits are in the range and the number of steps to find the error.

Run your tests and mark the commits as good or bad according to your results.

git bisect bad    
    Bisecting: 1 revision left to test after this (roughly 1 step)
    \[33023a856796c74c12915b55434f88d7602c555a] some commit 4
git bisect good
    Bisecting: 0 revisions left to test after this (roughly 0 steps)
    \[1c307570ac1b424fa58ea83c5dde3cd7316a3438] some commit 5
git bisect good
    15cfda61715d2d901ab7540078643e146b443dc6 is the first bad commit
    commit 15cfda61715d2d901ab7540078643e146b443dc6
    Author: User _<mailto:user@example.com>_
    Date: Wed Mar 13 15:25:20 2019 +0100
_commit 6_

After you find the right commit, you can get back to previous state of your working directory with:

git bisect reset

Set appropriate tags for marked branches

Sometimes it may be a little bit confusing to call commits either “bad” or “good” especially if we are just searching for a change in the repository which is neither good nor bad.

In this case, you can use “old” and “new” instead, or even include your own marks for clarity. Let’s assume that we want to track a commit where one of the project dependencies changed, we can mark old and new commits using more accurate names to make it easier to properly mark each commit:

git bisect start --term-old spring4 --term-new spring5    
git bisect spring5

Run scripts for automatic testing

Performing tests to verify if the commit under review meets our requirements may take some time. It is possible to introduce automation to the process with the “run” command. For each iteration, Git will run the given command and mark commits as good or bad according to the script return code.

Let’s consider this scenario: after pulling the latest commits from the remote repository you are no longer able to build your application. Maybe you need to tweak a configuration of your local environment? The easy way to find the exact cause may be to mark the last commit you were working on as “old” and the latest one as “new”. After that you can just use the run command to find the last working build for you:

git bisect start
git bisect bad
git bisect good c72ac
    Bisecting: 3 revisions left to test after this (roughly 2 steps)
    [b99f7cb7a35e5d8eb3a6a328ca4419a239b1a9dd] Non breaking change 3
git bisect run ./gradlew clean build
    running ./gradlew clean build

And that’s it! Git will now recursively bisect the given commit range and run a clean build for each selected commit. As a result, you’ll be notified of the first commit that is breaking your build.

576faa87e99fc29d20218529e92a9b652b6de78f is the first bad commit
...
bisect run success

2. Undoable things with reflog

“I broke my repository, what should I do?”

I think the first step you need to take in most cases is to check your reflog.

What is that and how is it different from the regular Git log?

Git log is supposed to keep track of our commit history. It contains information about branch, author, commit message and everything that could be helpful for checking the history and current status of the branch.

You might be also interested in the article:

10 Steps to Becoming a JavaScript Developer

10 Steps to Becoming a JavaScript Developer

As the name may suggest, reflog keeps information about changes to references. It allows you to check a list of all actions in the local repository. And with that knowledge, we are able to undo a lot of changes that could be very painful to fix, like a deleted local branch.

Restore deleted branch

Git does its best not to lose any data, so you should be still able to restore your lost work. We can create a commit in new branch and check if it’s possible to list differences from that commit after branch is deleted.

git commit -m 'new file'
[feature-branch acf370a] new file
1 file changed, 1 insertion(+)
create mode 100644 config.file
git checkout master
Switched to branch 'master'
git branch -D feature-branch
Deleted branch feature-branch (was acf370a).
git checkout feature-branch
error: pathspec 'feature-branch' did not match any file(s) known to git.
git diff acf370a
diff --git a/config.file b/config.file
deleted file mode 100644
index 2ef267e..0000000
--- a/config.file
+++ /dev/null
@@ -1 +0,0 @@
-some content

Quick Recap: The Git branch command can be used to create, delete or just list the branches in a repository. Here the -D parameter will delete a branch irrespective of whether or not it is merged in HEAD.

Use git branch -d feature-branch if you want to ensure the branch is fully merged in HEAD before deleting. You can use the Git branch command with the –merged parameter to list branches that are fully merged with the HEAD.

Even if the branch with commit doesn’t exist anymore we are still able to compare changes that were introduced into repository. Git diff command shows that commit contains file that doesn’t exist in our master. More likely when you realise that you branch was deleted unintentionally you will not be able to easily find a commit hash related to id.

The best way is to check your reflog - you can find the latest commit for the deleted branch; it should be marked with a commit hash. You can use it to create a new branch based on that commit and merge it or continue with further development.

git reflog
949bea0 (HEAD -> master) HEAD@{0}: checkout: moving from feature-branch to    master
2e373ae HEAD@{1}: commit: add config files
949bea0 (HEAD -> master) HEAD@{2}: checkout: moving from master to feature-branch
git branch feature-branch 2e373a

You can also use Git checkout with -b option to create the new branch and switch to it in one step:

git checkout -b feature-branch 2e373a
Switched to a new branch 'feature-branch'

Alternatively, you can move or rename and existing branch and the corresponding reflog by using the Git branch command with the -m parameter.

Split commit after using amend

To take another case, let’s assume that you have done some work on a feature branch and instead of creating a new commit you amended it for the previous one. For the sake of keeping a clean history of the Git repository, you want to split the commit into two separate ones. Simply check in reflog for the reference to the state before the amendment and do a soft reset. As a result, you will have amended changes staged and waiting to be committed separately to our repository with an appropriate commit message:

git log --oneline -n 2
82de5a6 (HEAD -> master) new feature
     949bea0 some changes in master branch
git reflog
82de5a6 (HEAD -> master) HEAD@{0}: commit (amend): new feature
ed8b56f HEAD@{1}: commit: new feature
git reset --soft HEAD@{1}
git commit -m 'another feature'
[master b2c24b0] another feature
1 file changed, 1 insertion(+)
git log --oneline -n 2
b2c24b0 (HEAD -> master) another feature
ed8b56f new feature

Revert rebase operation

This works the same for reversing Git rebase. After carrying out a rebase we find our commits attached at the end of the base branch. Basically, you can use reflog to check the last state before the rebase and use a hard reset to restore the repository to the previous state.

git log --oneline -n 2
64eef74 (HEAD -> feature-branch) code from feature branch
a4e78e2 (master) new code in master branch
git reflog
64eef74 (HEAD -> feature-branch) HEAD@{0}: rebase finished: returning to refs/heads/feature-branch
64eef74 (HEAD -> feature-branch) HEAD@{1}: rebase: code from feature branch
a4e78e2 (master) HEAD@{2}: rebase: checkout master
1ecb24d HEAD@{3}: checkout: moving from master to feature branch
a4e78e2 (master) HEAD@{4}: commit: new code in master branch
git reset --hard HEAD@{3}
HEAD is now at 1ecb24d code from feature branch
git log --oneline -n 2
1ecb24d (HEAD -> feature-branch) code from feature branch
b2c24b0 another feature

Of course, this works not only for rebase but also for merge, reset and so on!

ORIG_HEAD

One last and useful trick: as long as you have not carried out any further operations on your branch, you can easily bring up the previous state without searching for the right reference in reflog. All complicated operations like the above mentioned rebase or reset will set the reference ORIG_HEAD to its previous HEAD state, so you can simply revert almost everything with:

git reset --hard ORIG_HEAD
HEAD is now at 1ecb24d code from feature branch

3. Git Stash

Sometimes you need to change your focus from the task you are working on to something else in your repository with a higher priority. When the initial work has not yet been finished or would even cause the project in its current state to crash, you don’t want to commit that into your branch. Of course, you can create a temporary commit or branch and bring it back after you finish working on the urgent hotfix but it’s not very convenient. Git offers a great feature for stashing your code without unnecessary effort.

Keep your uncommitted changes

Git stash basically creates a record of your current changes and brings back the clean branch. After that, you can safely check out other branches or start working from the beginning on the current one. It can be also useful when you want to try a different approach for solving a problem and want to be sure that the previous solution can be easily restored. Simply use Git stash (which is just a short form of Git stash push) to store changes and display everything stored in stash with Git stash list. You can also add -m parameter and pass commit message like in regular Git commit.

git stash
Saved working directory and index state WIP on master: a4e78e2 new code in master branch
git stash -u -m'untracked files'
Saved working directory and index state On master: untracked files
git stash list
stash@{0}: On master: untracked files
stash@{1}: WIP on master: a4e78e2 new code in master branch

Restore your stashed code whenever you want

Restoring your code is also very simple. There are two possible ways for bringing back your changes. The first one is to use Git stash pop stash{0} – this will restore your latest stash. Instead of 0 you can use any stash reference. If you don’t use a reference, Git will pick up the latest one by default. Alternatively, you can restore changes with Git stash apply stash{0}; the reference is also optional for this command. The difference between these two is that pop not only applies changes but also removes them from the stash list.

You can remove a single stashed record with Git stash drop or remove all records at once with Git stash clear.

git stash list
stash@{0}: On master: readme
stash@{1}: WIP on master: a4e78e2 new code in master branch
stash@{2}: On master: untracked file
stash@{3}: On master: some code
git stash pop
...
Dropped refs/stash@{0} (9523d8c55a84e6aceb05346e2d42d1a6166f4bf4)
git stash list
stash@{0}: WIP on master: a4e78e2 new code in master branch
stash@{1}: On master: untracked file
stash@{2}: On master: some code
git stash pop stash@{2}
...
Dropped stash@{2} (9c829eace30f94c39a7eafc250494e2df4306c1c)
git stash list
stash@{0}: WIP on master: a4e78e2 new code in master branch
stash@{1}: On master: untracked file
git stash apply stash@{1}
...
git