Log() and MergeBase() fail with "object not found" on a shallow repository
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:
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 commitThen, 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:
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:
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 foundAdditional 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
Source: go-git/go-git