#2409·go-git

Log() and MergeBase() fail with "object not found" on a shallow repository

Author: HubertKalCreated Sep 20, 2026Updated Sep 20, 2026
Labelsbug

Bug Description

On a shallow repository, all six Repository.Log() orders and Commit.MergeBase() fail with plumbing.ErrObjectNotFound as soon as the walk reaches the shallow boundary commit. The boundary commit's object correctly records its parent hashes, but those parents were deliberately never fetched, so looking them up fails.

git log and git merge-base handle the same repository without complaint — for the fixture below, git merge-base A B prints the boundary commit while go-git returns object not found.

The practical effect is that no Log() walk can run to completion on a --depth-limited clone — it yields the locally-present commits and then errors — which makes the API awkward to use in CI, where shallow clones are common.

go-git Version

v6

Steps to Reproduce

Build a shallow clone with two branches rooted at the boundary commit:

bash
git init -q origin && cd origin
for i in 1 2 3; do echo $i > f$i; git add .; git commit -qm "c$i"; done
cd ..
git clone -q --depth=1 "file://$PWD/origin" shallow && cd shallow
git checkout -q -b A && echo a > a.txt && git add . && git commit -qm "on A"
git checkout -q - && git checkout -q -b B && echo b > b.txt && git add . && git commit -qm "on B"
git merge-base A B          # real git: prints the boundary commit

Then, in a module pinned to main (go get github.com/go-git/go-git/v6@0f3a0a2), run the following with the shallow clone's path as its argument:

go
package main

import (
	"fmt"
	"os"

	"github.com/go-git/go-git/v6"
	"github.com/go-git/go-git/v6/plumbing"
	"github.com/go-git/go-git/v6/plumbing/object"
)

func main() {
	repo, err := git.PlainOpen(os.Args[1])
	if err != nil {
		panic(err)
	}
	head, err := repo.Head()
	if err != nil {
		panic(err)
	}

	orders := []struct {
		name  string
		order git.LogOrder
	}{
		{"Default", git.LogOrderDefault},
		{"DFS", git.LogOrderDFS},
		{"DFSPost", git.LogOrderDFSPost},
		{"DFSPostFirstParent", git.LogOrderDFSPostFirstParent},
		{"BSF", git.LogOrderBSF},
		{"CommitterTime", git.LogOrderCommitterTime},
	}
	for _, o := range orders {
		iter, err := repo.Log(&git.LogOptions{From: head.Hash(), Order: o.order})
		if err != nil {
			panic(err)
		}
		n := 0
		err = iter.ForEach(func(*object.Commit) error { n++; return nil })
		fmt.Printf("Log(%-18s) -> visited %d, err = %v\n", o.name, n, err)
	}

	a, _ := commitAt(repo, "refs/heads/A")
	b, _ := commitAt(repo, "refs/heads/B")
	bases, err := a.MergeBase(b)
	fmt.Printf("MergeBase(A, B)           -> %d bases, err = %v\n", len(bases), err)
}

func commitAt(repo *git.Repository, name string) (*object.Commit, error) {
	ref, err := repo.Reference(plumbing.ReferenceName(name), true)
	if err != nil {
		return nil, err
	}
	return repo.CommitObject(ref.Hash())
}

Run it from inside shallow (the directory the shell block above ends in), passing . as the repository path:

bash
go run repro.go .

Output on main:

Log(Default           ) -> visited 2, err = object not found
Log(DFS               ) -> visited 2, err = object not found
Log(DFSPost           ) -> visited 1, err = object not found
Log(DFSPostFirstParent) -> visited 1, err = object not found
Log(BSF               ) -> visited 1, err = object not found
Log(CommitterTime     ) -> visited 1, err = object not found
MergeBase(A, B)           -> 0 bases, err = object not found

Additional Information

Root cause.

Upstream git discards a shallow commit's parents before any revision walk sees them: register_shallow() registers each shallow commit as a graft with nr_parent = -1 and frees commit->parents outright (shallow.c). go-git instead leaves Commit.ParentHashes populated and expects each caller to check shallow-ness itself. Three callers do — objectWalker.isShallow (added in #1792), revlist.objectWalk, and isFastForward, which pre-resolves every shallow commit just to seed its walker with the parents to skip — but the five Log() iterators and NewFilterCommitIter (which MergeBase and Independents use) do not.

Assisted-by: Claude Sonnet 5