gittuf/pkg/gitinterface/repository_test.go
lakshit verma a7e72c4c05
gitinterface: resolve worktrees for detached git dirs and worktrees
Worktree resolution chopped ".git" off the resolved GIT_DIR path, which
only works for the standard <repo>/.git layout. This breaks repositories
created with --separate-git-dir, misclassifies linked worktrees as bare
(so file restoration was silently skipped), and misclassifies bare
repositories named foo.git as non-bare. Linked worktrees could not even
be loaded because ensureNoCompatObjectFormat expects a config file that
only exists in the repository's common Git directory.

- add Repository.GetWorktree() resolving via $GIT_DIR/gitdir,
  core.worktree, load-time discovery, then the standard layout;
  validated so stale or malformed records fall through cleanly
- determine bareness via rev-parse --is-bare-repository instead of the
  GIT_DIR name, exposed through the ErrNoWorktree sentinel
- read configuration from the common directory via $GIT_DIR/commondir
- route Status(), RestoreWorktree(), and post-propagation restore
  through GetWorktree()

Fixes #1006

Signed-off-by: lakshit verma <vermalucky2004@gmail.com>
2026-08-24 16:16:00 +05:30

309 lines
9.2 KiB
Go

// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0
package gitinterface
import (
"os"
"path/filepath"
"sync"
"testing"
"github.com/gittuf/gittuf/pkg/gitstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var _ gitstore.Storer = (*Repository)(nil)
func TestErrReferenceNotFoundIsGitstoreSentinel(t *testing.T) {
t.Parallel()
assert.ErrorIs(t, ErrReferenceNotFound, gitstore.ErrReferenceNotFound)
}
func TestRepository(t *testing.T) {
t.Run("repository.isBare", func(t *testing.T) {
t.Run("bare=true", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, true)
assert.True(t, repo.IsBare())
})
t.Run("bare=false", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false)
assert.False(t, repo.IsBare())
})
})
t.Run("with specified path, not bare", func(t *testing.T) {
tmpDir := t.TempDir()
_ = CreateTestGitRepository(t, tmpDir, false)
repo, err := LoadRepository(tmpDir)
assert.Nil(t, err)
expectedPath, err := filepath.EvalSymlinks(filepath.Join(tmpDir, ".git"))
require.Nil(t, err)
actualPath, err := filepath.EvalSymlinks(repo.GetGitDir())
require.Nil(t, err)
assert.Equal(t, expectedPath, actualPath)
})
t.Run("with specified path, is bare", func(t *testing.T) {
tmpDir := t.TempDir()
_ = CreateTestGitRepository(t, tmpDir, true)
repo, err := LoadRepository(tmpDir)
assert.Nil(t, err)
expectedPath, err := filepath.EvalSymlinks(tmpDir)
require.Nil(t, err)
actualPath, err := filepath.EvalSymlinks(repo.GetGitDir())
require.Nil(t, err)
assert.Equal(t, expectedPath, actualPath)
})
t.Run("empty path", func(t *testing.T) {
_, err := LoadRepository("")
assert.ErrorIs(t, err, ErrRepositoryPathNotSpecified)
})
t.Run("invalid path", func(t *testing.T) {
tmpDir := t.TempDir()
if _, _, has, err := findGitDirPath(tmpDir); err == nil && has {
tmpDir = filepath.Join(tmpDir, "invalid-repository")
require.Nil(t, os.Mkdir(tmpDir, 0o700))
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, ".git"), []byte("not a gitdir file"), 0o600))
}
_, err := LoadRepository(tmpDir)
assert.Error(t, err)
})
}
func TestEnsureNoCompatObjectFormat(t *testing.T) {
t.Run("no compat object format", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
assert.Nil(t, repo.ensureNoCompatObjectFormat())
})
t.Run("compat object format", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
require.Nil(t, repo.SetGitConfig("extensions.compatObjectFormat", "sha1"))
assert.ErrorIs(t, repo.ensureNoCompatObjectFormat(), ErrCompatObjectFormatUnsupported)
_, err := LoadRepository(tmpDir)
assert.ErrorIs(t, err, ErrCompatObjectFormatUnsupported)
})
t.Run("missing config", func(t *testing.T) {
repo := &Repository{gitDirPath: t.TempDir()}
err := repo.ensureNoCompatObjectFormat()
assert.ErrorContains(t, err, "unable to read repository config")
})
t.Run("invalid config", func(t *testing.T) {
tmpDir := t.TempDir()
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, "config"), []byte("[extensions\n"), 0o600))
repo := &Repository{gitDirPath: tmpDir}
err := repo.ensureNoCompatObjectFormat()
assert.ErrorContains(t, err, "unable to parse repository config")
})
}
func TestFindGitDirPath(t *testing.T) {
t.Run("worktree git directory", func(t *testing.T) {
tmpDir := t.TempDir()
_ = CreateTestGitRepository(t, tmpDir, false)
nestedDir := filepath.Join(tmpDir, "nested", "dir")
require.Nil(t, os.MkdirAll(nestedDir, 0o700))
gitDirPath, worktreePath, has, err := findGitDirPath(nestedDir)
require.Nil(t, err)
assert.True(t, has)
assert.Equal(t, filepath.Join(tmpDir, ".git"), gitDirPath)
assert.Equal(t, tmpDir, worktreePath)
})
t.Run("bare git directory", func(t *testing.T) {
tmpDir := t.TempDir()
_ = CreateTestGitRepository(t, tmpDir, true)
gitDirPath, worktreePath, has, err := findGitDirPath(tmpDir)
require.Nil(t, err)
assert.True(t, has)
assert.Equal(t, tmpDir, gitDirPath)
assert.Empty(t, worktreePath)
})
t.Run("gitdir file", func(t *testing.T) {
tmpDir := t.TempDir()
worktreePath := filepath.Join(tmpDir, "worktree")
require.Nil(t, os.MkdirAll(worktreePath, 0o700))
gitDirPath := filepath.Join(tmpDir, "actual.git")
require.Nil(t, os.WriteFile(filepath.Join(worktreePath, ".git"), []byte("gitdir: ../actual.git\n"), 0o600))
gotGitDirPath, gotWorktreePath, has, err := findGitDirPath(worktreePath)
require.Nil(t, err)
assert.True(t, has)
assert.Equal(t, gitDirPath, gotGitDirPath)
assert.Equal(t, worktreePath, gotWorktreePath)
})
t.Run("invalid gitdir file", func(t *testing.T) {
tmpDir := t.TempDir()
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, ".git"), []byte("not a gitdir file"), 0o600))
_, _, has, err := findGitDirPath(tmpDir)
assert.False(t, has)
assert.ErrorContains(t, err, "invalid gitdir file")
})
t.Run("no git directory", func(t *testing.T) {
tmpDir := t.TempDir()
if _, _, has, err := findGitDirPath(tmpDir); err == nil && has {
tmpDir = "/dev"
if _, _, has, err := findGitDirPath(tmpDir); err != nil || has {
t.Skip("unable to find a filesystem path outside a Git repository")
}
}
_, _, has, err := findGitDirPath(tmpDir)
require.Nil(t, err)
assert.False(t, has)
})
}
func TestReadGitDirFile(t *testing.T) {
t.Run("absolute gitdir path", func(t *testing.T) {
tmpDir := t.TempDir()
gitDirPath := filepath.Join(tmpDir, "actual.git")
gitDirFilePath := filepath.Join(tmpDir, ".git")
require.Nil(t, os.WriteFile(gitDirFilePath, []byte("gitdir: "+gitDirPath+"\n"), 0o600))
gotGitDirPath, err := readGitDirFile(gitDirFilePath, tmpDir)
require.Nil(t, err)
assert.Equal(t, gitDirPath, gotGitDirPath)
})
t.Run("missing gitdir file", func(t *testing.T) {
_, err := readGitDirFile(filepath.Join(t.TempDir(), ".git"), t.TempDir())
assert.Error(t, err)
})
}
func TestIsBareGitDir(t *testing.T) {
t.Run("config without head", func(t *testing.T) {
tmpDir := t.TempDir()
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, "config"), nil, 0o600))
assert.False(t, isBareGitDir(tmpDir))
})
t.Run("head as directory", func(t *testing.T) {
tmpDir := t.TempDir()
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, "config"), nil, 0o600))
require.Nil(t, os.Mkdir(filepath.Join(tmpDir, "HEAD"), 0o700))
assert.False(t, isBareGitDir(tmpDir))
})
}
func TestRepositoryObjectFormat(t *testing.T) {
t.Run("sha1", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithObjectFormat(ObjectFormatSHA1))
assert.Equal(t, ObjectFormatSHA1, repo.GetObjectFormat())
assert.Equal(t, "0000000000000000000000000000000000000000", repo.ZeroHash().String())
loaded, err := LoadRepository(tmpDir)
require.Nil(t, err)
assert.Equal(t, ObjectFormatSHA1, loaded.GetObjectFormat())
})
t.Run("sha256", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
assert.Equal(t, ObjectFormatSHA256, repo.GetObjectFormat())
assert.Equal(t, "0000000000000000000000000000000000000000000000000000000000000000", repo.ZeroHash().String())
loaded, err := LoadRepository(tmpDir)
require.Nil(t, err)
assert.Equal(t, ObjectFormatSHA256, loaded.GetObjectFormat())
})
t.Run("GetGoGitRepository", func(t *testing.T) {
for _, bare := range []bool{false, true} {
name := "worktree"
if bare {
name = "bare"
}
t.Run(name, func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, bare)
ggr, err := repo.GetGoGitRepository()
require.NoError(t, err, "GetGoGitRepository on %s repo", name)
_, err = ggr.Head()
// Empty repo: ErrReferenceNotFound is fine; what matters is the
// repo opened. Anything else (esp. ErrRepositoryNotExists) is a
// failure to open the storage.
if err != nil {
assert.ErrorContains(t, err, "reference not found")
}
})
}
})
t.Run("GetGoGitRepository on .git-suffixed bare repo", func(t *testing.T) {
// Forge-style bare repos are conventionally named <name>.git.
dir := filepath.Join(t.TempDir(), "demo.git")
repo := CreateTestGitRepository(t, dir, true)
_, err := repo.GetGoGitRepository()
require.NoError(t, err)
})
}
func TestLoadRepositoryConcurrent(t *testing.T) {
// LoadRepository must not change the process working directory; concurrent
// callers for different paths must each resolve their own gitDirPath.
dirA := t.TempDir()
dirB := t.TempDir()
_ = CreateTestGitRepository(t, dirA, true)
_ = CreateTestGitRepository(t, dirB, false)
wantA, err := filepath.EvalSymlinks(dirA)
require.NoError(t, err)
wantB, err := filepath.EvalSymlinks(filepath.Join(dirB, ".git"))
require.NoError(t, err)
const iterations = 20
var wg sync.WaitGroup
for i := 0; i < iterations; i++ {
wg.Add(2)
go func() {
defer wg.Done()
r, err := LoadRepository(dirA)
if assert.NoError(t, err) {
assert.Equal(t, wantA, r.GetGitDir())
}
}()
go func() {
defer wg.Done()
r, err := LoadRepository(dirB)
if assert.NoError(t, err) {
assert.Equal(t, wantB, r.GetGitDir())
}
}()
}
wg.Wait()
}