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.
The easiest way to see the diff introduced by a reference (fancy name for commit) is not
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
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;
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
git commit -a
This will auto-
add all tracked, changed files and
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
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 (
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
A is usually
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 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
...somehow involves the common ancestor for both
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
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.
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.
When getting a remote branch, ignore the popular advise to fetch everything (
git fetch); a better option is to get just what you want; saves a lot of bandwidth:
# fetch just the interesting branch git fetch origin my_topic_branch # checkout locally and also set remote-tracking branch git checkout --track origin/my_topic_branch
Fetch Fiascos? Shallow Repos!
Irrespective of the number of times you yell
git fetch, Git swears that there’s only one branch in a remote! You look at
git branch -r in dismay 😰
Well, you forgot that it’s a shallow repro i.e. cloned with
--single-branch. The caveat lurks in
Further fetches into the resulting repository will only update the remote-tracking branch for the branch this option was used for the initial cloning.
Deepen the shallow repo with
git fetch --unshallow. Further details at an SO post.
restoreare still experimental ↩︎