Git has its oddities with unusual commands and flags. I keep learning something new every now and then.

A decent understanding of basic Git concepts is a prerequisite.

show time!

The easiest way to see the diff introduced by a reference (fancy name for commit) is not git diff!

git show REF

It shows details about the commit along with the changes it brings about. Of course, you can control the amount of information thrown at you with the usual options show shares with log: --name-status, --shortstat, --stat, --summary, etc.

show can also give you a file’s clean copy from a commit — very handy:

git show REF:path/to/a/file > file_copy

show is a versatile command that can explain any Git object, not just references.

Man’s search for meaning config 😛

Ever been annoyed by a Git setting you didn’t set; clueless about where it’s coming from?

git config --show-origin  --list

file:/usr/local/etc/gitconfig       credential.helper=osxkeychain
file:/Users/my_user/.gitconfig      user.name=John Q. Public
file:/tmp/Cool_Project/.gitconfig   core.pager=less --quit-if-one-screen --no-init

lists all settings along with the corresponding .gitconfig it’s coming from.

You’re on stage!

git add has some good tricks up its sleeves:

git add .    # stage changes (untracked and tracked)
git add -u   # stage changes (tracked-only)

-n dry runs add without actually staging anything; -v makes add tell what is staged. If you want to stage only parts of a file, pass -p to interactively stage hunks of patch.

If you’re sure of your changes and just want to commit, skipping the intermediate add

git commit -a

This will auto-add all tracked, changed files and commit.

Stuff that matters!

git ls-files [<pathspec>]

lists only tracked files of the repository; it optionally takes a pathspec argument. Quite useful when you’ve to separate the wheat from the chaff in a directory with many untracked files.

Pull another branch

Contrary to popular belief, git pull can be run on a branch that’s not current. Say HEAD is attached to master, you can still pull in changes to my-topic branch from a remote.

git fetch origin my-topic:my-topic

If my-topic and origin/my-topic have diverged, this gets reduced to an ordinary fetch i.e. only origin/my-topic gets updated.

Please mind the dots

git diff A..B = diff (A, B] i.e. the diff includes changes made by B but not by A. Since a diff-ing utility just takes a pair of file sets, with these references we simply denote the repository at different states – after committing A and after committing B. This diffs the repository at two points.

git diff A B    # same as below; only nicer
git diff A..B   # diff commits A and B
git diff A...B  # diff commits ancestor(A, B) and B

git diff A...B = git diff $(git merge-base A B) B i.e. difference between the common ancestor of both references and B. A is usually HEAD and B is commonly a branch head, so this shows work done independently in a branch. Memory aid: triple dots ≈ branch.

However, the meanings feel reversed for git log! 🤦 Also A..B and A B mean the same in diff while not in log! Before reading log’s nuances, remember that log operates on a range of commits while diff only does on two. Gitolite’s nice observation might also help you internalise ... better:

... somehow involves the common ancestor for both diff and log

git log A B    # (1) show A ∪ B commits (till root)
git log A..B   # (2) show B-only commits
git log A...B  # (3) show A-only and B-only commits

log takes a commit and lists all the way to its root, unless an end point is given. This explains (1); for (2) since A wouldn’t be reachable from B it stops at the common ancestor. (3) has ... so it involves the common ancestor; since both A and B can be reached from it, it lists both’s history up-to the ancestor.

Pro Git v2 explains these in git log’s context.

Overloaded checkout

Keep getting confused between the various tasks that the overloaded checkout command performs? It’s used to create new branches, switch branches and restore files from the index or a reference. You don’t have to use it much these days; Git 2.23 added two very useful commands to remedy the situation1:

# create new branch
git switch -c shiny-topic-branch

# switch BRANCH
git switch master

# restore hello from index
git restore hello

# restore hello from commit 1fe35f
git restore --source 1fe35f hello

# restore hello in index from HEAD
git restore --staged hello

# restore hello in both index and working tree from 1fe35f
git restore --WS --source 1fe35f hello

Strings Attached Tags

Annotated tags are tags with an accompanying message that are displayed by git show. Unlike lightweight tags they are unintuitive.

# Lightweight tag
git tag 1.0.0

# Annotated tag
git tag -m "Tag for release after sign off from CI team" 1.0.0

However, the differences go deeper. Annotated tags themselves are separate objects while lightweight objects are just pointers i.e. they point to a commit object. Like commit objects, apart from the message, annotated tags also carry their own tagger and date fields. When you git push --follow-tags, only annotated tags are pushed; git push --tags pushes both. Since they’re separate objects, like all Git objects, they’ve a distinct hash; consequently they can also be tagged! This can bite you when you rename an annotated tag by creating the new tag with the old one.


  1. switch and restore are still experimental as of Git 2.26.2 ↩︎