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.

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

Sometimes you find yourself heading in the wrong direction or selecting the wrong option when presented with a range of possible choices of how to proceed. In this case you may want to reverse back to an earlier time and choose a different path.

Revert

This command reverts the changes introduced by particular commits. This might be a change that introduced some undesirable behaviour or something that you only intended to add temporarily. Of course, it can be any other commit that you want to remove for some other 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.

git log --oneline
de429b0 (HEAD -> master) last commit
65ee869 other commit
f4b3d90 second feature
c9a1d3b first feature
56afa50 first commit
git revert c9a1d3b f4b3d90 --no-edit
[master 6ec5c46] Revert "first feature"
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 first_feature
[master 4724409] Revert "second feature"
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 second_feature
git log --oneline
4724409 (HEAD -> master) Revert "second feature"
6ec5c46 Revert "first feature"
de429b0 (HEAD -> master) last commit
65ee869 other commit
f4b3d90 second feature
c9a1d3b first feature
56afa50 first commit

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 soft Git reset 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 mixed Git reset 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 the hard Git reset. 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. The soft reset won’t change anything in that case, the mixed reset will remove changes from the staging area, and finally the hard reset 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.

There is another way to restore the file state from a remote branch.

git checkout myfile.txt

Sometimes there may be a file whose name is the same as a name of one of branches in your repository. For example, if you have file with name “develop” then performing a Git checkout will switch to the develop branch. There is a way to resolve any possible ambiguity when using this command.

git checkout –develop

The double dash operator is used to specify end of command line options, so in case of Git checkout “develop” will not be treated as branch, but as a file.

You might be also interested in the article:

Black-Box vs. White-Box Testing

Black-Box vs. White-Box Testing

Clean

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

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

The Git status command will test if there are any changes in the working directory that have not been pushed to the staging area or if any of the files aren’t being tracked in the Git repository. The result is a list of files that are staged, unstaged and untracked. Any unstaged files can be staged and any untracked files can be added to the repository using Git add commands.

Going back to the clean command: there is bunch of useful parameters that makes clean more powerful. For example, you can remove all untracked and ignored files from your repository 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

It’s always a good idea to use Git status to examine the state of a repository before committing any changes to prevent accidentally committing something that shouldn’t have been. Also, Git status is one of the ways for confirming that Git commit commands have worked and the updated file has arrived safely in the repository.

5. Git Aliases

If you often repeat the use of the same set of Git commands, especially with long sets of parameters, you can make life much easier by setting up aliases for them so you can use the alias instead of repeating the long form Git commands every time.

How to set up an alias

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

git config --global alias.unstage 'reset HEAD --'
You can list all available aliases with get-regexp git config parameter:
git config --get-regexp alias
alias.unstage reset HEAD --

Of course, this command is not only useful for listing aliases - git config –get-regexp could be used for listing any parameter which matches the provided regex:

git config --get-regexp user
user.name Example User
user.email user@example.com

Some additional examples Add changes to the last commit without changing its commit 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

Another useful alias allows us to push files to repository without entering the remote branch name explicitly. It will push the changes to an already existing remote branch, or it will create a new one if it’s not already present at the remote repository. The branch name will be obtained automatically by Git from HEAD (top of current branch).

git config --global alias.pushhead ‘push origin HEAD’
git pushhead
...
1221e2c..cbe855a  HEAD -> feature/my_branch
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
alias.pushhead push origin HEAD

Master your Git commands

Git commands can be broken down by their function, there are those for creating repositories (e.g. Git init and Git clone), those for basic snapshotting (e.g. Git add, Git commit and Git status), those for branching and merging (e.g. Git branch and Git checkout), those for collaborative sharing content (e.g. Git push and Git pull) and those for inspection and comparison (e.g. Git log and Git diff).

Some of the Git commands listed above are not ones you may use on a daily basis but knowing about their existence and what they can do will certainly pay off in the long term. Some of the Git commands are more commonly used but if you don’t know their full potential you will not be able to take full advantage of the potential of Git. I think that mastering 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