In lesson 02-06 we learned to move around the history: git log with its formats, its filters by author and date, and searching with -S and -G. That was enough for a single-branch project with a handful of commits.
task-manager is now eight months old, with four active branches, merges, version tags and three people working on it. And the questions the team asks itself have changed in nature:
- What shape does the history have? What has been merged into
mainthis month? - What is in Carla's branch that is not in
main, and the other way round? - Who has contributed what, and how much?
- What are the milestones — tags and merges — without the noise of the intermediate commits?
And there is a second problem, more prosaic but just as real. The invocation from the previous lesson was git blame -w -C --date=short -L :normaliseText:app.js app.js. Nobody types that twice. The second half of this lesson is about that: turning the long commands you use daily into short words, with Git aliases.
Contents
--graph: seeing the shape of the history--first-parent: the mainline, without noise--simplify-by-decoration: the milestones only--mergesand--no-merges- Three-dot ranges and
--left-right - Advanced
--pretty=format:: colours and alignment git shortlog: who has contributed what- Composing queries with what we have learned
- Aliases: what they are and where they live
- Aliases with
!: shell and functions - A set of aliases for day-to-day work
- Risks and good practice with aliases
--graph: seeing the shape of the history
--graph: seeing the shape of the historyBy default git log gives a flat list ordered by date, which in a history with branches is misleading: you cannot see what came from where. --graph draws the DAG (lesson 03-01) with ASCII characters down the left-hand side.
* c8f2a1e (HEAD -> main, origin/main) Merge feature/status-filter |\ | * 4e8b2c9 (feature/status-filter) Add the filter by task status | * 2c6e9a4 Extract the list styles into styles.css |/ * 8d4f2a7 Delegate the list events to the container | * 7a1f5c3 (fix/svg-delete) Use closest() when detecting the delete button |/ * 3f1a8d6 Remove invisible characters when normalising task text * b7e2c4a (tag: v1.0.0) Normalise task text before saving it
The three modifiers are inseparable in practice:
| Option | What it adds |
|---|---|
--graph |
The lines and asterisks that draw the topology |
--oneline |
One commit per line: without it, the graph is unreadable |
--decorate |
The (HEAD -> main, tag: v1.0.0) bits: where the references point |
--all |
All the references, not just the current branch |
How to read the drawing:
- Each
*is a commit. The column it is in indicates its "lane". |\marks a merge commit: two lines come into it.|/marks the point at which two lanes join going backwards: the common ancestor.- The
fix/svg-deletebranch appears hanging off3f1a8d6without having been merged: those are commits that exist only in that branch.
One detail about --decorate: since Git 2.13 it has been on by default when the output goes to a terminal (--decorate=auto), so you often do not need to type it. It can be pinned with:
And variants of --all that avoid the noise of dozens of old remote branches:
git log --graph --oneline --branches # local branches only
git log --graph --oneline --branches --tags # local ones + tags
git log --graph --oneline --remotes=origin # only origin's
git log --graph --oneline main feature/status-filter # only these two
--first-parent: the mainline, without noise
--first-parent: the mainline, without noiseIt has already come up in lesson 06-02 with bisect, and in 05-06 when choosing -m 1. The idea is the same: in a merge commit, the first parent is the branch you were on (normally main) and the second is the one you brought in.
c8f2a1e Merge feature/status-filter 8d4f2a7 Delegate the list events to the container 3f1a8d6 Remove invisible characters when normalising task text b7e2c4a Normalise task text before saving it
Compare it with the --graph from the previous section: 4e8b2c9 and 2c6e9a4 have disappeared, which were the commits inside the merged branch. What is left is main's history at the level of integrations: each line is either a direct commit or "a whole feature came in here".
It is the right view for:
- Drafting a version's release notes.
- Answering "what has gone into
mainthis week?". - Counting how many features were integrated in a period.
# What has gone into main since the last tag
git log --oneline --first-parent v1.0.0..main
# How many integrations in the last month
git log --oneline --first-parent --since=1.month main | wc -l
--simplify-by-decoration: the milestones only
--simplify-by-decoration: the milestones onlyThis option is little known and very useful. It shows only the commits that have a reference pointing at them (a branch, a tag or HEAD), plus the minimum needed to keep the graph connected.
* c8f2a1e (HEAD -> main, origin/main) Merge feature/status-filter |\ | * 4e8b2c9 (feature/status-filter) Add the filter by task status * | 7a1f5c3 (fix/svg-delete) Use closest() when detecting the delete button |/ * b7e2c4a (tag: v1.0.0) Normalise task text before saving it * 1a5c9f3 (tag: v0.9.0) Prepare the testing version
Four hundred commits reduced to five lines: the project's skeleton. It is the first thing worth running when you arrive at an unfamiliar repository, because in ten seconds it gives you the map: which versions exist, which branches are alive and where they were born.
Useful combinations:
# Only the evolution of the versions
git log --graph --oneline --simplify-by-decoration --tags
# With dates, to see the release cadence
git log --simplify-by-decoration --tags --date=short \
--pretty=format:'%C(yellow)%d%C(reset) %ad %s'
--merges and --no-merges
--merges and --no-mergesTwo complementary, straightforward filters:
git log --oneline --merges # ONLY merge commits
git log --oneline --no-merges # all of them EXCEPT the merges| Filter | What it shows | What it is for |
|---|---|---|
--merges |
Only commits with two or more parents | Seeing the history of integrations |
--no-merges |
Only commits of real work | Statistics, release notes, reviews |
--min-parents=N |
Commits with at least N parents | --min-parents=3 finds octopus merges |
--max-parents=1 |
Equivalent to --no-merges |
The same, with different syntax |
--max-parents=0 |
The root commits (with no parents) | Detecting grafted histories |
--no-merges is especially important for statistics: if you count commits per author without it, whoever merges most looks like the most productive person.
# This month's merges, with who did them
git log --merges --since=1.month --pretty=format:'%h %an %s'
# The real work since the last version, for the release notes
git log --no-merges --oneline v1.0.0..HEAD
- Three-dot ranges and
--left-right
--left-rightFrom lesson 02-06 we remember A..B: "the commits reachable from B but not from A". It is asymmetric and answers "what is A missing from B?".
The triple dot A...B is the symmetric difference: the commits that are in one or the other, but not in both. It answers "how have these two branches diverged?".
gitGraph commit id: "base" commit id: "M1" branch feature/status-filter commit id: "F1" commit id: "F2" checkout main commit id: "M2" commit id: "M3"
git log --oneline main..feature/status-filter # F1, F2
git log --oneline feature/status-filter..main # M2, M3
git log --oneline main...feature/status-filter # F1, F2, M2, M3With a plain A...B you cannot tell which commit is on which side. That is what --left-right is for:
> 6f2b9d4 Add the filter by task status > a1e5c93 Extract the list styles into styles.css < e91d4a8 Fix the pending task counter < 7d3a8f4 Update the README with the start-up instructions
| Marker | Meaning |
|---|---|
< |
The commit is on the left-hand side of the range (here, main) |
> |
The commit is on the right-hand side (here, the branch) |
It is the perfect view before merging or rebasing: at a glance you see how far each side has moved. And with the remote:
# How do I stand with respect to my tracking branch? (lesson 04-06)
git log --oneline --left-right --graph HEAD...@{u}
# Just the count, which is often the only thing you want
git rev-list --left-right --count main...feature/status-filterTwo commits ahead on main, two on the branch. That rev-list --count is, incidentally, what lies behind the [ahead 2, behind 2] of git status and git branch -vv.
A useful addition: --cherry-mark marks with = the commits that are equivalent to one on the other side (same change, different hash) — what we saw with git cherry in lesson 05-03:
- Advanced
--pretty=format:: colours and alignment
--pretty=format:: colours and alignmentIn lesson 02-06 we saw the basic --pretty=format: placeholders. Now we come to the two groups that turn a readable output into an excellent one: colours and alignment.
Colours
| Placeholder | Effect |
|---|---|
%C(red), %C(green), %C(yellow), %C(blue), %C(magenta), %C(cyan) |
Text colour |
%C(bold blue) |
Colour with an attribute (bold, dim, ul, reverse) |
%C(reset) |
Goes back to the default colour |
%C(auto) |
Uses the colour Git would use by default for that field |
%C(auto,yellow) |
Yellow, but only if colour is enabled |
%C(always,red) |
Red always, even when redirecting to a file |
%C(auto) is the one to use by default, and there is a specific reason: applied to %d (the decorations), it paints each reference in its canonical colour — HEAD in cyan, local branches in green, remote ones in red, tags in yellow — exactly as git log --decorate does without a custom format. Writing that by hand is impossible.
On top of that, %C(auto) respects the color.ui configuration: if you redirect the output to a file or to grep, the escape codes are not emitted and the file stays clean.
Alignment
When the format has several columns, without alignment it is a visual disaster. The padding placeholders solve it:
| Placeholder | Effect |
|---|---|
%<(N) |
The following fields take up at least N characters, left-aligned |
%<(N,trunc) |
The same, but truncates with .. if it overflows |
%<(N,ltrunc) |
Truncates from the left (..end) |
%<(N,mtrunc) |
Truncates in the middle (beg..end) |
%>(N) |
Right-aligned |
%><(N) |
Centred |
%<|(N) |
Pads up to column N (absolute, not relative) |
The complete format the team uses:
git log --pretty=format:'%C(auto)%h %C(blue)%<(16,trunc)%an%C(reset) %C(green)%<(12)%ar%C(reset) %C(auto)%d%C(reset) %s'c8f2a1e Ana Ferrer 2 days ago (HEAD -> main, origin/main) Merge feature/status-filter 4e8b2c9 Bruno Salas 3 days ago (feature/status-filter) Add the filter by status 2c6e9a4 Carla Vidal 4 days ago Extract the list styles into styles.css 8d4f2a7 Bruno Salas 6 weeks ago Delegate the list events to the container b7e2c4a Carla Vidal 5 months ago (tag: v1.0.0) Normalise task text
Breaking it down piece by piece:
%C(auto)%h→ abbreviated hash, with Git's standard colour for hashes.%C(blue)%<(16,trunc)%an%C(reset)→ author in blue, exactly 16 columns, truncated if it does not fit.%C(green)%<(12)%ar%C(reset)→ relative date ("2 days ago") in green, 12 columns.%C(auto)%d%C(reset)→ the decorations, each in its canonical colour.%s→ the subject, taking up the rest.
The result is tabular, readable and compact. In section 11 we shall turn it into an alias.
A reminder of the most-used placeholders, so you do not have to go back to lesson 02-06:
| Placeholder | Content |
|---|---|
%H / %h |
Full / abbreviated hash |
%an / %ae |
Name / email of the author |
%cn / %ce |
Name / email of the committer |
%ad / %cd |
Authorship / commit date |
%ar / %cr |
The same ones, in relative format |
%s / %b |
Subject / body of the message |
%d / %D |
Decorations with brackets / without them |
%p / %P |
Abbreviated / full hashes of the parents |
%G? |
GPG signature status (G good, N unsigned…) |
%n / %% |
Line break / literal percent sign |
And so as not to repeat the format every time, it can be saved by name in the configuration:
git config --global pretty.table '%C(auto)%h %C(blue)%<(16,trunc)%an%C(reset) %C(green)%<(12)%ar%C(reset) %C(auto)%d%C(reset) %s'
git log --pretty=table
git shortlog: who has contributed what
git shortlog: who has contributed whatgit shortlog groups the commits by author. With no arguments it lists the messages grouped; with -s (summary) and -n (numbered), it gives the sorted count:
Useful options:
| Option | Effect |
|---|---|
-s |
The count only, without the messages |
-n |
Sorts by number of commits, not alphabetically |
-e |
Shows the email alongside the name |
--no-merges |
Excludes merges: you nearly always want this |
-c |
Groups by committer rather than by author |
--since / --until |
Narrows the period |
# Real contributions over the last three months
git shortlog -sn --no-merges --since=3.months
# Who has touched app.js
git shortlog -sn --no-merges -- app.js
# Release notes grouped by person
git shortlog --no-merges v1.0.0..HEADAna Ferrer (12):
Remove invisible characters when normalising task text
Add the pending task counter
...
Bruno Salas (9):
Delegate the list events to the container
...That last output is, literally, a draft set of release notes.
Two important warnings, because shortlog is easily misread:
- The number of commits does not measure productivity. A commit can be a comma or a whole feature. As a performance metric it is worse than useless: it is perverse, because it rewards artificial slicing.
- The same person can show up several times if they have used different names or emails (personal laptop, server, web platform). This is unified with a
.mailmapfile at the root of the repository:
# .mailmap Ana Ferrer <[email protected]> <[email protected]> Bruno Salas <[email protected]> <[email protected]> Carla Vidal <[email protected]>
Git applies it automatically in shortlog, log and blame.
- Composing queries with what we have learned
Everything above combines, and that is where the real power lies. Some examples that answer specific day-to-day questions about task-manager:
# What has gone into main since v1.0.0, with no merge noise,
# with who and when?
git log --no-merges --first-parent v1.0.0..main \
--pretty=format:'%C(auto)%h %C(blue)%<(14,trunc)%an%C(reset) %ad %s' \
--date=short# Which commits touched app.js and mention localStorage in the diff?
# (the -S pickaxe from lesson 02-06 + format)
git log -S "localStorage" --oneline -- app.js# The guilty commit bisect found, in its graph context
git log --graph --oneline --all --ancestry-path 8d4f2a7..main--ancestry-path restricts the range to the commits that are on the path between the two ends, which is exactly what you want for answering "how did that commit get into main?".
# The evolution of a specific function (log -L from lesson 06-03)
# limited to the last three months
git log -L :normaliseText:app.js --since=3.months --oneline# How many lines has each person changed? (with all the caveats
# from section 7 about metrics)
git log --no-merges --numstat --pretty=format:'%an' | \
awk 'NF==1 {author=$0} NF==3 {plus[author]+=$1; minus[author]+=$2}
END {for (a in plus) printf "%-20s +%-8d -%d\n", a, plus[a], minus[a]}'And a comparison of the three views of the history we have seen, to have them together:
| I want to see… | Command |
|---|---|
| The project's complete shape | git log --graph --oneline --all |
main's history at the level of integrations |
git log --oneline --first-parent main |
| The milestones only (tags and branches) | git log --graph --oneline --simplify-by-decoration --all |
| The real work, without merges | git log --oneline --no-merges |
| How two branches have diverged | git log --oneline --left-right main...other |
| Who has contributed how much | git shortlog -sn --no-merges |
Having got this far, the problem is obvious: none of these commands gets typed by hand twice.
- Aliases: what they are and where they live
A Git alias is a short name for a long command. It is defined with git config:
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commitFrom then on, git st is git status. Git resolves the alias before running and passes it the extra arguments:
Where they live. In the .gitconfig corresponding to the level you choose (lesson 01-05), under the [alias] section:
# ~/.gitconfig
[user]
name = Carla Vidal
email = [email protected]
[alias]
st = status
co = checkout
br = branch
ci = commitThey can be edited by hand in the file — which is more convenient for the long ones — or with git config. And queried:
git config --get-regexp '^alias\.' # list them all
git config --get alias.lg # see a specific one
git config --global --unset alias.co # delete oneThe three levels work exactly as for the rest of the configuration: --system, --global and local. Personal aliases go in --global; an alias specific to a project, in the local one. And remember that configuration is not cloned: the aliases are yours, not the repository's.
Two rules that avoid surprises:
- An alias cannot override an existing command. If you define
alias.status, Git will ignore the alias and run the real command. It is a deliberate protection. - Aliases can be chained, but with care: an alias that invokes another one works, and a recursive alias hangs Git.
- Aliases with
!: shell and functions
!: shell and functionsAn ordinary alias can only start with a Git subcommand. For everything else — chaining commands, using pipes, invoking external programs, placing the arguments somewhere other than at the end — you prefix !, which tells Git: "this is a shell command, run it as it is".
git config --global alias.cleanup '!git branch --merged main | grep -v main | xargs -r git branch -d'Three fundamental things about aliases with !:
- They run from the ROOT of the repository
This is the main risk and the number-one source of surprises. Git does a cd to the root of the working tree before running the alias. So if you are in ~/projects/task-manager/components/ and you run an alias with !ls, you will see the content of ~/projects/task-manager/, not that of the directory you are in.
Git provides a variable to mitigate it:
GIT_PREFIX contains the path relative to the root of the directory you were in when you invoked the alias (empty if you were already at the root). Any alias with ! that works with relative paths must take it into account:
- The arguments are appended at the end… unless you use a function
Git sticks the arguments you type at the end of the alias's command. That is a problem when you need them in the middle:
# WRONG: 'git find hello' runs 'git log --oneline | grep hello'
git config --global alias.find '!git log --oneline | grep'In this case it works by coincidence. But if the argument goes in the middle, it does not:
# WRONG: 'git since main' runs 'git log --oneline ..HEAD main'
git config --global alias.since '!git log --oneline ..HEAD'The canonical solution is to define a shell function and call it:
The pattern '!f() { ...; }; f' reads like this: define a function called f, and then invoke it. The arguments Git sticks on the end become f's arguments. It is the standard idiom and you will see it in every .gitconfig in the world.
Use "$@" to pass all the arguments and "$1", "$2" for specific positions. And always quote them: without quotes, a branch name with odd characters breaks the alias.
- They run with
sh, not with bash
sh, not with bashGit uses /bin/sh. On many systems that is dash, not bash, and the bash extensions ([[ ]], arrays, mapfile) do not work. Write aliases in POSIX shell or invoke the interpreter explicitly:
For anything longer than three lines, the right answer is not an alias but a script on the PATH. Git has a lovely convention for that: any executable called git-<something> that is on the PATH can be invoked as git <something>:
# ~/bin/git-summary (with chmod +x)
#!/usr/bin/env bash
set -euo pipefail
echo "=== Current branch ==="
git branch --show-current
echo
echo "=== Last 10 commits ==="
git log --oneline -10
echo
echo "=== Status ==="
git status --shortWithout configuring anything. It is the right route for complex logic, and as a bonus the script can be version-controlled and shared.
- A set of aliases for day-to-day work
These are the ones that genuinely earn their place. Starting with the absolute classic:
git config --global alias.lg "log --graph --abbrev-commit --decorate --all --pretty=format:'%C(auto)%h%C(reset) -%C(auto)%d%C(reset) %s %C(green)(%ar)%C(reset) %C(blue)<%an>%C(reset)'"* c8f2a1e - (HEAD -> main, origin/main) Merge feature/status-filter (2 days ago) <Ana Ferrer> |\ | * 4e8b2c9 - (feature/status-filter) Add the filter by status (3 days ago) <Bruno Salas> | * 2c6e9a4 - Extract the list styles into styles.css (4 days ago) <Carla Vidal> |/ * 8d4f2a7 - Delegate the list events to the container (6 weeks ago) <Bruno Salas> * b7e2c4a - (tag: v1.0.0) Normalise task text (5 months ago) <Carla Vidal>
lg is probably the most copied alias in the history of Git, and with good reason: it replaces a 180-character command that nobody remembers.
The complete table of the recommended set:
| Alias | Definition | What for |
|---|---|---|
st |
status --short --branch |
Compact status with the branch and its tracking |
lg |
The one above | The complete graph, coloured |
lgd |
log --graph --oneline --decorate |
The same but only the current branch |
last |
log -1 HEAD --stat |
The last commit with its files |
recent |
log --oneline -20 |
The last 20, nothing more |
milestones |
log --graph --oneline --simplify-by-decoration --all |
The project's skeleton |
integrated |
log --oneline --first-parent |
main's mainline |
who |
shortlog -sn --no-merges |
Contributions per person |
praise |
blame -w -C --date=short |
The blame from lesson 06-03, properly configured |
history |
!f() { git log -L :"$1":"$2"; }; f |
git history normaliseText app.js |
pending |
!git log --oneline @{u}..HEAD |
What I have not pushed |
incoming |
!git fetch -q && git log --oneline HEAD..@{u} |
What is on the remote and not fetched |
divergence |
!f() { git log --oneline --left-right "$1"...HEAD; }; f |
How I have diverged from a branch |
wip |
!git add -A && git commit -m 'WIP: work in progress' --no-verify |
Quick save before switching context |
undo |
reset HEAD~1 --mixed |
Undoes the last commit keeping the changes |
amend |
commit --amend --no-edit |
Adds what is staged to the last commit |
cleanup |
!git branch --merged main | grep -vE '^\*|main' | xargs -r git branch -d |
Deletes the already-merged branches |
branches |
branch -vv --sort=-committerdate |
Branches by recent activity, with tracking |
saved |
stash list --pretty=format:'%C(yellow)%gd%C(reset) %s' |
The stash stack, readable |
tags |
tag -l --sort=-v:refname --format='%(refname:short) %(creatordate:short) %(subject)' |
Tags by descending version |
alias |
!git config --get-regexp '^alias\.' | sed 's/^alias\.//' |
Remembering which aliases I have |
That last one is more useful than it looks: after six months nobody remembers all their aliases.
To install them in one go, it is more convenient to edit the file than to fire off twenty git configs:
[alias]
st = status --short --branch
lg = log --graph --abbrev-commit --decorate --all --pretty=format:'%C(auto)%h%C(reset) -%C(auto)%d%C(reset) %s %C(green)(%ar)%C(reset) %C(blue)<%an>%C(reset)'
milestones = log --graph --oneline --simplify-by-decoration --all
integrated = log --oneline --first-parent
who = shortlog -sn --no-merges
praise = blame -w -C --date=short
history = "!f() { git log -L :\"$1\":\"$2\"; }; f"
pending = "!git log --oneline @{u}..HEAD"
divergence = "!f() { git log --oneline --left-right \"$1\"...HEAD; }; f"
undo = reset HEAD~1 --mixed
amend = commit --amend --no-edit
branches = branch -vv --sort=-committerdate
alias = "!git config --get-regexp '^alias\\.' | sed 's/^alias\\.//'"Note the escaping: in the file, values with quotes or backslashes go inside double quotes and with the backslashes doubled. It is the most annoying part of aliases with !, and the reason it is worth testing them right after writing them.
And this connects with the earlier modules: aliases are not just for log. pending uses @{u} from lesson 04-06; amend is the --amend from 02-04; cleanup is the branch management from 03-06; praise is the blame from 06-03. An alias is the way of fixing into one gesture what you have learned to do properly, so that you do not have to remember the seven options every time.
- Risks and good practice with aliases
Risk 1: the directory it runs from. Already seen: aliases with ! run from the root of the repository. Use GIT_PREFIX if the alias works with paths.
Risk 2: destructive aliases with short names. A git p that does push --force is an accident waiting to happen. Rule: the more destructive, the longer the name. And in the case of push --force, always use --force-with-lease (lesson 04-05).
Risk 3: dependence on your machine. Your aliases are not on the laptop of the colleague you ask for help, nor on the server. It is worth knowing the real command behind each one.
Risk 4: sh is not bash. The bash extensions fail silently or with odd errors.
Risk 5: aliases that hide effects. A git sync that does fetch + rebase + push is fine until the day there are conflicts and you cannot work out where the process has stopped.
Good practice:
- Let the name say what it does.
pendingis better thanp. - Version-control your
.gitconfig. Many people keep their dotfiles in a repository of their own: aliases are valuable configuration and recreating them is a chore. - Add aliases little by little. When you notice you are typing the same thing for the third time, there you have a candidate. Copying a list of forty aliases off the internet guarantees you will use none of them.
- Test the alias as soon as you create it, especially the ones with
!and quotes. - For complex logic,
git-<name>on thePATH, not a three-line alias.
Common Mistakes and Tips
Mistake 1: --graph without --oneline. The graph with full messages is unreadable. They always go together.
Mistake 2: forgetting --all and believing the branch does not exist. git log --graph only shows the current branch. Without --all, you do not see the others.
Mistake 3: confusing .. with .... A..B is asymmetric ("what A is missing"); A...B is symmetric ("how they have diverged"). With --left-right the ambiguity disappears.
Mistake 4: counting commits with shortlog without --no-merges. Whoever integrates most looks like the one who produces most.
Mistake 5: using the commit count as a performance metric. It is a bad metric and it creates perverse incentives.
Mistake 6: an alias with ! that assumes the current directory. It runs from the root. GIT_PREFIX.
Mistake 7: arguments in the middle of an alias without using a function. Git sticks them at the end. The pattern '!f() { ...; }; f' is the solution.
Mistake 8: badly done escaping in the .gitconfig. Double quotes around the value and doubled backslashes. Test the alias immediately.
Tip 1: %C(auto) by default in your formats. It colours the decorations correctly and respects both the configuration and redirections.
Tip 2: save formats by name. git config --global pretty.table '...' and then git log --pretty=table.
Tip 3: --simplify-by-decoration when you arrive at a new repository. Ten seconds for the complete map.
Tip 4: git rev-list --count A...B for the pure count. When you only want to know how much, not what.
Tip 5: a .mailmap as soon as somebody shows up duplicated. It is a three-line file that fixes shortlog, log and blame all at once.
Exercises
Exercise 1: reading the shape of a history
Create a repository with this topology: four commits on main, a feature/a branch with two commits merged with --no-ff, a feature/b branch with two commits not merged, and a v1.0 tag on main's second commit. Then:
- Show the complete graph with
--graph --oneline --decorate --all. - Show only the mainline with
--first-parent. - Show only the milestones with
--simplify-by-decoration --all. - Show only the merges, and then only the commits that are not merges.
- Explain which commits disappear in each view and why.
Exercise 2: comparing two branches
On the same repository:
- Add two more commits to
mainand two more tofeature/b. - Use
main..feature/bandfeature/b..main, and explain the difference. - Use
main...feature/bwith--left-rightand identify each side. - Get the count with
git rev-list --left-right --count. - Create a
--prettyformat with colours and alignment that shows hash, author (14 truncated columns), short date and subject.
Exercise 3: building your aliases
- Create the
lg,who,pendingandpraisealiases from the table in section 11. - Create a
historyalias that accepts two arguments (function and file) and runsgit log -L. Test it. - Create an alias that demonstrates the
GIT_PREFIXproblem: one that just runsls, and another that does it properly. Run them from a subdirectory and compare. - List all your aliases with an alias.
Solutions
Solution 1:
mkdir /tmp/practice-log && cd /tmp/practice-log
git init -b main
for i in 1 2; do echo "main $i" >> main.txt; git add .; git commit -q -m "Commit main $i"; done
git tag v1.0
git switch -qc feature/a
echo "a1" > a.txt && git add . && git commit -q -m "Feature A part 1"
echo "a2" >> a.txt && git commit -qam "Feature A part 2"
git switch -q main
echo "main 3" >> main.txt && git commit -qam "Commit main 3"
git merge -q --no-ff feature/a -m "Merge feature/a"
echo "main 4" >> main.txt && git commit -qam "Commit main 4"
git switch -qc feature/b v1.0
echo "b1" > b.txt && git add . && git commit -q -m "Feature B part 1"
echo "b2" >> b.txt && git commit -qam "Feature B part 2"
git switch -q main* 9c4e7b2 (HEAD -> main) Commit main 4 * 5f1d8a3 Merge feature/a |\ | * 2a8c6f9 (feature/a) Feature A part 2 | * 7d3e1b5 Feature A part 1 * | 4b9f2c8 Commit main 3 |/ | * 8e5a3d7 (feature/b) Feature B part 2 | * 1c7b4f9 Feature B part 1 |/ * 3a6d9e2 (tag: v1.0) Commit main 2 * 6f2c8b1 Commit main 1
9c4e7b2 Commit main 4 5f1d8a3 Merge feature/a 4b9f2c8 Commit main 3 3a6d9e2 Commit main 2 6f2c8b1 Commit main 1
2a8c6f9 and 7d3e1b5 have disappeared: they are the commits inside feature/a, which are only reached through the merge's second parent.
* 9c4e7b2 (HEAD -> main) Commit main 4 | * 8e5a3d7 (feature/b) Feature B part 2 |/ | * 2a8c6f9 (feature/a) Feature A part 2 |/ * 3a6d9e2 (tag: v1.0) Commit main 2
Only the commits with a reference and the minimum needed to connect them: the skeleton.
9c4e7b2 Commit main 4 2a8c6f9 Feature A part 2 7d3e1b5 Feature A part 1 4b9f2c8 Commit main 3 3a6d9e2 Commit main 2 6f2c8b1 Commit main 1
Solution 2:
echo "main 5" >> main.txt && git commit -qam "Commit main 5"
echo "main 6" >> main.txt && git commit -qam "Commit main 6"
git switch -q feature/b
echo "b3" >> b.txt && git commit -qam "Feature B part 3"
echo "b4" >> b.txt && git commit -qam "Feature B part 4"
git switch -q mainWhat the branch has and main does not: what would come in on merging.
7f3c2e8 Commit main 6 2d9b5a1 Commit main 5 9c4e7b2 Commit main 4 5f1d8a3 Merge feature/a 2a8c6f9 Feature A part 2 7d3e1b5 Feature A part 1 4b9f2c8 Commit main 3
What the branch is missing from main: what it would bring in on rebasing or merging main into it.
< 7f3c2e8 Commit main 6 < 2d9b5a1 Commit main 5 < 9c4e7b2 Commit main 4 < 5f1d8a3 Merge feature/a < 2a8c6f9 Feature A part 2 < 7d3e1b5 Feature A part 1 < 4b9f2c8 Commit main 3 > d4a8f2c Feature B part 4 > b1e6c9f Feature B part 3 > 8e5a3d7 Feature B part 2 > 1c7b4f9 Feature B part 1
git log --pretty=format:'%C(auto)%h %C(blue)%<(14,trunc)%an%C(reset) %C(green)%ad%C(reset) %s' --date=short -8Solution 3:
git config --global alias.lg "log --graph --abbrev-commit --decorate --all --pretty=format:'%C(auto)%h%C(reset) -%C(auto)%d%C(reset) %s %C(green)(%ar)%C(reset) %C(blue)<%an>%C(reset)'"
git config --global alias.who "shortlog -sn --no-merges"
git config --global alias.pending "!git log --oneline @{u}..HEAD"
git config --global alias.praise "blame -w -C --date=short"
git config --global alias.history '!f() { git log -L :"$1":"$2"; }; f'
git config --global alias.alias "!git config --get-regexp '^alias\\.' | sed 's/^alias\\.//'"The alias with arguments:
cat > functions.js <<'END'
function greet(name) {
return "Hello " + name;
}
END
git add . && git commit -q -m "Add the greeting function"
sed -i 's/"Hello "/"Hello there, "/' functions.js
git commit -qam "Make the greeting friendlier"
git history greet functions.jsThe GIT_PREFIX problem:
mkdir -p components && echo "x" > components/button.js
git add . && git commit -q -m "Add the button component"
git config --global alias.bad '!ls'
git config --global alias.good '!f() { cd "${GIT_PREFIX:-.}" && ls; }; f'
cd components
git badIt has listed the root of the repository, not the directory we are in.
With GIT_PREFIX, the alias respects the directory it was invoked from.
alias !git config --get-regexp '^alias\.' | sed 's/^alias\.//'
bad !ls
good !f() { cd "${GIT_PREFIX:-.}" && ls; }; f
history !f() { git log -L :"$1":"$2"; }; f
lg log --graph --abbrev-commit --decorate --all --pretty=format:...
pending !git log --oneline @{u}..HEAD
praise blame -w -C --date=short
who shortlog -sn --no-mergesConclusion
This lesson has turned git log from a listing into a querying tool, and has removed the friction of using it. The essentials:
--graph --oneline --decorate --allis the canonical view of the topology. All four go together.--first-parentgivesmain's history at the level of integrations, without the insides of the merged branches. It is the same "first parent" asrevert's-m 1andbisect --first-parent.--simplify-by-decorationreduces the history to its milestones: the first thing to run in an unfamiliar repository.--merges/--no-mergesseparate integrations from real work;--no-mergesis all but mandatory in any statistic.A...Bwith--left-rightanswers "how have these two branches diverged?", with<and>marking each side.git rev-list --left-right --countgives just the count.--pretty=format:with%C(auto)and the alignment placeholders%<(N,trunc)produces tabular, coloured output; save them by name inpretty.<name>.git shortlog -sn --no-mergessummarises the contributions, with.mailmapto unify identities — and with the warning that counting commits measures nothing useful.- Aliases live in the
[alias]section of the.gitconfigat whichever level you choose. The simple ones are a name for a subcommand; the ones starting with!are shell, with three cautions: they run from the root (useGIT_PREFIX), the arguments are stuck on the end (use the pattern'!f() { ...; }; f') and they run undersh, notbash. - For long logic, an executable
git-<name>on thePATHis better than an alias. - And the underlying idea: an alias fixes into one gesture what you have learned to do properly.
praise,pending,milestonesorcleanupare the previous lessons condensed into a single word.
The team now knows how to interrogate its history comfortably. But task-manager is about to stop being a single repository: the buttons, dialogs and form fields that Ana has gradually been extracting have turned into an internal library, ui-components, which lives at git.example.com/team/ui-components.git and which two other company projects also want to use.
The question is how a project is composed out of several repositories without copying and pasting code, and without losing the traceability of which version of the library each version of the application uses. Git's native answer is submodules, and that is lesson 06-05: Git Submodules.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
