1. Introduction
The Git file version tracking system has several types of Git trees at the root of most of its functions. Perhaps the most well-known is the tree-like structure of commits, especially because we often branch out, thereby expanding it.
In this tutorial, we explore ways to navigate the commit tree for activities like tracing ancestors of Git branches. First, we explore the three trees of Git. After that, we focus on the commit tree. Next, we show different ways to navigate the commit tree. Finally, we go over three interfaces that can make the visual inspection of Git trees more convenient.
We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. Unless otherwise specified, it should work in most POSIX-compliant environments.
2. Git Trees
There are three different main trees that Git is built on:
- root HEAD tree: current Git low-level snapshot tree
- staging area: tracked files for committing
- working tree: current working directory sandbox
Of these, we usually most often interact with the working tree as we introduce changes to a project. For example, we might want to modify data, create some branches, prune others, and so on. *Even when we stage changes, they don’t become a [push]able part of the repository before a commit operation*.
Notably, we can see the status of the last two trees via the status subcommand:
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file3
new file: file4
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file1
Untracked files:
(use "git add <file>..." to include in what will be committed)
file2
Here, we note several changes:
- staging area: file3 is modified, file4 is created, both are staged
- working tree: file1 has an unstaged modification, file2 isn’t officially tracked as part of the repository
In case there are no changes or unexpected files, we usually see that there are no staged changes to commit and the working tree hasn’t changed:
$ git status
On branch master
nothing to commit, working tree clean
Further, only the root tree is made of commits, and they point to other root trees.
3. Git Commit Tree
The HEAD root or commit tree is the structure that points to all the data permanently saved within the repository. Once a change is within this structure, only an explicit action removes it.
Let’s create a sample repository for use within this article and explore its tree via the Git log subcommand:
$ git log --all --decorate --oneline --graph
* 7763ee6 (tag: v0.1, branch1) branch feature
| * 24bfcf4 (branch2) secondary feature WIP
| * 1a063ad secondary branch modifications
|/
| * 435ac3e (branch3) experimental
|/
* 0ad45b6 branch modifications
| * 9369e35 (HEAD -> master) major modifications
| * 1724387 minor modifications
|/
* 84cbf13 restructuring
* ceb18d3 structuring
* 992361a init commit
Here, we see the [init]ial commit at the bottom, leading to the following two commits. At 84cbf13, we see the main master branch veers to the right, while the rest of the tree continues up. Finally, three branches sprout from 0ad45b6: branch3, branch2, and branch1.
Notably, this view might be a bit misleading when we’re used to looking at the main branch as the backbone of the structure.
4. Git Commit Tree Navigation
Depending on the particular case and requirements, there are different ways to go around the Git commit tree.
4.1. Specific Commits and References
In Git, a ref (reference) is a way to refer to an object, most commonly a commit:
$ git show-ref
7763ee672134e3fb174c72f03f07ae8ddf8ddf24 refs/heads/branch1
24bfcf443f3b17137af5a2422f5a12f56b7e8a4e refs/heads/branch2
435ac3e6ced3ba45553ac73bb47cfe88b9b70d54 refs/heads/branch3
9369e354b09137a19db9a600fcd13f39d4bff55c refs/heads/master
7763ee672134e3fb174c72f03f07ae8ddf8ddf24 refs/tags/v0.1
Here, the show-ref command provides us with a list of refs. Most of them are branches and one is a Git tag.
On the other hand, each commit has a commit identifier (ID). Even the refs above show their identifiers as the first field of each row. Still, we can use only a minimal unique prefix to identify a commit within the context of a given repository.
Using these pointers, we can always jump and manipulate a specific part of the tree.
4.2. Related Ref and Commit ID
Since refs and commits are related within the tree, we can apply special operators to reach a relative from a certain point:
$ git show --quiet --oneline HEAD^
1724387 minor modifications
In this case, the show subcommand reaches the first parent of the current HEAD, displaying a compact summary of the commit without a diff output.
If we look at the repository tree, we can see that commit 1724387 is indeed the commit just below the current HEAD in the master branch. Similarly, we can reach second parents and upper relatives by combining ^, ~, and the respective numbers.
4.3. Special Refs
Another way to navigate the tree is by using special refs directly or with relations operators:
- HEAD: current commit and branch
- ORIG_HEAD: backup reference to HEAD
- FETCH_HEAD: most recent branch of remote repository
- MERGE_HEAD: commit or commits being merged
- CHERRY_PICK_HEAD: cherry-picking commit
This way, we can avoid remembering or manually tagging some key commit identifiers.
4.4. Most Recent Common Ancestor
Usually, we don’t aimlessly navigate the commit tree. One common goal we might want to achieve is finding out the most recent common ancestor of two branches.
To do so, we can use the merge-base subcommand:
$ git merge-base <COMMIT_A> <COMMIT_B>
Here, COMMIT_A and COMMIT_B are usually branch names.
Let’s see an example with our repository:
$ git merge-base branch1 branch2
0ad45b6db8bc17beeece36040392d6690c876162
Indeed, we can see that the merge point between branch1 and branch2 in the tree is 0ad45b6:
$ git log --all --decorate --oneline --graph
* 7763ee6 (tag: v0.1, branch1) branch feature
| * 24bfcf4 (branch2) secondary feature WIP
| * 1a063ad secondary branch modifications
|/
| * 435ac3e (branch3) experimental
|/
* 0ad45b6 branch modifications
[...]
Other times, we might search for different junction points.
4.5. Farthest Common Ancestor and More
Another example point of interest is the first common commit between two branches.
To locate it, we can use a more complex command pipeline:
$ diff --unified <(git rev-list --first-parent COMMIT_A) \
<(git rev-list --first-parent COMMIT_B) | \
sed --quiet -e 's/^ //p' | head -1
Again, COMMIT_A and COMMIT_B are often branch names.
Let’s break this down:
- diff with the –unified option to limit the context lines around a difference to a known number (default of 3)
- <() is process substitution, which makes the output from the command within look like a file
- rev-list is a git subcommand that returns commit objects in reverse order chronologically, in this case following only the –first-parent
- sed [–quiet]ly runs the [s]ubstitution [-e]xpression and prints each modified line
- head leaves only the -1 first such junction, since each point before it can effectively also be one
In summary, the unified diff prefixes lines without changes with a space, so the first common commit we find between the lists of the two commit lists in reverse chronological order is the one we’re after.
Of course, we can extrapolate this solution to more complex cases.
5. Tig
In addition to the automated Git commands, we can visually inspect trees like we do with the Git log subcommand. However, the built-in git subcommands don’t always produce easily readable output.
This is why the Tig terminal user interface (TUI) was developed with the main idea of making Git monitoring and management easier.
5.1. Install
To begin with, let’s install Tig.
Most major Linux distributions include the tig package:
$ apt install tig
When not using a package manager, we can build and install the official TAR package.
5.2. Basic Usage
Due to its interactivity, we can directly run tig in a Git repository directory:
$ tig
[...]
2024-02-22 06:56 -0600 x o [master] major modifications
2024-02-22 06:55 -0600 x o minor modifications
2024-02-22 06:54 -0600 x o restructuring
2024-02-22 06:53 -0600 x o structuring
2024-02-22 06:50 -0600 x I init commit
[main] 9369e354b09137a19db9a600fcd13f39d4bff55c - commit 1 of 5 100%
Here, we can immediately see the main view of the interface, which includes the checked-out branch commits in chronological order.
By pressing the h key, we get help with navigating the different views.
5.3. Views
Tig offers many views:
- main (m): chronological checked-out branch commit tree
- diff (d): when over a commit, shows the changes
- log (l): checked-out branch log
- reflog (L): repository reflog
- tree (t): working directory view
- blob (f): when over a file or commit, shows related blobs
- blame (b): checked-out branch blame log
- refs (r): repository references
- status (s, S): formatted status view
- stage (c): formatted staging view
- stash (y): stashed changes
We can even reach some via a command-line parameter to the tig command like log, reflog, blame, refs, stash, and status.
In addition, grep (g) searches within the working tree.
5.4. Complete Commit Tree
We can use the –all flag to show the entire commit tree instead of only the checked-out branch:
$ tig --all
[...]
2024-02-22 10:11 -0600 x o [branch1] <v0.1> branch feature
2024-02-22 10:10 -0600 x │ o [branch2] secondary feature WIP
2024-02-22 09:50 -0600 x │ o secondary branch modifications
2024-02-22 08:58 -0600 x │ │ o [branch3] experimental
2024-02-22 06:58 -0600 x o─┴─┘ branch modifications
2024-02-22 06:56 -0600 x │ o [master] major modifications
2024-02-22 06:55 -0600 x │ o minor modifications
2024-02-22 06:54 -0600 x o─┘ restructuring
2024-02-22 06:53 -0600 x o structuring
2024-02-22 06:50 -0600 x I init commit
This way, we get a good overview with the ability to select commits for more information.
6. GitUI
Similar to Tig, the GitUI project is a TUI Git front-end. However, GitUI is also available for macOS and Microsoft Windows.
6.1. Install
GitUI provides the gitui package native to many distributions. However, it doesn’t have one for apt.
Still, there is a regular TAR release that contains the gitui single binary executable:
$ curl --silent https://api.github.com/repos/extrawurst/gitui/releases/latest | grep -wo "https.*linux.*gz" | wget -qi -
$ tar xzvf gitui-linux-musl.tar.gz
$ rm gitui-linux-*
$ chmod +x gitui
$ install gitui /usr/local/bin
Here, we parse the latest version via curl and grep and download it with wget. After that, we unpack with tar and remove the packages. Finally, we make the resulting binary executable and install under /usr/local/bin.
Of course, we can always build GitUI from its source code as well.
6.2. Basic Usage
As with other TUI implementations, we can just run the command to get an interactive interface:
$ gitui
[...]
Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5] /home/baeldung/xrep/
────────────────────────────────────────────────────────────────────────────────────────────────────
┌Commit 5/5──────────────────────────────────────────────────────────────────────────────────────────┐
│9369e35 06:56:27 x {master} major modifications █
│1724387 06:55:15 x minor modifications ║
│84cbf13 06:54:13 x restructuring ║
│ceb18d3 06:53:11 x structuring ║
│992361a 06:50:10 x init commit ║
| |
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
Scroll [↑↓] Mark [˽] Details [⏎] Branches [b] Compare [⇧C] Copy Hash [y] Tag [t] more [.]
Although the default tab is Status [1], we can switch tabs via the Tab key, so this view presents the current Log. Further, each tab has its specific activities, outlined at the bottom of the screen.
As usual, the h key spawns a brief help menu.
6.3. Complete Commit Tree
Although there isn’t an option to display all commits at once or structure them in a tree, we can list the master branch in the Log tab and press Return to get details:
┌Commit 5/5─────────────────────────────────────────────────┐┌Info───────────────────────────────────┐
│9369e35 06:56:27 x {master} major modificati█│Author: x <[email protected]> │
│1724387 06:55:15 x minor modifications ║│Date: 2024-02-22 06:50:10 │
│84cbf13 06:54:13 x restructuring ║│Sha: 9369e354b09137a19db9a600fcd13f39d4│
│ceb18d3 06:53:11 x structuring ║│ │
│992361a 06:50:10 x init commit ║│ │
│ ║│ │
│ ║└───────────────────────────────────────┘
│ ║┌Message ───────────────────────────────┐
│ ║│major modifications │
│ ║│ │
│ ║│ │
│ ║│ │
│ ║│ │
│ ║│ │
│ ║│ │
│ ║│ │
│ ║│ │
│ ║└───────────────────────────────────────┘
│ ║┌Files: 1───────────────────────────────┐
│ ║│M file │
└───────────────────────────────────────────────────────────┘└───────────────────────────────────────┘
Scroll [↑↓] Mark [˽] Details [⏎] Inspect [→] Branches [b] Compare [⇧C] Copy Hash [y] Tag [t] more [.]
This is helpful, as we can have an overview of the commits and details about the current selection on the same screen. More details and a diff can be acquired via the Right Arrow key.
Further, we can navigate through branches with b.
7. Lazygit
Perhaps the most used implementation of a Git TUI is Lazygit.
It offers a stable interface with many features, mostly within the same screen.
7.1. Install
Same as GitUI, Lazygit doesn’t have a native APT package, although it provides one for many other package managers.
Still, we can use the deployment TAR:
$ LAZYGIT_VERSION=$(curl --silent 'https://api.github.com/repos/jesseduffield/lazygit/releases/latest' | perl -n0we 'print $1 if /"tag_name": "v(.*?)"/;')
$ curl --location --output lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz"
$ tar xf lazygit.tar.gz lazygit
$ install lazygit /usr/local/bin
After running the commands above to get the relevant package version and [install]ing that, we should have the lazygit binary available in the /usr/local/bin default $PATH directory.
As always, we can also employ the sources as an installation method.
7.2. Basic Usage
Let’s run lazygit without any arguments:
$ lazygit
[...]
┌─Status─────────────────────────┐┌─Diff─────────────────────────────────────────────────────────────┐
│ xrep → master ││No changed files │
└────────────────────────────────┘│ │
┌─Files - Worktrees - Submodules─┐│ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
└─────────────────────────0 of 0─┘│ │
┌─Local branches - Remotes - Tag─┐│ │
│ * master ││ │
│26h branch2 ││ │
│26h branch1 ││ │
│24h branch3 ││ │
│ ││ │
└─────────────────────────1 of 4─┘│ │
┌─Commits - Reflog───────────────┐│ │
│9369e354 x major modifications ││ │
│1724387c x minor modifications ││ │
│84cbf133 x restructuring ││ │
│ceb18d38 x structuring ││ │
│992361a0 x init commit ││ │
└─────────────────────────1 of 5─┘└──────────────────────────────────────────────────────────────────┘
┌─Stash──────────────────────────┐┌─Command log──────────────────────────────────────────────────────┐
│ ││mergetool' │
└─────────────────────────0 of 0─┘└──────────────────────────────────────────────────────────────────┘
<pgup>/<pgdown>: Scroll, <esc>: Cancel, q: Quit, ?: Keybindings, 1-5: JumpDonate Ask Question 0.40.2
The main screen of Lazygit shows all the key points of contact with a Git repository:
- Status in terms of Files
- Local branches with a way to select a new one
- Commits of the checked-out branch
- Stash
- general information pane on the right
Further, we can navigate between the panes via the Left Arrow and Right Arrow keys or Tab and Shift+Tab. Selections within a pane can be made via the Up Arrow and Down Arrow keys. Finally, we can iterate through the tabs of a given pane via the [ and ] open and close square brackets. Notably, Lazygit supports a basic driver, providing mouse support for the interface.
Depending on the current focus, the information pane on the right changes its contents. For example, the Local branches tab shows the relevant branch Log, which also changes according to the specific selection within the pane. In addition, the pane in focus can be controlled via a command-line argument such as status, branch, log, or stash:
$ lazygit branch
Lazygit offers the ? question mark key as a way to access help for the interface.
7.3. Complete Commit Tree
When in the Status view, we can press the a key to show all branch logs as a tree:
$ lazygit status
[...]
[a]
┌─Status─────────────────────────┐┌─Log──────────────────────────────────────────────────────────────┐
│ xrep → master ││* commit 7763ee6 (tag: v0.1, branch1) ▲
└────────────────────────────────┘│| Author: x <[email protected]> █
┌─Files - Worktrees - Submodules─┐│| Date: 26 hours ago █
│ ││| █
│ ││| branch feature █
│ ││| █
│ ││| * commit 24bfcf4 (branch2) █
│ ││| | Author: x <[email protected]> █
│ ││| | Date: 26 hours ago █
│ ││| | █
│ ││| | secondary feature WIP █
│ ││| | █
│ ││| * commit 1a063ad █
│ ││|/ Author: x <[email protected]> █
│ ││| Date: 26 hours ago █
│ ││| █
└─────────────────────────0 of 0─┘│| secondary branch modifications █
┌─Local branches - Remotes - Tag─┐│| █
│ * master ││| * commit 435ac3e (branch3) █
│26h branch2 ││|/ Author: x <[email protected]> █
│26h branch1 ││| Date: 24 hours ago │
│24h branch3 ││| │
│ ││| experimental │
│ ││| │
│ ││* commit 0ad45b6 │
│ ││| Author: x <[email protected]> │
│ ││| Date: 24 hours ago │
│ ││| │
│ ││| branch modifications │
│ ││| │
└─────────────────────────1 of 4─┘│| * commit 9369e35 (HEAD -> master) │
┌─Commits - Reflog───────────────┐│| | Author: x <[email protected]> │
[...]
To see and scroll around the whole tree, we use the PageUp and PageDown keys.
8. Summary
In this article, we explored ways to navigate the Git trees and go to common ancestors.
In conclusion, the flexible Git platform offers many ways to see specific parts of a repository, but we can also use third-party tools such as TUI applications to potentially assist with such tasks.