5 Git commands that will make your work smarter

When I started my first project with Git, I learned how to use the basic 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 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.

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 has been changed so that isn’t the cause of the malfunction. 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.

So, what Git can do for you? 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 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 with the feature name. 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 <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 your previous branch state 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 application.

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 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 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 commits history. It contains information about branch, author and everything that could be helpful for checking the history and current state of the branch.

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. Check your reflog to 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: changes from feature branch 949bea0 (HEAD -> master) HEAD@{2}: checkout: moving from master to feature-branch $ git checkout -b restored-feature-branch 2e373a Switched to a new branch 'restored-feature-branch'

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 Git history, 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:

$ 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 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 but not least 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 with a higher priority. When the 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.

$ 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 stash list stash@{0}: WIP on master: a4e78e2 new code in master branch stash@{1}: On master: untracked file $ git stash drop stash@{1} Dropped stash@{1} (bf0271ac3b38f29556df9af2e39564635b6dac85) $ git stash list stash@{0}: WIP on master: a4e78e2 new code in master branch $ git stash clear $ git stash list ...

4. Undo your changes

Actually, this is three Git commands for the price of one. All three are really basic, but for new Git users it can be a little bit confusing which one should be used.

Revert

This command reverts the changes introduced by particular commits. This might be a change that introduced some undesirable behavior or something that you only intended to add temporarily. Of course, it can be any other commit that you want to remove for some reason. Type git revert and list all the commits that should be undone. Git will create one commit that will remove all changes introduced by those commits passed as a parameter.

Reset

The purpose of reset is to take your branch history back to a particular commit. It can also be used to remove changes that have not been committed yet. Git reset can be executed in three different modes:

$ git reset –soft

This one will reset the HEAD to a particular commit, but all changes will remain in the staging area. This means that these changes are ready to be committed again.

$ git reset –mixed

This is the default mode, it’s not required to pass parameters. Changes will not be staged after reset, but will be kept in the work tree, so you can manually pick which files you want to stage and commit.

$ git reset --hard HEAD is now at ee1164c last commit

If you want to discard all changes without preserving data in the working tree, use –hard mode. You don’t need to point to an exact commit to perform this reset, you can also use a reference relative to current the HEAD.

To reset the last commit:

$ git reset HEAD^ Unstaged changes after reset: M file_changed_in_last_commit

To reset the last three commits:

$ git reset HEAD~3 Unstaged changes after reset: M file_changed_in_last_commit M file_changed_two_commits_ago M file_changed_three_commits_ago

If you don’t provide a commit hash or a reference, the reset will affect only your current staging area. Soft reset won’t change anything in that case, mixed will remove changes from the staging area, and finally hard will discard all changes that have not been committed yet.

And if the reset goes wrong you can always count on _ORIG_HEAD_ and reflog.

Clean

This third one is used for cleaning the working directory. It will remove all untracked files, but will preserve all repository files.

$ git status On branch master Untracked files: (use "git add <file>..." to include in what will be committed)

    untracked_file
    untracked_file_2

nothing added to commit but untracked files present (use "git add" to track)

$ git clean -f Removing untracked_file Removing untracked_file_2

There is bunch of useful parameters that makes clean more powerful. For example you can remove all untracked and ignored files excluding jar archives (-x for including -e for an exclude pattern):

$ ls app.jar app.war code untracked_file $ git status On branch master Untracked files: (use "git add <file>..." to include in what will be committed)

    untracked_file

nothing added to commit but untracked files present (use "git add" to track)

$ git clean -f -x -e'*.jar' Removing app.war Removing untracked_file $ ls app.jar code

5. Git Aliases

Another thing that can be very useful on a daily basis are Git aliases. If you often use the same set of Git commands, especially with long sets of parameters, you can make life much easier by setting up aliases for them.

How to set up an alias

To set up an alias you need to use git config and provide an alias name and the command that should be executed:

$ git config --global alias.unstage 'reset HEAD --'

You can list all available aliases with get-regexp config parameter:

$ git config --get-regexp alias alias.unstage reset HEAD --

Some additional examples

Add changes to the last commit without changing its message:

$ git config --global alias.amend 'commit --amend --no-edit' $ git amend [master ca43c17] add file Date: Thu Mar 14 12:14:34 2019 +0100 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 file

Display a readable graph of Git history:

$ git config --global alias.graph 'log --all --decorate --oneline --graph' $ git graph

* 868aa22 (feature-branch) feature
| * ca43c17 (HEAD -> master) add file
|/  
* cdc896b add git ignore
* af1e063 initial commit

Of course, you can also add an alias for listing all aliases:

$ git config --global alias.aliases 'config --get-regexp alias' $ git aliases alias.unstage reset HEAD -- alias.amend commit --amend --no-edit alias.graph log --all --decorate --oneline --graph

Master your Git commands

Some of the Git commands listed above are not ones you use on a daily basis but knowing about their existence and what they can do will certainly pay off. Some of them are more commonly used but if you don’t know their full possibilities, you will not be able to take full advantage of the potential of Git. I think that mastering our Git skills is very underestimated, given it is one of the most commonly used basic tools for any programmer.

Contact:

Let's talk about
your business