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--formatoption ofgit log, such as* [%h] %s. (Peep the PRETTY FORMATS section ofgit-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--dateoption in the Commit Formatting section ofgit-log(1).) Useful with--group=format:<format>. Date glow-up unlocked. ๐๏ธโจ--group=<type>โ group commits based on<type>. If no--groupoption is specified, the default isauthor.<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 (seegit-interpret-trailers(1)). For example, if your project usesReviewed-bytrailers, you might want to see who has been reviewing withgit 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 aname <email>identity. If successful, the mailmap is applied and the email is omitted unless the--emailoption 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--formatoption ofgit log. (See the PRETTY FORMATS section ofgit-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 atwidth. The first line of each entry is indented byindent1spaces, and the second and subsequent lines are indented byindent2spaces.width,indent1, andindent2default to76,6and9respectively. Ifwidthis0(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 toHEAD(i.e. the whole history leading to the current commit).origin..HEADspecifies all the commits reachable from the current commit (i.e.HEAD), but not fromorigin. For a complete list of ways to spell<revision-range>, see the Specifying Ranges section ofgitrevisions(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,todaymeans 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-reflogsis 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--notesis 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=1is the same as--no-merges,--min-parents=2is the same as--merges.--max-parents=0gives all root commits and--min-parents=3all octopus merges.--no-min-parentsand--no-max-parentsreset 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 inrefs/, along withHEAD, are listed on the command line as<commit>. All the refs, all the vibes. ๐--branches[=<pattern>]โ pretend as if all the refs inrefs/headsare 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 inrefs/tagsare 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 inrefs/remotesare 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 leadingrefs/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,--remotesor--globto skip anything matching<glob-pattern>. Stacking this option up keeps piling on exclusion patterns all the way until the next--all,--branches,--tags,--remotesor--globshows 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 thatgit-fetch,git-receive-packorgit-upload-packwould have hidden, by checking the matchingfetch.hideRefs,receive.hideRefsoruploadpack.hideRefsconfig plustransfer.hideRefs(peepgit-config(1)). This one only affects the next pseudo-ref option โ--allor--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 inobjects/info/alternates. Heads up: the set of included objects can get modified bycore.alternateRefsCommandand friends, seegit-config(1).--single-worktreeโ by default, when you've got more than one working tree (seegit-worktree(1)), these options examine all of them:--all,--reflogand--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 refrefs/bisect/badgot listed, followed by--notand the good bisection refsrefs/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--alland--glob=. When a--separator shows up, everything after it in the input is treated as paths and used to limit the result. Flags like--notthat 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-rightwould 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, likegit log --cherry upstream...mybranchโ similar togit 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:
- If the starting point was specified as
ref@{<Nth>}, show the index format. - If the starting point was specified as
ref@{now}, show the timestamp format. - If neither was used but
--datewas given on the command line, show the timestamp in the format--dateasked for. - 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 rangeHEAD...<other>, where<other>is the first existing pseudoref out ofMERGE_HEAD,CHERRY_PICK_HEAD,REVERT_HEADorREBASE_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:
Iis the initial commit, wherefooexists with contentsasdf, and a filequuxexists with contentsquux. Initial commits get compared to an empty tree, soIis !TREESAME.- In
A,foocontains justfoo. Bcontains the same change asA. Its mergeMis trivial and therefore TREESAME to all parents.Cdoesn't changefoo, but its mergeNchanges it tofoobar, so it's not TREESAME to any parent.Dsetsfootobaz. Its mergeOcombines the strings fromNandDintofoobarbaz; i.e., it's not TREESAME to any parent.Echangesquuxtoxyzzy, and its mergePcombines the strings intoquux xyzzy.Pis TREESAME toO, but not toE.Xis an independent root commit that added a new fileside, andYmodified it.Yis TREESAME toX. Its mergeQaddedsidetoP, andQis TREESAME toP, but not toY.
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'toC. - Replace each parent
PofC'with its simplificationP'. 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 hadIremoved, because it's an ancestor of the other parentM. Still,Nremained because it's !TREESAME. ๐P's parent list similarly hadIremoved.Pwas then removed completely, because it had one parent and is TREESAME.Q's parent list hadYsimplified toX.Xwas then removed, because it was a TREESAME root.Qwas 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
- arch rolling: git 2.56.0-1 ยท GPL-2.0-only
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms