GIT-SHORTLOG(1)

๐Ÿ’– GIT-SHORTLOG(1) โ€” Git Manual glow-up ๐Ÿ’–

Okay bestie, this is the Git Manual page for git-shortlog, and it's literally the tool that summarizes git log output. Group-chat energy: on. Facts: also on. ๐Ÿซถ


โœจ NAME โœจ

git-shortlog โ€” summarize git log output. It's giving release-notes energy, no cap. ๐Ÿ’…


๐ŸŒธ SYNOPSIS ๐ŸŒธ

git shortlog [<options>] [<revision-range>] [[--] <path>...]
git log --pretty=short | git shortlog [<options>]

๐Ÿ’… DESCRIPTION ๐Ÿ’…

So git-shortlog takes git log output and turns it into a tidy summary that's suitable for inclusion in release announcements. Each commit gets grouped by author and title. Iconic behavior, honestly. โœจ

Also, [PATCH] gets stripped from the commit description. Not me loving a little cleanup. ๐ŸŽ€

If no revisions are passed on the command line and either standard input is not a terminal or there is no current branch, git shortlog will output a summary of the log read from standard input, without reference to the current repository. Yes bestie, it can absolutely vibe on stdin alone. ๐Ÿฆ‹


๐Ÿ”ฅ OPTIONS ๐Ÿ”ฅ

  • -n, --numbered โ€” sort the output by the number of commits per author instead of alphabetical author order. Main character sorting, periodt. ๐Ÿ‘‘
  • -s, --summary โ€” suppress the commit description and give you a commit count summary only. Short, sweet, slay. ๐Ÿ’–
  • -e, --email โ€” show the email address of each author. Doxxing? No. Release-notes accuracy? Yes. ๐ŸŒธ
  • --format[=<format>] โ€” instead of the commit subject, use some other info to describe each commit. <format> can be any string accepted by the --format option of git log, such as * [%h] %s. (Peep the PRETTY FORMATS section of git-log(1).) Each pretty-printed commit will be rewrapped before it is shown. Fancy formatting, but make it readable. ๐Ÿ’…
  • --date=<format> โ€” show dates formatted according to the given date string. (See the --date option in the Commit Formatting section of git-log(1).) Useful with --group=format:<format>. Date glow-up unlocked. ๐Ÿ—“๏ธโœจ
  • --group=<type> โ€” group commits based on <type>. If no --group option is specified, the default is author. <type> is one of:
    • author โ€” commits are grouped by author. ๐Ÿ‘‘
    • committer โ€” commits are grouped by committer (the same as -c). ๐Ÿซถ
    • trailer:<field> โ€” <field> is interpreted as a case-insensitive commit message trailer (see git-interpret-trailers(1)). For example, if your project uses Reviewed-by trailers, you might want to see who has been reviewing with git shortlog -ns --group=trailer:reviewed-by. Note that commits that do not include the trailer will not be counted. Likewise, commits with multiple trailers (e.g., multiple signoffs) may be counted more than once (but only once per unique trailer value in that commit). Shortlog will attempt to parse each trailer value as a name <email> identity. If successful, the mailmap is applied and the email is omitted unless the --email option is specified. If the value cannot be parsed as an identity, it will be taken literally and completely. Trailer girlies, this one's for you. ๐ŸŽ€
    • format:<format> โ€” any string accepted by the --format option of git log. (See the PRETTY FORMATS section of git-log(1).) ๐ŸŒˆ

If --group is specified multiple times, commits are counted under each value (but again, only once per unique value in that commit). For example, git shortlog --group=author --group=trailer:co-authored-by counts both authors and co-authors. Double-counting said no; unique-value counting said yes. ๐Ÿ’€

  • -c, --committer โ€” this is an alias for --group=committer. Shortcut slay. โญ
  • -w[<width>[,<indent1>[,<indent2>]]] โ€” linewrap the output by wrapping each line at width. The first line of each entry is indented by indent1 spaces, and the second and subsequent lines are indented by indent2 spaces. width, indent1, and indent2 default to 76, 6 and 9 respectively. If width is 0 (zero) then indent the lines of the output without wrapping them. Wrapping but make it cute. ๐ŸŽ
  • <revision-range> โ€” show only commits in the specified revision range. When no <revision-range> is specified, it defaults to HEAD (i.e. the whole history leading to the current commit). origin..HEAD specifies all the commits reachable from the current commit (i.e. HEAD), but not from origin. For a complete list of ways to spell <revision-range>, see the Specifying Ranges section of gitrevisions(7). Range vibes, but precise. ๐Ÿ”
  • [--] <path>... โ€” consider only commits that are enough to explain how the files that match the specified paths came to be. Paths may need to be prefixed with -- to separate them from options or the revision range, when confusion arises. Path era, but respectful. ๐Ÿ›ฃ๏ธ

๐Ÿซถ Commit Limiting ๐Ÿซถ

Besides specifying a range of commits that should be listed using the special notations explained in the description, additional commit limiting may be applied. Yes bestie, there are levels to this. ๐Ÿ”ฅ

Using more options generally further limits the output (e.g. --since=<date1> limits to commits newer than <date1>, and using it with --grep=<pattern> further limits to commits whose log message has a line that matches <pattern>), unless otherwise noted. Stack those filters, but know what they do. ๐Ÿ’…

Note that these are applied before commit ordering and formatting options, such as --reverse. Order of operations said hello. ๐Ÿ‘‘

  • -<number>, -n <number>, --max-count=<number> โ€” limit the output to the first <number> commits that would be shown. First <number>, periodt. ๐Ÿฅ‡
  • --max-count-oldest=<number> โ€” limit the output to the last <number> commits that would be shown. Last <number>, but make it intentional. ๐Ÿฅ€
  • --skip=<number> โ€” skip <number> commits before starting to show the commit output. Not me skipping the intro. ๐Ÿ™ˆ
  • --since=<date>, --after=<date> โ€” show commits more recent than <date>. As a special case, today means the last midnight. Newer-than filter, slay. ๐ŸŒ…
  • --since-as-filter=<date> โ€” show all commits more recent than <date>. This visits all commits in the range, rather than stopping at the first commit which is older than <date>. It's a filter, not a stop sign. ๐Ÿ›‘โžก๏ธโœจ
  • --until=<date>, --before=<date> โ€” show commits older than <date>. Older-than filter, but make it fashion. ๐Ÿ•ฐ๏ธ
  • --author=<pattern>, --committer=<pattern> โ€” limit the commits output to ones with author/committer header lines that match the <pattern> regular expression. With more than one --author=<pattern>, commits whose author matches any of the <pattern> are chosen (similarly for multiple --committer=<pattern>). Author/committer vibe check. ๐Ÿ•ต๏ธโ€โ™€๏ธ
  • --grep-reflog=<pattern> โ€” limit the commits output to ones with reflog entries that match the <pattern> regular expression. With more than one --grep-reflog, commits whose reflog message matches any of the given patterns are chosen. It is an error to use this option unless --walk-reflogs is in use. Reflog gremlins, beware. ๐Ÿ’€
  • --grep=<pattern> โ€” limit the commits output to ones with a log message that matches the <pattern> regular expression. With more than one --grep=<pattern>, commits whose message matches any of the <pattern> are chosen (but see --all-match). When --notes is in effect, the message from the notes is matched as if it were part of the log message. Message matching, but make it thorough. ๐Ÿ”Ž
  • --all-match โ€” limit the commits output to ones that match all given --grep, instead of ones that match at least one. All the greps, not just any grep. ๐Ÿ’ฏ
  • --invert-grep โ€” limit the commits output to ones with a log message that do not match the <pattern> specified with --grep=<pattern>. Flip the grep, bestie. ๐Ÿ”„
  • -i, --regexp-ignore-case โ€” match the regular expression limiting patterns without regard to letter case. Case? Never heard of her. ๐Ÿ™ˆ
  • --basic-regexp โ€” consider the limiting patterns to be basic regular expressions; this is the default. Basic but iconic. ๐ŸŽ€
  • -E, --extended-regexp โ€” consider the limiting patterns to be extended regular expressions instead of the default basic regular expressions. Extended regex, extended slay. โœจ
  • -F, --fixed-strings โ€” consider the limiting patterns to be fixed strings (don't interpret pattern as a regular expression). Just literal strings, no regex drama. ๐Ÿงต
  • -P, --perl-regexp โ€” consider the limiting patterns to be Perl-compatible regular expressions. Support for these types of regular expressions is an optional compile-time dependency. If Git wasn't compiled with support for them, providing this option will cause it to die. Perl regex girlies, check your build first. ๐Ÿ’€
  • --remove-empty โ€” stop when a given path disappears from the tree. Empty path says bye, and so do we. ๐Ÿ‘‹
  • --merges โ€” print only merge commits. This is exactly the same as --min-parents=2. Merge main character energy. ๐Ÿ”€
  • --no-merges โ€” do not print commits with more than one parent. This is exactly the same as --max-parents=1. No merge chaos here. ๐Ÿšซ
  • --min-parents=<number>, --max-parents=<number>, --no-min-parents, --no-max-parents โ€” show only commits which have at least (or at most) that many parent commits. In particular, --max-parents=1 is the same as --no-merges, --min-parents=2 is the same as --merges. --max-parents=0 gives all root commits and --min-parents=3 all octopus merges. --no-min-parents and --no-max-parents reset these limits (to no limit) again. Equivalent forms are --min-parents=0 (any commit has 0 or more parents) and --max-parents=-1 (negative numbers denote no upper limit). Parent-count math, but make it dramatic. ๐Ÿ™
  • --first-parent โ€” when finding commits to include, follow only the first parent commit upon seeing a merge commit. This option can give a better overview when viewing the evolution of a particular topic branch, because merges into a topic branch tend to be only about adjusting to updated upstream from time to time, and this option allows you to ignore the individual commits brought in to your history by such a merge. First-parent loyalty, honestly. ๐Ÿ‘‘
  • --exclude-first-parent-only โ€” when finding commits to exclude (with a ^), follow only the first parent commit upon seeing a merge commit. This can be used to find the set of changes in a topic branch from the point where it diverged from the remote branch, given that arbitrary merges can be valid topic branch changes. Exclude era, but first-parent only. ๐Ÿ’…
  • --maximal-only โ€” restrict the output commits to be those that are not reachable from any other commits in the revision range. Only the maximal ones, bestie. ๐Ÿ’ฏ
  • --not โ€” reverses the meaning of the ^ prefix (or lack thereof) for all following revision specifiers, up to the next --not. When used on the command line before --stdin, the revisions passed through stdin will not be affected by it. Conversely, when passed via standard input, the revisions passed on the command line will not be affected by it. Not me flipping the meaning. ๐Ÿ”„๐Ÿ’€
  • --all โ€” pretend as if all the refs in refs/, along with HEAD, are listed on the command line as <commit>. All the refs, all the vibes. ๐ŸŒˆ
  • --branches[=<pattern>] โ€” pretend as if all the refs in refs/heads are listed on the command line as <commit>. If <pattern> is given, limit branches to ones matching given shell glob. If <pattern> lacks ?, *, or [, /* at the end is implied. Branch behavior, glob behavior. ๐ŸŒฟ
  • --tags[=<pattern>] โ€” pretend as if all the refs in refs/tags are listed on the command line as <commit>. If <pattern> is given, limit tags to ones matching given shell glob. If pattern lacks ?, *, or [, /* at the end is implied. Tag era. ๐Ÿท๏ธ
  • --remotes[=<pattern>] โ€” pretend as if all the refs in refs/remotes are listed on the command line as <commit>. If <pattern> is given, limit remote-tracking branches to ones matching given shell glob. If pattern lacks ?, *, or [, /* at the end is implied. Remote-tracking branchies, this one's yours. ๐Ÿ›ฐ๏ธ
  • --glob=<glob-pattern> โ€” act like every ref matching the shell glob <glob-pattern> got typed out on the command line as a <commit>. A leading refs/ gets auto-prepended if you forgot it (she's got you ๐Ÿ’…). And if your pattern has no ?, no *, and no [ in it, then /* at the end is just implied, no need to spell it out.
  • --exclude=<glob-pattern> โ€” tells the next --all, --branches, --tags, --remotes or --glob to skip anything matching <glob-pattern>. Stacking this option up keeps piling on exclusion patterns all the way until the next --all, --branches, --tags, --remotes or --glob shows up (other options and arguments do not wipe the accumulated patterns, fyi).

Okay and the pattern rules, listen up: patterns shouldn't start with refs/heads, refs/tags or refs/remotes when they're being applied to --branches, --tags or --remotes respectively, and they must start with refs/ when applied to --glob or --all. Also if you want a trailing /*, you have to actually write it โ€” nobody's implying it for you here.

  • --exclude-hidden=(fetch|receive|uploadpack) โ€” skip any refs that git-fetch, git-receive-pack or git-upload-pack would have hidden, by checking the matching fetch.hideRefs, receive.hideRefs or uploadpack.hideRefs config plus transfer.hideRefs (peep git-config(1)). This one only affects the next pseudo-ref option โ€” --all or --glob โ€” and then it's cleared after they're processed.
  • --reflog โ€” act like every object the reflogs mention got listed on the command line as a <commit>.
  • --alternate-refs โ€” act like every object mentioned as a ref tip of an alternate repository got listed on the command line. An alternate repository is any repo whose object directory is spelled out in objects/info/alternates. Heads up: the set of included objects can get modified by core.alternateRefsCommand and friends, see git-config(1).
  • --single-worktree โ€” by default, when you've got more than one working tree (see git-worktree(1)), these options examine all of them: --all, --reflog and --indexed-objects. This option forces them to only look at the current working tree. One tree, one vibe.
  • --ignore-missing โ€” when it hits an invalid object name in the input, it just pretends the bad input never happened ๐Ÿ™ˆ.
  • --bisect โ€” act like the bad bisection ref refs/bisect/bad got listed, followed by --not and the good bisection refs refs/bisect/good-*, all on the command line.
  • --stdin โ€” on top of grabbing arguments from the command line, read them from standard input too. It accepts commits and pseudo-options like --all and --glob=. When a -- separator shows up, everything after it in the input is treated as paths and used to limit the result. Flags like --not that get read via standard input only count for arguments passed in that same way โ€” they will not influence any later command line arguments. Respect the boundary, bestie.
  • --cherry-mark โ€” like --cherry-pick (right below ๐Ÿ‘‡) except it marks equivalent commits with = instead of yeeting them, and inequivalent ones with +.
  • --cherry-pick โ€” drops any commit that introduces the same change as another commit on the โ€œother sideโ€, when the commit set is limited with symmetric difference.

Example time: say you've got two branches, A and B. The usual way to list all the commits on just one side of them is --left-right (there's an example down in the --left-right description). But that still shows the commits that were cherry-picked from the other branch (like, โ€œ3rd on bโ€ might be cherry-picked from branch A ๐Ÿคญ). With this option, those matching pairs get excluded from the output entirely.

  • --left-only, --right-only โ€” list only the commits on the respective side of a symmetric difference, i.e. only the ones that --left-right would mark < resp. >.

For example, --cherry-pick --right-only A...B omits the commits from B that are in A, or that are patch-equivalent to a commit in A. In other words: it lists the + commits from git cherry A B. And to be extra precise โ€” --cherry-pick --right-only --no-merges gives you the exact list.

  • --cherry โ€” a synonym for --right-only --cherry-mark --no-merges. Super useful for limiting the output to the commits on our side and marking the ones that already got applied to the other side of a forked history, like git log --cherry upstream...mybranch โ€” similar to git cherry upstream mybranch.
  • -g, --walk-reflogs โ€” instead of walking the commit ancestry chain, walk reflog entries from the most recent one back to the older ones. When you use this, you cannot specify commits to exclude โ€” so the ^<commit>, <commit1>..<commit2> and <commit1>...<commit2> notations are off the table.

With a --pretty format other than oneline and reference (for obvious reasons), this makes the output carry two extra lines of info grabbed from the reflog. That reflog designator can show up as ref@{<Nth>} (where <Nth> is the reverse-chronological index in the reflog) or as ref@{<timestamp>} (with the <timestamp> for that entry) โ€” depends on a few rules:

  1. If the starting point was specified as ref@{<Nth>}, show the index format.
  2. If the starting point was specified as ref@{now}, show the timestamp format.
  3. If neither was used but --date was given on the command line, show the timestamp in the format --date asked for.
  4. Otherwise, show the index format.

Under --pretty=oneline, the commit message gets prefixed with this info on the same line. This option cannot be combined with --reverse. See also git-reflog(1).

And under --pretty=reference, this info won't be shown at all. Gone. Not invited to the party.

  • --merge โ€” shows commits touching conflicted paths in the range HEAD...<other>, where <other> is the first existing pseudoref out of MERGE_HEAD, CHERRY_PICK_HEAD, REVERT_HEAD or REBASE_HEAD. Only works when the index has unmerged entries. Honestly iconic for showing the relevant commits while you're resolving conflicts from a 3-way merge.
  • --boundary โ€” outputs the excluded boundary commits too. Boundary commits get prefixed with -.

๐ŸŒฟ History Simplification ๐ŸŒฟ

Sometimes you only care about parts of the history โ€” like, the commits that modify a particular <path>. But there are two separate pieces to this: one piece is choosing the commits, and the other piece is how you do it, because there are a whole bunch of strategies for simplifying history. ๐Ÿงš

Here's the options that select which commits get shown:

<paths>

Commits that modify the given <paths> get selected.

--simplify-by-decoration

Commits that are referred to by some branch or tag get selected.

Note that extra commits can be shown, so the history actually makes sense. โœจ

And here's the options that affect how the simplification happens:

Default mode

Simplifies the history down to the simplest history that explains the final state of the tree. "Simplest" because it prunes some side branches when the end result is the same (like, merging branches that have the same content).

--show-pulls

Includes all the commits from the default mode, but also any merge commits that aren't TREESAME to the first parent but are TREESAME to a later parent. This mode is a lifesaver for showing the merge commits that "first introduced" a change to a branch.

--full-history

Same as the default mode, except it doesn't prune some history.

--dense

Only the selected commits get shown, plus some extras so the history stays meaningful.

--sparse

All commits in the simplified history get shown.

--simplify-merges

An extra option on top of --full-history that removes some needless merges from the resulting history, since there are no selected commits contributing to that merge.

--ancestry-path[=<commit>]

When you give a range of commits to display (like <commit1>..<commit2> or <commit2> ^<commit1>), and a commit <commit> in that range, only display commits in that range that are ancestors of <commit>, descendants of <commit>, or <commit> itself. If no commit is specified, it uses <commit1> (the excluded part of the range) as <commit>. Can be passed multiple times โ€” if you do, a commit is included if it's any of the commits given, or if it's an ancestor or descendant of one of them.


A more detailed explanation follows. Strap in, bestie ๐Ÿซถ

Suppose you specified foo as the <paths>. Let's call the commits that modify foo !TREESAME, and the rest TREESAME. (In a diff filtered for foo, they look different and equal, respectively. That's the whole bit.)

In everything below, we're going to keep referring to the same example history so you can see how the different simplification settings differ. We're assuming you're filtering for a file foo in this commit graph:

              .-A---M---N---O---P---Q
             /     /   /   /   /   /
            I     B   C   D   E   Y
             \   /   /   /   /   /
              `-------------'   X

The horizontal line of history A---Q is taken to be the first parent of each merge. The commits are:

  • I is the initial commit, where foo exists with contents asdf, and a file quux exists with contents quux. Initial commits get compared to an empty tree, so I is !TREESAME.
  • In A, foo contains just foo.
  • B contains the same change as A. Its merge M is trivial and therefore TREESAME to all parents.
  • C doesn't change foo, but its merge N changes it to foobar, so it's not TREESAME to any parent.
  • D sets foo to baz. Its merge O combines the strings from N and D into foobarbaz; i.e., it's not TREESAME to any parent.
  • E changes quux to xyzzy, and its merge P combines the strings into quux xyzzy. P is TREESAME to O, but not to E.
  • X is an independent root commit that added a new file side, and Y modified it. Y is TREESAME to X. Its merge Q added side to P, and Q is TREESAME to P, but not to Y.

rev-list walks backwards through history, including or excluding commits based on whether --full-history and/or parent rewriting (via --parents or --children) are used. These are the settings you've got:

Default mode

Commits get included if they're not TREESAME to any parent (though this can change โ€” see --sparse below). If the commit was a merge and it was TREESAME to one parent, follow only that parent. (Even if there are several TREESAME parents, follow only one of them, okay?) Otherwise, follow all parents.

Which gives you this:

                .-A---N---O
               /     /   /
              I---------D

So like, peep how the "only follow the TREESAME parent if one's available" rule straight up removed B from consideration entirely. Wild. C was still considered via N, but it's TREESAME. And root commits get compared to an empty tree, so I is !TREESAME.

Parent/child relations are only visible with --parents, but that doesn't change which commits get selected in default mode, so the parent lines are shown anyway. ๐Ÿ’…

--full-history without parent rewriting

This mode differs from the default in exactly one way: it always follows all parents of a merge, even if that merge is TREESAME to one of them. And even if more than one side of the merge has commits that get included, that does not mean the merge itself is included! In the example, we get:

             I  A  B  N  D  O  P  Q

M was excluded because it's TREESAME to both parents. E, C, and B were all walked, but only B was !TREESAME, so the others don't appear. โœจ

ngl, without parent rewriting you can't really talk about parent/child relationships between commits, so they just show up disconnected. it's giving "we don't do relationships here" ๐Ÿ˜ญ

--full-history with parent rewriting

Ordinary commits are only included if they're !TREESAME (though that can be changed โ€” see --sparse below, bestie).

Merges are always included. But their parent list gets rewritten: along each parent, commits that aren't included themselves get pruned away. Which gives us:

            .-A---M---N---O---P---Q
           /     /   /   /   /
          I     B   /   D   /
           \   /   /   /   /
            `-------------'

Compare that to --full-history without rewriting up above. Note that E got pruned away because it's TREESAME, but the parent list of P was rewritten to contain E's parent I. Same thing happened for C and N, and X, Y, and Q. ๐Ÿฆ‹


In addition to the settings above, you can change whether TREESAME actually affects inclusion at all:

--dense

Commits that are walked are included if they're not TREESAME to any parent.

--sparse

All commits that are walked are included. Periodt.

Heads up though: without --full-history, this still simplifies merges โ€” if one of the parents is TREESAME, only that one gets followed, so the other sides of the merge are never walked at all. ๐Ÿ™ˆ

--simplify-merges

Okay so first, it builds a history graph the same way --full-history with parent rewriting does (see above, bestie).

Then it simplifies each commit C to its replacement C' in the final history, according to these rules:

  • Set C' to C.
  • Replace each parent P of C' with its simplification P'. While doing that, drop parents that are ancestors of other parents, or that are root commits TREESAME to an empty tree, and remove duplicates โ€” but be super careful to never drop all parents that we are TREESAME to.
  • If, after this parent rewriting, C' is a root or merge commit (has zero or >1 parents), a boundary commit, or !TREESAME, it stays. Otherwise it gets replaced with its only parent.

The effect of this is best shown by comparing to --full-history with parent rewriting. The example turns into:

            .-A---M---N---O
           /     /       /
          I     B       D
           \   /       /
            `---------'

Note the major differences in N, P, and Q versus --full-history:

  • N's parent list had I removed, because it's an ancestor of the other parent M. Still, N remained because it's !TREESAME. ๐Ÿ’–
  • P's parent list similarly had I removed. P was then removed completely, because it had one parent and is TREESAME.
  • Q's parent list had Y simplified to X. X was then removed, because it was a TREESAME root. Q was then removed completely, because it had one parent and is TREESAME.

There's another simplification mode available:

--ancestry-path[=<commit>]

Limits the displayed commits to those which are an ancestor of <commit>, or which are a descendant of <commit>, or are <commit> itself. ๐Ÿ‘‘

As an example use case, consider this commit history:

            D---E-------F
           /     \       \
          B---C---G---H---I---J
         /                     \
        A-------K---------------L--M

A regular D..M computes the set of commits that are ancestors of M, but excludes the ones that are ancestors of D. That's useful for seeing what happened to the history leading to M since D โ€” like, "what does M have that did not exist in D". The result in this example would be all the commits, except A and B (and D itself, obviously).

But when we wanna find out which commits in M are contaminated with the bug introduced by D and need fixing, we might want to view only the subset of D..M that are actually descendants of D, i.e. excluding C and K. That's exactly what the --ancestry-path option does. Applied to the D..M range, it results in:

            E-------F
             \       \
              G---H---I---J
                           \
                            L--M

We can also use --ancestry-path=D instead of --ancestry-path, which means the same thing when applied to the D..M range, but is just more explicit. ๐Ÿ’…

If instead we're interested in a given topic within this range, and all commits affected by that topic, we may only want to view the subset of D..M which contain that topic in their ancestry path. So, using --ancestry-path=H D..M for example would result in:

            E
             \
          C---G---H---I---J
                           \
                            L--M

Whereas --ancestry-path=K D..M would result in:

            K---------------L--M

Before we discuss another option, --show-pulls, we gotta create a new example history. ๐ŸŒธ

A common problem users face when looking at simplified history is that a commit they know changed a file somehow doesn't appear in the file's simplified history. Let's demonstrate a new example and show how options like --full-history and --simplify-merges work in that case:

             .-A---M-----C--N---O---P
            /     / \  \  \/   /   /
           I     B   \  R-'`-Z'   /
            \   /     \/         /
             \ /      /\        /
              `---X--'  `---Y--'

For this example, suppose I created file.txt, which was modified by A, B, and X in different ways. The single-parent commits C, Z, and Y do not change file.txt. The merge commit M was created by resolving the merge conflict to include both changes from A and B, and hence is not TREESAME to either. The merge commit R, however, was created by ignoring the contents of file.txt at M and taking only the contents of file.txt at X. Hence R is TREESAME to X but not M. Finally, the natural merge resolution to create N is to take the contents of file.txt at R, so N is TREESAME to R but not C. The merge commits O and P are TREESAME to their first parents, but not to their second parents, Z and Y respectively.

When using the default mode, N and R both have a TREESAME parent, so those edges get walked and the others get ignored. The resulting history graph is:

           I---X

When using --full-history, Git walks every edge. This discovers the commits A and B and the merge M, but also reveals the merge commits O and P. With parent rewriting, the resulting graph is:

             .-A---M--------N---O---P
            /     / \  \  \/   /   /
           I     B   \  R-'`--'   /
            \   /     \/         /
             \ /      /\        /
              `---X--'  `------'

Here, the merge commits O and P contribute extra noise, because they didn't actually contribute a change to file.txt. They only merged a topic that was based on an older version of file.txt. This is a super common issue in repos using a workflow where tons of contributors work in parallel and merge their topic branches along a single trunk โ€” lots of unrelated merges show up in the --full-history results. ๐Ÿ’€

When using the --simplify-merges option, O and P straight up disappear from the results. Why? Because the rewritten second parents of O and P are reachable from their first parents. Those edges get removed, and then the commits look like single-parent commits that are TREESAME to their parent. Same thing happens to N, resulting in a history view like this:

             .-A---M--.
            /     /    \
           I     B      R
            \   /      /
             \ /      /
              `---X--'

In this view we see all the important single-parent changes from A, B, and X. We also see the carefully-resolved merge M and the not-so-carefully-resolved merge R. That's usually enough info to figure out why A and B "disappeared" from history in the default view. But there are a few issues with this approach. ๐Ÿ˜ญ

The first issue is performance. Unlike any previous option, --simplify-merges requires walking the entire commit history before returning a single result. That can make the option difficult to use for very large repositories.

The second issue is one of auditing. When many contributors are working on the same repo, it seriously matters which merge commits introduced a change into an important branch. The problematic merge R above is not likely to be the merge commit used to merge into an important branch. Instead, the merge N was used to merge R and X into the important branch. That commit may carry info in its commit message about why change X came to override the changes from A and B. ๐Ÿซถ

--show-pulls

In addition to the commits shown in the default history, this shows each merge commit that is not TREESAME to its first parent but is TREESAME to a later parent. ๐Ÿ”ฅ

When a merge commit is included by --show-pulls, the merge is treated as if it "pulled" the change from another branch. When using --show-pulls on this example (and no other options), the resulting graph is:

           I---X---R---N

Here, the merge commits R and N are included because they pulled the commits X and R into the base branch, respectively. These merges are literally the reason commits A and B don't appear in the default history. ๐Ÿ‘‘

okay so when --show-pulls is paired with --simplify-merges, the graph includes all of the necessary information:

                         .-A---M--.   N
                        /     /    \ /
                       I     B      R
                        \   /      /
                         \ /      /
                          `---X--'

Notice that since M is reachable from R, the edge from N to M was simplified away. However, N still appears in the history as an important commit because it โ€œpulledโ€ the change R into the main branch. literally giving main character energy ๐Ÿ‘‘

The --simplify-by-decoration option lets you view only the big picture of the topology of the history, by omitting commits that are not referenced by tags. Commits are marked as !TREESAME (in other words, kept after history simplification rules described above) if (1) they are referenced by tags, or (2) they change the contents of the paths given on the command line. All other commits are marked as TREESAME (subject to be simplified away). <em>bestie, thatโ€™s the whole vibe check โœจ</em>


๐Ÿฆ‹ MAPPING AUTHORS ๐Ÿฆ‹

See gitmailmap(5).

Note that if git shortlog is run outside of a repository (to process log contents on standard input), it will look for a .mailmap file in the current directory. <em>iykyk ๐Ÿซถ</em>

๐Ÿ‘‘ GIT ๐Ÿ‘‘

Part of the git(1) suite.


and thatโ€™s a wrap, bestie ๐Ÿ’… โ€” this iconic page comes from Git 2.56.0, dated 2026-09-27, and itโ€™s GIT-SHORTLOG(1). periodt ๐ŸŽ€

๐Ÿ“ฆ where this page comes from