How to Check If a Git Commit Is Reachable from HEAD

GitGitBeginner
Practice Now

Introduction

In this lab, you will learn how to determine if a specific Git commit is reachable from the current HEAD. This is a fundamental skill for understanding your repository's history and identifying commits that are part of the active development line.

We will explore two primary methods: using the git log --ancestry-path command to visualize the commit lineage between two points, and employing the git merge-base --is-ancestor command for a direct check of ancestry. You will practice these techniques by setting up a sample repository with multiple branches and commits, including some that are intentionally made unreachable from HEAD, allowing you to test and confirm your understanding.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL git(("Git")) -.-> git/BranchManagementGroup(["Branch Management"]) git(("Git")) -.-> git/SetupandConfigGroup(["Setup and Config"]) git(("Git")) -.-> git/BasicOperationsGroup(["Basic Operations"]) git/SetupandConfigGroup -.-> git/init("Initialize Repo") git/BasicOperationsGroup -.-> git/add("Stage Files") git/BasicOperationsGroup -.-> git/commit("Create Commit") git/BranchManagementGroup -.-> git/branch("Handle Branches") git/BranchManagementGroup -.-> git/checkout("Switch Branches") git/BranchManagementGroup -.-> git/merge("Merge Histories") git/BranchManagementGroup -.-> git/log("Show Commits") git/BranchManagementGroup -.-> git/reflog("Log Ref Changes") git/BranchManagementGroup -.-> git/rebase("Reapply Commits") subgraph Lab Skills git/init -.-> lab-560063{{"How to Check If a Git Commit Is Reachable from HEAD"}} git/add -.-> lab-560063{{"How to Check If a Git Commit Is Reachable from HEAD"}} git/commit -.-> lab-560063{{"How to Check If a Git Commit Is Reachable from HEAD"}} git/branch -.-> lab-560063{{"How to Check If a Git Commit Is Reachable from HEAD"}} git/checkout -.-> lab-560063{{"How to Check If a Git Commit Is Reachable from HEAD"}} git/merge -.-> lab-560063{{"How to Check If a Git Commit Is Reachable from HEAD"}} git/log -.-> lab-560063{{"How to Check If a Git Commit Is Reachable from HEAD"}} git/reflog -.-> lab-560063{{"How to Check If a Git Commit Is Reachable from HEAD"}} git/rebase -.-> lab-560063{{"How to Check If a Git Commit Is Reachable from HEAD"}} end

Use git log --ancestry-path

In this step, we will explore how to use the git log --ancestry-path command. This command is useful for viewing the commit history along a specific path between two commits. It helps you understand the lineage of changes.

First, let's create a simple Git repository and make a few commits to set up a scenario for using --ancestry-path.

Navigate to your project directory:

cd ~/project

Create a new directory for this lab and initialize a Git repository:

mkdir ancestry-lab
cd ancestry-lab
git init

You should see output indicating that an empty Git repository has been initialized:

Initialized empty Git repository in /home/labex/project/ancestry-lab/.git/

Now, let's create a file and make the first commit:

echo "Initial content" > file1.txt
git add file1.txt
git commit -m "Initial commit"

You will see output confirming the commit:

[master (root-commit) <commit-hash>] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 file1.txt

Next, let's make another commit:

echo "Adding more content" >> file1.txt
git add file1.txt
git commit -m "Add more content"

You will see output for the second commit:

[master <commit-hash>] Add more content
 1 file changed, 1 insertion(+)

Now, let's create a new branch and make a commit on that branch:

git branch feature
git checkout feature
echo "Feature work" > file2.txt
git add file2.txt
git commit -m "Add feature file"

You will see output for creating the branch, switching to it, and the new commit:

Switched to a new branch 'feature'
[feature <commit-hash>] Add feature file
 1 file changed, 1 insertion(+)
 create mode 100644 file2.txt

Let's go back to the master branch and make another commit:

git checkout master
echo "More master work" >> file1.txt
git add file1.txt
git commit -m "More master content"

You will see output for switching branches and the new commit:

Switched to branch 'master'
[master <commit-hash>] More master content
 1 file changed, 1 insertion(+)

Now we have a commit history with a branch. Let's use git log to see the full history:

git log --all --decorate --oneline

You will see a log similar to this (commit hashes and order may vary):

<commit-hash> (HEAD -> master) More master content
<commit-hash> Add more content
<commit-hash> (feature) Add feature file
<commit-hash> Initial commit

Now, let's use git log --ancestry-path. This command requires two commit references. It will show the commits that are ancestors of the second commit and descendants of the first commit.

Let's find the commit hash for "Initial commit" and "More master content". You can get these from the git log --all --decorate --oneline output. Replace <initial-commit-hash> and <master-commit-hash> with the actual hashes from your output.

git log --ancestry-path <initial-commit-hash> <master-commit-hash> --oneline

This command will show the commits on the path from the initial commit to the latest commit on the master branch. You should see the "Initial commit", "Add more content", and "More master content" commits.

The --ancestry-path option is useful for understanding the direct line of development between two points in your history, ignoring commits from other branches that might have been merged in later.

Run git merge-base --is-ancestor

In this step, we will learn about git merge-base --is-ancestor. This command is used to check if one commit is an ancestor of another commit. It's a simple check that returns a status code (0 for true, 1 for false) rather than outputting commit information. This is particularly useful in scripting or for quick checks.

We will continue using the ancestry-lab repository we created in the previous step. Make sure you are in the correct directory:

cd ~/project/ancestry-lab

Recall the commit history from the previous step. We have commits on both the master and feature branches.

Let's find the commit hashes for the "Initial commit" and the latest commit on the master branch ("More master content"). You can use git log --oneline to see the recent commits.

git log --oneline

Output will be similar to:

<master-commit-hash> (HEAD -> master) More master content
<commit-hash> Add more content
<initial-commit-hash> Initial commit

Now, let's use git merge-base --is-ancestor to check if the "Initial commit" is an ancestor of the latest commit on master. Replace <initial-commit-hash> and <master-commit-hash> with the actual hashes.

git merge-base --is-ancestor <initial-commit-hash> <master-commit-hash>
echo $?

The echo $? command prints the exit status of the previous command. If the first commit is an ancestor of the second, the exit status will be 0. Otherwise, it will be 1.

Since the "Initial commit" is indeed an ancestor of the latest commit on master, the output of echo $? should be 0.

Now, let's check if the "Initial commit" is an ancestor of the latest commit on the feature branch. First, find the commit hash for the "Add feature file" commit.

git log --all --decorate --oneline

Output will be similar to:

<master-commit-hash> (HEAD -> master) More master content
<commit-hash> Add more content
<feature-commit-hash> (feature) Add feature file
<initial-commit-hash> Initial commit

Now, use git merge-base --is-ancestor to check if the "Initial commit" is an ancestor of the "Add feature file" commit. Replace <initial-commit-hash> and <feature-commit-hash> with the actual hashes.

git merge-base --is-ancestor <initial-commit-hash> <feature-commit-hash>
echo $?

Again, the output of echo $? should be 0 because the "Initial commit" is the starting point for both branches.

Finally, let's check if the latest commit on feature is an ancestor of the latest commit on master. Replace <feature-commit-hash> and <master-commit-hash> with the actual hashes.

git merge-base --is-ancestor <feature-commit-hash> <master-commit-hash>
echo $?

In this case, the latest commit on feature is not an ancestor of the latest commit on master (they are on different branches after the initial split). So, the output of echo $? should be 1.

Understanding the ancestor relationship between commits is fundamental to understanding how Git tracks history and how operations like merging and rebasing work. The --is-ancestor flag provides a simple way to check this relationship.

Test Unreachable Commits

In this step, we will explore the concept of "unreachable" commits in Git. An unreachable commit is a commit that cannot be reached from any branch, tag, or other reference. These commits are not part of your current project history as seen by standard commands like git log.

We will continue using the ancestry-lab repository. Make sure you are in the correct directory:

cd ~/project/ancestry-lab

Currently, all our commits are reachable from either the master or feature branch. Let's create a scenario where a commit becomes unreachable.

First, let's make a new commit on the master branch:

echo "Temporary commit" >> file1.txt
git add file1.txt
git commit -m "Temporary commit"

You will see output for this new commit:

[master <commit-hash>] Temporary commit
 1 file changed, 1 insertion(+)

Now, let's reset the master branch back to the previous commit. This will make the "Temporary commit" unreachable from the master branch. We will use git reset --hard HEAD~1. The HEAD~1 refers to the commit directly before the current HEAD.

Be careful with git reset --hard as it discards changes! In this case, we are intentionally discarding the "Temporary commit" from the master branch's history.

git reset --hard HEAD~1

You will see output indicating that the HEAD is now at the previous commit:

HEAD is now at <previous-commit-hash> More master content

Now, let's look at the standard git log:

git log --oneline

You will see that the "Temporary commit" is no longer in the log output for the master branch.

<previous-commit-hash> (HEAD -> master) More master content
<commit-hash> Add more content
<initial-commit-hash> Initial commit

The "Temporary commit" still exists in the Git database, but it's not referenced by any branch or tag. It is now an "unreachable" commit.

How can we see unreachable commits? Git has a special reference called reflog which records updates to the tip of branches and other references. We can use git log with the --walk-reflogs option or simply git reflog to see these commits.

Let's use git reflog:

git reflog

You will see a log of actions taken, including the commit we just made and then reset:

<master-commit-hash> (HEAD -> master) master@{0}: reset: moving to HEAD~1
<temporary-commit-hash> master@{1}: commit: Temporary commit
<previous-commit-hash> master@{2}: commit: More master content
<commit-hash> master@{3}: commit: Add more content
<initial-commit-hash> master@{4}: commit (initial): Initial commit

Notice the entry for "Temporary commit". It is reachable via master@{1} in the reflog. However, it is not reachable from the current HEAD or any branch tip.

Unreachable commits are eventually cleaned up by Git's garbage collection (git gc), but they remain accessible via the reflog for a default period (usually 30 or 90 days). This can be a lifesaver if you accidentally reset or delete commits.

Understanding unreachable commits helps you grasp the difference between Git's internal object database and the references (branches, tags, HEAD) that point to commits within that database.

Summary

In this lab, we learned how to check if a Git commit is reachable from HEAD using two primary methods. First, we explored the git log --ancestry-path command, which allows us to visualize the commit history along the path between two specified commits. We set up a simple repository with multiple branches and commits to demonstrate how this command helps understand the lineage and identify if one commit is an ancestor of another.

Secondly, we will learn how to use the git merge-base --is-ancestor command, which provides a more direct and programmatic way to determine if a commit is an ancestor of another. Finally, we will test these methods with unreachable commits to solidify our understanding of how to verify reachability within a Git repository.