A few git tricks

git is the most used version-control tool today.
Although having a learning curve, the command line will always be the most powerful way to use it.
Here I want to share a few git features and conclusions that make my experience with git really great.

1. Interactive mode

Use git add -p to be guided through all your changes and select which ones you want to add.
It does not work with untracked files though, only modified.
The -p flag also works with git reset to unstage, git checkout to discard, and git stash to partially stash.
An unknown, yet in my opinion essential git feature.

2. Inspecting changes on a branch

Seeing the changes on my branch

How to show the changes of my branch, like in Github PR view ?
You can use the 3 dot comparison to display only how you branch deviates from the common parent of your branch and master.

git diff origin/master...HEAD

Seeing the changes on my branch commit-by-commit

git log -p --reverse HEAD ^origin/master

3. Understanding how branches are related

git log --graph

I recommend setting this alias: git alias logg=log --oneline --graph.
Now, try it with your feature branch based on a previous ref of master: git logg origin/master feature3.

* b6c4aea (HEAD, origin/master) feat: add frontend
| * fa165b8 (feature3) fix foo
|/
* 12ea3bc feat: create bar
* 05c7cb7 feat: create foo

You can even specify multiple branches with git logg origin/main feature-branch feature-branch2, or see all branches with git logg --branches.
This is especially handy with multiple levels of branches based on other branches.

git cherry

If you did a few cherry-picks here and there, you may end up with branches that are similar but different.
To compare them, git cherry shows a list of differing commits.

4. Advanced rebasing

Rebasing from one base to another

You can rebase commits from one parent onto another, with the --onto flag.

git rebase <source-upstream> --onto <target-upstream>

Useful especially to rebase a feature branch on top of another feature branch which got squash-merged.

Patching previous commits

Although there are some fair arguments against rewriting history in git, very often, you just want to make your log of changes understandable for your reviewers, and avoid redundant steps.

12ea3bc (HEAD -> my-branch) feat: create bar
05c7cb7 feat: create foo
05c7cb7 (origin/master) initial commit

Let's say you made a mistake on create foo.
To edit it, you can create fixup or squash commits.
Once your code corrected, do git commit --fixup 05c7cb7 (the value of --fixup being the hash of the commit that is being fixed).
It will create a commit named fixup! feat: create foo, which carry a special meaning.

When doing the rebase,

git rebase -i --autosquash origin/master

...the editor will immediately show the following rebase instructions.

pick 05c7cb7 feat: create foo
fixup 957113f fixup! feat: create foo
pick 12ea3bc feat: create bar

Which are exactly what we want, and it's ready to be applied. The fixup! commit we created is identified as fixup next to its target commit.

This technique is quite useful, but requires some care, and is not always worth it.

5. Merge strategy: merge commit vs squash

Squash for all ephemeral branches, merge commits for branches which will keep on existing.
A squash makes the history log simpler/cleaner on the main branch, but does not have any notion of relationship with the source branch.
Once squashed merged, the two merged branch and the main branch are in conflict.

6. git stash

Stashing is for temporarily putting your changes in a drawer.
You can then restore them anytime.
As said below, it's incredibly useful to do partial interactive stash with git stash -p.

You can list stashes with git stash list.
And perform the following operations:

  • git stash show -p: show the detail of a stash
  • git stash apply apply the stash on your working tree but leave it existing
  • git stash pop: apply the stash and remove it
  • git stash drop: remove the stash

You can specify the stash number, for example git stash show -p 3.
If none is provided, 0 is assumed, i.e. the latest stash.

7. Using the CLI

You're missing out if you use the default basic shell.
I like using ohmyzsh. It has tons of features I don't use, but what I like is:

  • history search (with up arrow) filtered by the beginning of input
  • git aliases
    • gst: git status
    • gf: git fetch
    • gco: git checkout
    • gd: git diff
    • gp: git push
    • grb: git rebase
    • glo: git log
    • and many more...
      I did not use them in this article for better readability.

Using the pager

A pager is a basic terminal tool used to browse through long content in a more user-friendly manner.
The default pager on most systems is less, and more.
You'll use it very often,
A few very useful features are:

  • Q to quit :)
  • Ctrl+D to go down half a screen, and Ctrl+U to go up.
  • /foo searches foo in the displayed content. Use n and N to go to next and previous instance.

Knowing how to effectively use it is great for productivity. Many of them are similar to vim, of which I also recommend to learn the basics.

8. Avoiding local branches

If you're using Github or another system for managing git repositories, chances are you use a workflow with a master branch, branches, and pull-requests.
In that case, the master branch is a remote branch, that you'll never edit directly locally.
Which means, you should not have a local master branch.
You can delete your local branch with git branch -d master.
And then only use origin/master.

Which approach is simpler ?

git checkout master
git pull
git checkout my-feature-branch
git rebase master
git fetch
git rebase origin/master

It just makes much more sense to use remote branches when possible. You can update all of them anytime with just git fetch.

Why ?

The first time you checkout a remote branch using git checkout some-branch, you are greeted with this message.

branch 'some-branch' set up to track 'origin/some-branch'.
Switched to a new branch 'some-branch'

A local branch is created from the remote branch. It is on your system, and under your responsibility. You have to keep it up-to-date, and delete it when you don't need it anymore.

Local branches exist so that their state can differ from their remote state.
In other words, they are useful only when you modify a branch.

Removing branches of which remote branches have been removed

By default, git doesn't remove remote branches even when they have been deleted remotely (for example in case of a merged PR).
To remove all removed remote branches, You need to add this option to git fetch:

 -p, --prune
        After fetching, remove any remote-tracking branches which no longer exist on the remote.

Then, you'll see with git branch -v that your local branches are still there, but are marked as [gone]. To remove them, you can use this script:

git branch -D $(git for-each-ref --format='%(if:equals=[gone])%(upstream:track)%(then)%(refname:short)%(end)' refs/heads)

Conclusion

Git plays a central part in development workflows.
Don't stop at the basics, and feel free to explore and find ways to optimize the way you use it.
Mastering the tools you use daily is worth the investment.