Skip to content

Instantly share code, notes, and snippets.

@datagrok
Last active April 16, 2024 17:26
Show Gist options
  • Save datagrok/4221767 to your computer and use it in GitHub Desktop.
Save datagrok/4221767 to your computer and use it in GitHub Desktop.
How to simplify the graph produced by git log --graph

Ideas for improvements to git log --graph

I will maybe someday get around to dusting off my C and making these changes myself unless someone else does it first.

Make the graph for --topo-order less wiggly

Imagine a long-running development branch periodically merges from master. The git log --graph --all --topo-order is not as simple as it could be, as of git version 1.7.10.4.

It doesn't seem like a big deal in this example, but when you're trying to follow the history trails in ASCII and you've got several different branches displayed at once, it gets difficult quickly.

To simulate:

git init example
cd example
git commit --allow-empty -m "initial empty commit"
git branch dev
for x in `seq 4`; do
    git commit --allow-empty -m "commit"
    git checkout dev
    git merge --no-ff --no-edit master
    git checkout master
done

The result:

$ git log --graph --topo-order --decorate --oneline --all
*   f19e46d (dev) Merge branch 'master' into dev
|\
| * 600a33f (HEAD, master) commit
* |   c9ccbf1 Merge branch 'master' into dev
|\ \
| |/
| * 4c1fb52 commit
* |   dfdcb8a Merge branch 'master' into dev
|\ \
| |/
| * 42b1c24 commit
* |   7714be3 Merge branch 'master' into dev
|\ \
| |/
| * f7eae2d commit
|/
* 885700a initial empty commit

What I would like it to display:

$ git log --graph --topo-order --decorate --oneline --all
*   f19e46d (dev) Merge branch 'master' into dev
|\
| * 600a33f (HEAD, master) commit
* |   c9ccbf1 Merge branch 'master' into dev
|\|
| * 4c1fb52 commit
* |   dfdcb8a Merge branch 'master' into dev
|\|
| * 42b1c24 commit
* |   7714be3 Merge branch 'master' into dev
|\|
| * f7eae2d commit
|/
* 885700a initial empty commit

--boundary makes mountains

The previous example wasn't so bad. But we had the luxury of having only two branches in the repo, with not many commits on each, allowing us to specify --all.

Let's now assume there are many branches. A way to ask for the graph of a long-running parallel development branch that merges frequently from master might be to show the log for master..branch, but also to use --boundary. But, that produces this strange mountainous result:

$ git log --graph --topo-order --decorate --oneline --boundary master..dev
*   f19e46d (dev) Merge branch 'master' into dev
|\
* \   c9ccbf1 Merge branch 'master' into dev
|\ \
* \ \   dfdcb8a Merge branch 'master' into dev
|\ \ \
* \ \ \   7714be3 Merge branch 'master' into dev
|\ \ \ \
| | | | o 600a33f (HEAD, master) commit
| | | |/
| | | o 4c1fb52 commit
| | |/
| | o 42b1c24 commit
| |/
| o f7eae2d commit
|/
o 885700a initial empty commit

That's a big diagram for only 5 commits and 5 merges! As with the previous example, I think this should output:

$ git log --graph --topo-order --decorate --oneline --boundary master..dev
*   f19e46d (dev) Merge branch 'master' into dev
|\
| o 600a33f (HEAD, master) commit
* |   c9ccbf1 Merge branch 'master' into dev
|\|
| o 4c1fb52 commit
* |   dfdcb8a Merge branch 'master' into dev
|\|
| o 42b1c24 commit
* |   7714be3 Merge branch 'master' into dev
|\|
| o f7eae2d commit
|/
* 885700a initial empty commit

Let me specify which branches sink to the left.

Sometimes I'm faced with a very complex mess of merges and branches. I'd like to be able to ask git log to show me the graph, but to always set commits from specific branches into specific columns in the output. This might make for more line-drawing to connect the commits, but that's okay in this situation.

Using the very same repo above, I'd like to say "make left-most columns for master and dev, in that order" and see:

$ git log --graph --all --topo-order --decorate --oneline --boundary --force-branch-columns=master,dev

.-- master
| .-- dev
v v

  * a9f8d93 (dev) Merge branch 'master' into dev
 /|
* | 5f32650 (HEAD, master) commit
| * b511501 Merge branch 'master' into dev
|/|
* | 4e6810e commit
| * 2cd55b4 Merge branch 'master' into dev
|/|
* | 4f74695 commit
| * 372799e Merge branch 'master' into dev
|/|
* | 076669f commit
 \|
  * 7382440 initial empty commit

Maybe that doesn't seem like it's of much value, but let's throw another branch into the mix and see what it might look like.

Branch from somewhere in the middle of dev, create two commits, then merge from master:

git checkout -b newbranch dev~3
echo 2 >> 2.txt; git add 2.txt; git commit -m "commit"
echo 2 >> 2.txt; git add 2.txt; git commit -m "commit"
git merge --no-ff --no-edit master

The unmodified result:

$ git log --graph --all --topo-order --decorate --oneline --boundary
*   f745bf5 (HEAD, newbranch) Merge branch 'master' into newbranch
|\
* | 7031537 commit
* | 416ab2c commit
| | *   a9f8d93 (dev) Merge branch 'master' into dev
| | |\
| | |/
| |/|
| * | 5f32650 (master) commit
| | *   b511501 Merge branch 'master' into dev
| | |\
| | |/
| |/|
| * | 4e6810e commit
| | *   2cd55b4 Merge branch 'master' into dev
| | |\
| |/ /
|/| /
| |/
| * 4f74695 commit
* |   372799e Merge branch 'master' into dev
|\ \
| |/
| * 076669f commit
|/
* 7382440 initial empty commit

What's most striking to me about the as-is unmodified output is the frequency with which we see this cross-legged pattern:

  *   b511501 Merge branch 'master' into dev
  |\
  |/
 /|
* | 4e6810e commit
  *   2cd55b4 Merge branch 'master' into dev

Is the output perhaps intentionally like this to show which parent of a merge commit is the "first" parent? If so, that may be useful sometimes, so I wouldn't want to see that style completely removed in favor of my suggestion.

With different line-drawing logic and forced columns, it might look like this:

$ git log --graph --all --topo-order --decorate --oneline --boundary --force-branch-columns=master,dev

.-- master
| .-- dev
v v

    * f745bf5 (HEAD, newbranch) Merge branch 'master' into newbranch
   /|
  / * 7031537 commit
 /  * 416ab2c commit
| * | a9f8d93 (dev) Merge branch 'master' into dev
|/| |
* | | 5f32650 (master) commit
| * | b511501 Merge branch 'master' into dev
|/| |
* | | 4e6810e commit
| * | 2cd55b4 Merge branch 'master' into dev
|/|/
| /
|/|
* | 4f74695 commit
| * 372799e Merge branch 'master' into dev
|/|
* | 076669f commit
 \|
  * 7382440 initial empty commit

I think that it is much easier with that visualization to track which commits are in master and dev, and see the points at which other branches forked from and merged with them.

Elide "branch tails" to avoid "mountains"

Consider a workflow where feature branches diverge from some integration branch like "master," are worked on for some time, and are then merged back into that upstream branch. They may be worked on concurrently, and they may or may not be merged in the same order that they were begun.

They often have long tails that show where the branch was started, which is information I don't really need. When too many of these tails collect in the output, they become impossible to visually trace, forming an ugly "mountain."

To simulate:

git init example
cd example
git commit --allow-empty -m "initial empty commit"
for b in 1 2 3 4 5 6; do
    git checkout master
    git commit --allow-empty -m "start of feature-$b"
    git branch feature-$b
done
for x in `seq 3`; do
    for b in 1 2 3 4 5 6; do
        git checkout feature-$b
        git commit --allow-empty -m "commit $b-1.$x"
    done
done
git checkout master
for b in 6 5 4 1 2 3; do
    git checkout master
    git merge --no-ff --no-edit feature-$b
    git branch -d feature-$b
done

The result:

*   85a4880 (HEAD, master) Merge branch 'feature-3'
|\
| * 6057a0a commit 3-1.3
| * aa2d7e7 commit 3-1.2
| * c5130d2 commit 3-1.1
* |   a6f44d9 Merge branch 'feature-2'
|\ \
| * | 97612ed commit 2-1.3
| * | f3e66fc commit 2-1.2
| * | 2a5af7a commit 2-1.1
* | |   02bfff3 Merge branch 'feature-1'
|\ \ \
| * | | 50a334e commit 1-1.3
| * | | 29d5386 commit 1-1.2
| * | | eccd08a commit 1-1.1
* | | |   d185b26 Merge branch 'feature-4'
|\ \ \ \
| * | | | 6c044c8 commit 4-1.3
| * | | | 3f9cdd9 commit 4-1.2
| * | | | 5da9916 commit 4-1.1
* | | | |   bcc9b74 Merge branch 'feature-5'
|\ \ \ \ \
| * | | | | 56ef96c commit 5-1.3
| * | | | | 22d50d7 commit 5-1.2
| * | | | | 5d539a3 commit 5-1.1
* | | | | |   84b1d36 Merge branch 'feature-6'
|\ \ \ \ \ \
| * | | | | | b5f5ef6 commit 6-1.3
| * | | | | | 691a8f6 commit 6-1.2
| * | | | | | 50dc2ae commit 6-1.1
|/ / / / / /
* | | | | | 989fe62 start of feature-6
|/ / / / /
* | | | | 22fc09d start of feature-5
|/ / / /
* | | | 7b9ac41 start of feature-4
| |_|/
|/| |
* | | 4fbc57e start of feature-3
| |/
|/|
* | e4c2da5 start of feature-2
|/
* 9f0e3b1 start of feature-1
* f7d3186 initial empty commit

I'd like to be able to elide branch tails, maybe only when they split from the same branch that they will eventually merge into:

*   85a4880 (HEAD, master) Merge branch 'feature-3'
|\
| * 6057a0a commit 3-1.3
| * aa2d7e7 commit 3-1.2
| * c5130d2 commit 3-1.1
| :
* a6f44d9 Merge branch 'feature-2'
|\
| * 97612ed commit 2-1.3
| * f3e66fc commit 2-1.2
| * 2a5af7a commit 2-1.1
| :
* 02bfff3 Merge branch 'feature-1'
|\
| * 50a334e commit 1-1.3
| * 29d5386 commit 1-1.2
| * eccd08a commit 1-1.1
| :
* d185b26 Merge branch 'feature-4'
|\
| * 6c044c8 commit 4-1.3
| * 3f9cdd9 commit 4-1.2
| * 5da9916 commit 4-1.1
| :
* bcc9b74 Merge branch 'feature-5'
|\
| * 56ef96c commit 5-1.3
| * 22d50d7 commit 5-1.2
| * 5d539a3 commit 5-1.1
| :
* 84b1d36 Merge branch 'feature-6'
|\
| * b5f5ef6 commit 6-1.3
| * 691a8f6 commit 6-1.2
| * 50dc2ae commit 6-1.1
|/
* 989fe62 start of feature-6
* 22fc09d start of feature-5
* 7b9ac41 start of feature-4
* 4fbc57e start of feature-3
* e4c2da5 start of feature-2
* 9f0e3b1 start of feature-1
* f7d3186 initial empty commit
@refaelsh
Copy link

+1

@refaelsh
Copy link

+1

@pb-cdunn
Copy link

👍

@Artalus
Copy link

Artalus commented Aug 15, 2018

Yeah, those wiggly

* |   da8e8d4 Merge branch 'master' into x
|\ \  
| |/  
| * cda6197

instead of compact

* | da8e8d4 Merge branch 'master' into x
|\|  
| |  
| * cda6197

look extremely ugly. I wish someone would do something about it already.

@eedrah
Copy link

eedrah commented Sep 24, 2018

I have a feeling (and I'm guessing from intuition here, not from reading the code) that the 'cross-legged' pattern comes from git sorting by parent number, and putting them by order on the left. That is, the branch to which you are merging will always be the one on the left, and the branch from which you are merging will always be the second (or more), and be on the right.

Thus, the 'cross legs' - if you are merging the line on the left into the right, git needs to draw the cross legs to approach from the correct direction.

That being said, this sounds like a great option to ignore this ordering. If I'm correct in the above, perhaps a better name for the option would be --ignore-parent-order?

Do people think that they will go ahead with this, or is it caught in development limbo?

@daef
Copy link

daef commented Dec 11, 2018

++bump

@gpaulsen
Copy link

++bump

@sorashi
Copy link

sorashi commented Jan 30, 2019

@magol
Copy link

magol commented Feb 25, 2019

+1

@ellio167
Copy link

ellio167 commented Mar 5, 2019

+1

@somyadashora
Copy link

+1
Anybody found an alternative in 2019

Copy link

ghost commented Jul 30, 2019

+1

@ImLuze
Copy link

ImLuze commented Sep 19, 2019

+1

@hyliang96
Copy link

+1

@hyliang96
Copy link

hyliang96 commented Jan 14, 2020

I am considering another easier style to plot reader friendly git log.

In this style, a branch either is a permanent branch (e.g. master, release, dev) or belongs to a temporary branch group (e.g. features, hotfixes). A permanent branch takes one column, while branches in a temporary branch group share one column.

Nodes among all permanent branches are vertically arranged by temporal order, even if across different permanent branches. Nodes on a temporary branch maintain temporal order only within this branch rather than across branches. On a temporary branch, the nodes between two merges (into permanent branches), are plotted together.

In the log graph, '*' denotes commit node and '+' denotes the merged node. Merging into a permanent branch is denoted by a horizontal line. While merging into a temporary branch is not plotted explicitly, instead, the log shows the hash of the source node in the other branch.

This style is not only easy to read, but also easy to implement, because it avoids '' and '/' which are quite difficult to handle. Maybe it can be implemented with shell by processing the output of original git log.

Here is a imagined example of how to use the tool:

git log --graph --all --decorate --oneline --permanent=master,dev --temporary-group=feature-* --temporary-group=hotfix-*
●  horizontally shared node
o  commited node
M  merged node
I  initialization node
:

┌─ master
│ ┌─ release
│ │ ┌─dev
│ │ │ ┌─ feature-*
│ │ │ │ ┌─ hotfix-*
│ │ │ │ │ ┌─ other temporary branches
│ │ │ │ │ │
│ │ │ : : :
│ │ │
│ │ │     o  qwe13ad (playground) commit
│ │ │     o  p23fea2 commit
│ │ ├─────●          start of branch 'playground'
│ │ │
│ │ │ o      10asd12 (HEAD->feature-2) commit
│ │ │ o      12oiads commit
│ │ │ :
│ │ │
M─┤ │        7714be3 Merge branch 'release'
├─M │        a324fss Merge branch 'master'
│ M─┤        12jh4p3 Merge branch 'dev'
│ │ │
│ │ M─┐      24hjh92 Merge branch 'feature-1'
│ │ │ M      1sa3421 Merge branch 'dev' (12asd2l)
│ │ │ o      pa9bba2 commit
│ │ │ o      1doiajn commit
│ │ │ :
│ │ │
│ │ M─┐      12asd2l (feature2) Merge branch 'feature-2'
│ │ │ M      13os03n Merge branch 'dev' (sp3ns02)
│ │ │ o      s03asd2 commit
│ │ │ o      sp02nd3 commit
│ │ │ M      12p0wjd Merge branch 'dev' (a10asd2)
│ │ │ o      123asd2 commit
│ │ │ :
│ │ │
│ │ M─┐      sp3ns02 Merge branch 'feature-1'
│ │ │ M      1sa3421 Merge branch 'dev' (a10asd2)
│ │ │ o      pa9bba2 commit
│ │ │ o      1doiajn commit
│ │ │ :
│ │ │
M─│─│───●    12o234d (hotfix-1) Merge branch 'hotfix-1'
│ │ M───┤    a10asd2 Merge branch 'hotfix-1'
│ │ │   o    p812nas commit
│ │ │   o    p023nas commit
│ │ │   :
│ │ │
│ │ │ :
│ │ ├─●              start of branch 'feature-2'
│ │ │
M─│─│───┐    12o234d Merge branch 'hotfix-2'
│ │ M───┤    21sad32 Merge branch 'hotfix-2'
│ │ │   o    p812nas (hotfix-2) commit
│ │ │   o    p023nas commit
├─│─│───●             start of branch 'hotfix-1'
│ │ │
M─┤ │        13p324s Merge branch 'release'    # when a marged mode is shared by banches
│ │ │
│ │ │   :
├─│─│───●            start of branch 'hotfix-2'
│ │ │
│ │ │   :
│ │ ├───●            start of branch 'feature-1'
│ ├─●
├─●
I            pa3123s initial empty commit

Are there anyone interested in it? Let’s have a discussion and implement it.

@studgeek
Copy link

@datagrok Have you filed a feature request for this with git?

@onilton
Copy link

onilton commented Mar 2, 2020

Although I didn't implement the suggestion here, I created https://github.com/onilton/ogl to have better lines and help visualize the git log graph better by using Box Drawing chars.

@garyo
Copy link

garyo commented Mar 2, 2020

@onilton looks great!

@somyadashora
Copy link

somyadashora commented Mar 13, 2020

Although I didn't implement the suggestion here, I created https://github.com/onilton/ogl to have better lines and help visualize the git log graph better by using Box Drawing chars.
@onilton
I tried it, its Great. You can add more configuration options.

@ruohola
Copy link

ruohola commented Nov 13, 2020

+1 This would be awesome.

@onilton
Copy link

onilton commented Nov 13, 2020

@somyadashora what configurations would you like to see?

@fangxlmr
Copy link

fangxlmr commented Apr 2, 2021

++bump

Those mountains still annoy me in 2021 :(

@jlcjunk
Copy link

jlcjunk commented Jul 20, 2021

+1 better graph formatting options

@onilton ogl is nice, but it doesn't seem very configurable & the lines still wiggle.

@onilton
Copy link

onilton commented Aug 5, 2021

@jlcjunk what configurations do you need/want?

@KES777
Copy link

KES777 commented Feb 10, 2022

+1
Do you send this proposition to maillist?

@ObiWahn
Copy link

ObiWahn commented Mar 9, 2022

+1
Would love it!

@Joinyy
Copy link

Joinyy commented Mar 31, 2022

+1
What I really can't wrap my head around is that even most GUIs do those weird things too...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment