Git offers a large variety of features from the basic to the mind-blowing ones. Yet we find many tweaks along the way that make the development process smoother and faster. In this matter I would like to share with you valuable knowledge I have gained since my first touch with Git.

Have you ever wondered how you can make Git fit your needs? Have you ever asked yourself how it would be to have Git at your fingertip and use it seamlessly? I’m not saying I’m giving this to you but at least I can show you the way towards Git mastery. For other cool micro-improvements to your programming flow, you can also check out our “Actionable tips” article.

Here is my guide to using Git the pro’s way. Please note that this is my own perspective and not all the approaches below may suit you. Try picking up the ones that answer your demands. I’ve previously written an article covering Git tips and tricks, so consider that your Git 101… and now let’s move to the big leagues.

We are going to cover the following:

Setting up the environment Coping with typos Customised formats Preparing the commit Partially adding files Pre-commit hooks Finding a bug’s root cause Blame Bisect Undoing changes Commit changes Undoing… anything Aliases

Let’s start.

1. Setting up the environment

These are basically the first steps to take in order to tune your Git experience so that it will become quicker and sharper.

1.1 Coping with typos

Typos are always very annoying. Rewriting the entire command due to a typo has happened to me many times before. Moving the cursor back to only correct the mistyped word is quite cumbersome work. Anyway, it would be nice to have an auto correct feature, wouldn’t it?

Fortunately, Git has this built-in, we just need to turn it on!

$ git config --global help.autocorrect 1 1 $ git config -- global help . autocorrect 1

Yes, you’ve just done the magic. Don’t believe me? Let’s try it:

$ git reste HEAD . WARNING: You called a Git command named 'reste', which does not exist. Continuing under the assumption that you meant 'reset' in 0.1 seconds automatically... 1 2 3 4 $ git reste HEAD . WARNING : You called a Git command named 'reste' , which does not exist . Continuing under the assumption that you meant 'reset' in 0.1 seconds automatically . . .

How cool is that?

1.2 Customised formats

As you continuously work with Git, you may want to speed the process up, to make it prettier, more concise. By default, Git comes with oversized outputs for its commonly used commands like git status and git log .

Let’s take a look at how the standard git status command looks like:

git status $ git status On branch feature-x Your branch is ahead of 'origin/feature-x' by 1 commit.. Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: path/to/staged/file.ext Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: path/to/unstaged/file.ext Untracked files: (use "git add <file>..." to include in what will be committed) path/to/untracked/file1.ext path/to/untracked/file2.ext path/to/untracked/file3.ext 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $ git status On branch feature - x Your branch is ahead of 'origin/feature-x' by 1 commit . . Changes to be committed : ( use "git reset HEAD <file>..." to unstage ) modified : path / to / staged / file . ext Changes not staged for commit : ( use "git add <file>..." to update what will be committed ) ( use "git checkout -- <file>..." to discard changes in working directory ) modified : path / to / unstaged / file . ext Untracked files : ( use "git add <file>..." to include in what will be committed ) path / to / untracked / file1 . ext path / to / untracked / file2 . ext path / to / untracked / file3 . ext

We should rapidly learn how to stage, unstage or revert the changes for a file. Why not get rid of all those useless things, in order to have a better look at what really matters? And let’s also save some space:

git status -sb $ git status -sb ## feature-x...origin/feature-x [ahead 1] M path/to/staged/file.ext M path/to/unstaged/file.ext ?? path/to/untracked/file1.ext ?? path/to/untracked/file2.ext ?? path/to/untracked/file3.ext 1 2 3 4 5 6 7 $ git status - sb ## feature-x...origin/feature-x [ahead 1] M path / to / staged / file . ext M path / to / unstaged / file . ext ? ? path / to / untracked / file1 . ext ? ? path / to / untracked / file2 . ext ? ? path / to / untracked / file3 . ext

Much better!

The git log command output can be modified as well, according to your wishes. The subject is already covered in the previous Git Tips and Tricks article so you can take a quick look there in the “Log. Prettier format” section.

2. Preparing the commit

Let’s go further and see what tricks we can use when being about to commit the changes.

2.1 Partially adding files

You may have encountered this case: A bunch of files were changed, but you want those changes organised in several concise commits, rather than one big messy commit. Why split the changes into multiple commits? Well, not only will you make your life easier, but your code reviewing colleagues will also be charmed.

Interactive staging is a nice feature Git offers, helping you easily craft your commits to include partial file changes. If you run the git add command with the -i or --interactive option, you’ll go into an interactive shell mode:

git add -i $ git add -i staged unstaged path 1: unchanged +1/-1 path/to/file1.ext 2: unchanged +1/-1 path/to/file2.ext *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 1 2 3 4 5 6 7 8 9 $ git add - i staged unstaged path 1 : unchanged + 1 / - 1 path / to / file1 . ext 2 : unchanged + 1 / - 1 path / to / file2 . ext * * * Commands * * * 1 : status 2 : update 3 : revert 4 : add untracked 5 : patch 6 : diff 7 : quit 8 : help What now >

While most of the commands are quite straightforward, I think we can use the example of patch, which gives you the possibility to individually stage pieces of a file. This way, you can include one change to a certain commit and another change to a different one, even if the two changes have been made to the same file.

From the interactive prompt, you can type 5 or p . Or you can use the -p or --patch option to accomplish the same thing:

git add -p $ git add -p Car.php diff --git a/Car.php b/Car.php index 1e378a1..19932f6 100644 --- a/Car.php +++ b/Car.php @@ -16,6 +16,16 @@ class Car protected $model; /** + * Get manufacturer. + * + * @return string + */ + public function getManufacturer() + { + return $this->manufacturer; + } + + /** * Set manufacturer. * * @param string $manufacturer Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ git add - p Car . php diff -- git a / Car . php b / Car . php index 1e378a1..19932f6 100644 -- - a / Car . php ++ + b / Car . php @ @ - 16 , 6 + 16 , 16 @ @ class Car protected $ model ; /** + * Get manufacturer. + * + * @return string + */ + public function getManufacturer ( ) + { + return $ this -> manufacturer ; + } + + / * * * Set manufacturer . * * @ param string $ manufacturer Stage this hunk [ y , n , q , a , d , / , j , J , g , e , ? ] ?

There’s a lot of options at this point. By typing ? you can see an explanation of each of those:

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 g - select a hunk to go to / - search for a hunk matching the given regex j - leave this hunk undecided, see next undecided hunk J - leave this hunk undecided, see next hunk k - leave this hunk undecided, see previous undecided hunk K - leave this hunk undecided, see previous hunk s - split the current hunk into smaller hunks e - manually edit the current hunk ? - print help 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 g - select a hunk to go to / - search for a hunk matching the given regex j - leave this hunk undecided , see next undecided hunk J - leave this hunk undecided , see next hunk k - leave this hunk undecided , see previous undecided hunk K - leave this hunk undecided , see previous hunk s - split the current hunk into smaller hunks e - manually edit the current hunk ? - print help

Most of the time you will use y or n to decide for a hunk, but sometimes splitting the current hunk into smaller ones or leaving the hunk undecided until later can also be helpful.

If you stage a hunk and let other(s) unstaged, you’ll see the file in both the staging area and the working tree. That’s because the file is only partially staged:

$ git status -sb ## master MM Car.php 1 2 3 $ git status - sb ## master MM Car . php

One more thing is worth mentioning here. If you use the add -p command to an untracked file you will get the following:

$ git add -p Plane.php No changes. 1 2 $ git add - p Plane . php No changes .

However, if you first use add -N or add --intent-to-add, you can overcome that:

$ git add -N Plane.php $ git status -sb ## master M Car.php AM Plane.php $ git add -p Plane.php diff --git a/Plane.php b/Plane.php index e69de29..8887dc5 100644 --- a/Plane.php +++ b/Plane.php @@ -0,0 +1,17 @@ +<?php + +/** + * Plane + */ +class Plane +{ + /** + * @var float + */ + protected $power; + + /** + * @var float + */ + protected $maximumSpeed; +} Stage this hunk [y,n,q,a,d,/,e,?]? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 $ git add - N Plane . php $ git status - sb ## master M Car . php AM Plane . php $ git add - p Plane . php diff -- git a / Plane . php b / Plane . php index e69de29 . . 8887dc5 100644 -- - a / Plane . php ++ + b / Plane . php @ @ - 0 , 0 + 1 , 17 @ @ + < ? php + + /** + * Plane + */ + class Plane + { + /** + * @var float + */ + protected $ power ; + + /** + * @var float + */ + protected $ maximumSpeed ; + } Stage this hunk [ y , n , q , a , d , / , e , ? ] ?

More than that, the patch mode can be used for partially resetting a file git reset -p, checking it out git checkout -p or stashing it git stash save -p. Did we mention how easy it is sometimes to use interactive consoles for everything but the kitchen sink?

2.2 Pre-commit hooks

It might have happened to you: You accidentally commited and pushed some debugging code. Even if it’s not something that necessarily causes trouble, it’s simply not recommended to have debugging code in production. What if a script would run every time you made a commit? A script which would tell you if you have any stray pieces of debugging code?

You’ve guessed it! Git offers you the possibility to execute a custom script at specific events. One of them is the pre-commit, when you can prevent the commit from happening if certain conditions are not met. In order to accomplish that, you need to create the file pre-commit in your repository’s .git/hoooks/ directory with the following content:

pre-commit #!/usr/bin/env ruby FORBIDDEN = [ # General /\bbitch\b/, /\bdebugger\b/, /\bdo not commit\b/i, /\bfuck\b/, /\bLorem\.ipsum\.dolor\.sit\.amet\b/, /\bshit\b/, /\btodo\b/i, /\bwtf\b/, # JavaScript /\bconsole\.debug\b/, /\bconsole\.log\b/, # PHP /\bdie\b/, /\becho\b/, /\bvar_dump\b/ ] full_diff = `git diff --cached --` full_diff.scan(%r{^\+\+\+ b/(.+)

@@.*

([\s\S]*?)(?:^diff|\z)}).each do |file, diff| added = diff.split("

").select { |x| x.start_with?("+") }.join("

") if FORBIDDEN.any? { |re| added.match(re) } puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" puts %{WHAT ARE YOU THINKING YOU ASSHOLE! You cannnot commit "#{$1 || $&}" to #{file}} puts "To commit anyway, use --no-verify (which you fucking shouldn't do!)" puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" exit 1 end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #!/usr/bin/env ruby FORBIDDEN = [ # General / \ bbitch \ b / , / \ bdebugger \ b / , / \ bdo not commit \ b / i , / \ bfuck \ b / , / \ bLorem \ . ipsum \ . dolor \ . sit \ . amet \ b / , / \ bshit \ b / , / \ btodo \ b / i , / \ bwtf \ b / , # JavaScript / \ bconsole \ . debug \ b / , / \ bconsole \ . log \ b / , # PHP / \ bdie \ b / , / \ becho \ b / , / \ bvar_dump \ b / ] full_diff = ` git diff -- cached -- ` full_diff . scan ( % r { ^ \ + \ + \ + b / ( . + ) \ n @ @ . * \ n ( [ \ s \ S ] * ? ) ( ? : ^ diff | \ z ) } ) . each do | file , diff | added = diff . split ( "

" ) . select { | x | x . start_with ? ( "+" ) } . join ( "

" ) if FORBIDDEN . any ? { | re | added . match ( re ) } puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" puts % { WHAT ARE YOU THINKING YOU ASSHOLE ! You cannnot commit "#{$1 || $&}" to #{file}} puts "To commit anyway, use --no-verify (which you fucking shouldn't do!)" puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" exit 1 end end

I must note that the above is not my original work, I took it from here and adapted it to my needs. I recommend you to try something similar: Start from a basic working version, then change it (over time), according to your own needs.

3. Finding a bug’s root cause

So you have a piece of code causing you trouble… Where is it? Who wrote it? Could it be Colonel Mustard, in the lounge, with the spanner? As much as we enjoy whodunits, we have much more effective tools in Git.

3.1 Blame

Git has a built-in tool that helps you blame the culprit of a bug. I’m joking. Even though you can find out who introduced a specific change, the commit hash is more important. Therefore, you have access to the context of the change. Let’s take a look:

287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 35) /** 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 36) * Encode password 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 37) * 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 38) * @param Admin $admin 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 39) */ 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 40) public function updatePassword(Admin $admin) 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 41) { 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 42) if (!$admin->getPlainPassword()) { 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 43) return; 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 44) } 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 45) 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 46) $encoder = $this->encoderFactory->getEncoder($admin); 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 47) 716a200d (Felix Jeb Gladwyn 2017-03-04 13:51:06 +0200 48) $admin->setPassword($encoder->encodePassword( 716a200d (Felix Jeb Gladwyn 2017-03-04 13:51:06 +0200 49) $admin->getPlainPassword(), 716a200d (Felix Jeb Gladwyn 2017-03-04 13:51:06 +0200 50) $admin->getSalt()) 716a200d (Felix Jeb Gladwyn 2017-03-04 13:51:06 +0200 51) ); 287bdd1d (River Albin Bourke 2017-03-04 13:51:06 +0200 52) 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 53) $admin->setPlainPassword(null); 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 54) } 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 55) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 35 ) /** 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 36) * Encode password 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 37) * 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 38) * @param Admin $admin 287bdd1d (River Albin Bourke 2014-09-14 13:13:58 +0300 39) */ 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 40 ) public function updatePassword ( Admin $ admin ) 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 41 ) { 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 42 ) if ( ! $ admin -> getPlainPassword ( ) ) { 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 43 ) return ; 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 44 ) } 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 45 ) 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 46 ) $ encoder = $ this -> encoderFactory -> getEncoder ( $ admin ) ; 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 47 ) 716a200d ( Felix Jeb Gladwyn 2017 - 03 - 04 13 : 51 : 06 + 0200 48 ) $ admin -> setPassword ( $ encoder -> encodePassword ( 716a200d ( Felix Jeb Gladwyn 2017 - 03 - 04 13 : 51 : 06 + 0200 49 ) $ admin -> getPlainPassword ( ) , 716a200d ( Felix Jeb Gladwyn 2017 - 03 - 04 13 : 51 : 06 + 0200 50 ) $ admin -> getSalt ( ) ) 716a200d ( Felix Jeb Gladwyn 2017 - 03 - 04 13 : 51 : 06 + 0200 51 ) ) ; 287bdd1d ( River Albin Bourke 2017 - 03 - 04 13 : 51 : 06 + 0200 52 ) 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 53 ) $ admin -> setPlainPassword ( null ) ; 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 54 ) } 287bdd1d ( River Albin Bourke 2014 - 09 - 14 13 : 13 : 58 + 0300 55 ) }

So you find the commit hash, the author and the timestamp for each line of the specified file. What could happen, though, would be to get the wrong culprit. Maybe someone innocently removed whitespaces or simply moved the code around from other places. So does the real culprit get away? Of course not. We can use the following options to ignore whitespace changes

$ git blame -w 1 $ git blame - w

or to ignore moving text

$ git blame -M 1 $ git blame - M

or to ignore moving text into other files

$ git blame -C 1 $ git blame - C

Depending on the situation you can choose to use one of or combine them.

3.2 Bisect

We discussed above about finding the author of a specific change. At that point, we supposedly know where the bad code is. Now, let’s make it more interesting. We only know that a bug has been introduced, but nothing more. Don’t worry, Git will help us again, with one of its superpowers: git bisect .

You specify a “good” commit, a “bad” one, then Git takes care of the rest. Well, almost. Git runs a binary search within the commit range delimited by the good and the bad commit. The only thing you need to do is to tell Git if the currently presented commit is good or bad. The process continues until the range is narrowed to a single commit, the one introducing the bug. It’s that easy and it looks like this:

git bisect $ git bisect start $ git bisect good 857765e $ git bisect bad HEAD Bisecting: 0 revisions left to test after this (roughly 1 step) [cb826ee495e54365cd153fb0fc4a261de753e7fd] Car - add getters. $ git bisect bad Bisecting: 0 revisions left to test after this (roughly 0 steps) [cd0e1f1e3fa8178f365ea1e8b2d8177d23f98aaa] Car - add setters. $ git bisect good cb826ee495e54365cd153fb0fc4a261de753e7fd is the first bad commit commit cb826ee495e54365cd153fb0fc4a261de753e7fd Author: Sergiu-Ioan Ungur <sergiu@algotech.solutions> Date: Sat Mar 4 17:00:55 2017 +0200 Car - add getters. :100644 100644 1e378a1c2f724280f245f82ca714ad9140fb487b 19932f6432e79e8181a7e00653fab9c0ebe0a2c9 M Car.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ git bisect start $ git bisect good 857765e $ git bisect bad HEAD Bisecting : 0 revisions left to test after this ( roughly 1 step ) [ cb826ee495e54365cd153fb0fc4a261de753e7fd ] Car - add getters . $ git bisect bad Bisecting : 0 revisions left to test after this ( roughly 0 steps ) [ cd0e1f1e3fa8178f365ea1e8b2d8177d23f98aaa ] Car - add setters . $ git bisect good cb826ee495e54365cd153fb0fc4a261de753e7fd is the first bad commit commit cb826ee495e54365cd153fb0fc4a261de753e7fd Author : Sergiu - Ioan Ungur < sergiu @ algotech . solutions > Date : Sat Mar 4 17 : 00 : 55 2017 + 0200 Car - add getters . : 100644 100644 1e378a1c2f724280f245f82ca714ad9140fb487b 19932f6432e79e8181a7e00653fab9c0ebe0a2c9 M Car . php

Don’t forget to initially specify both the good and the bad commits, otherwise Git can’t help you out.

4. Undoing changes

Here at Algotech Solutions, we’ve seen a lot of projects. Inevitably, during the lifecycle of a project, there are cases when changes have to be reverted. Whether you need to revert changes that a certain commit has introduced or more complex ones like a faulty rebasing, there is always a way to get out.

4.1 Commit changes

You can undo commit changes in several ways using:

A “safe” revert that creates a new commit containing the reverted changes:

$ git revert <commit> 1 $ git revert < commit >

A checkout specifying the version that a certain file will be brought to:

$ git checkout <commit> -- <filename> 1 $ git checkout < commit > -- < filename >

An interactive rebasing where you are able to remove unnecessary commits:

$ git rebase --interactive 1 $ git rebase -- interactive

A hard reset that removes all the commits applied on top of a specified one:

$ git reset --hard <commit> 1 $ git reset -- hard < commit >

4.2 Undoing anything

Git Tips and Tricks article, in the “Revert. Undo commit changes” section.

The git reflog command comes to get you out of trouble by reverting all kinds of harmful operations. Most of the times, these are faulty rebases or a branch deletions. You can basically go back in time at any moment during your Git operations history. First let’s see how the history looks like:

git reflog $ git reflog 1234571 HEAD@{0}: checkout: moving from one-branch to another-branch 1234570 HEAD@{1}: merge origin/feature-branch: Fast-forward 1234569 HEAD@{2}: commit: The commit message goes here. 1234568 HEAD@{3}: reset: moving to 2234567 1234567 HEAD@{4}: rebase finished: returning to refs/heads/one-branch 1 2 3 4 5 6 $ git reflog 1234571 HEAD @ { 0 } : checkout : moving from one - branch to another - branch 1234570 HEAD @ { 1 } : merge origin / feature - branch : Fast - forward 1234569 HEAD @ { 2 } : commit : The commit message goes here . 1234568 HEAD @ { 3 } : reset : moving to 2234567 1234567 HEAD @ { 4 } : rebase finished : returning to refs / heads / one - branch

Now all you have to do is to run a checkout in order to move to the desired state of the source code. Further details can be found in Git Tips and Tricks article, “Reflog. Revert operations” section.

5. Aliases

One other tip that is worth mentioning is using aliases for no other reason than writing less. A simple example for a commonly used command is having an alias git co for the command git checkout . For more details about how to set up Git aliases and not only please check our dotaliases repository on GitHub.

Conclusion

There is no such thing as doing it the right way. We all have our own needs and preferences. But at least we can try to help each other out by sharing our knowledge. This is what I’ve done here, by presenting my way of dealing with Git issues. I hope you enjoyed.

Free email updates! Get the latest content first. No spam. Just occasional emails with great engineering posts. Send me great engineering posts