gittuf/pkg/gitinterface/worktree_test.go
lakshit verma a4cbba9b15
gitinterface: placate gosec taint analysis on worktree path reads
golangci-lint (gosec G703) flags the worktree and commondir path reads as
path traversal because it cannot see across the validation functions. Each
resolved path is constrained before use: it must point into an existing
directory that is not the repository's GIT_DIR, and for linked worktrees
the .git entry must in turn reference this repository. Annotate the read
sites to reflect that these are trusted repository-local paths.

Also fix test-only lints from the CI gate: use 0600 file permissions for
temporary files and mark TestGetWorktree parallel at the top level.

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

359 lines
11 KiB
Go

// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0
package gitinterface
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mustRunGit runs a Git command directly for test setup scenarios that have no
// Repository instance yet (e.g. `git init --separate-git-dir`) and fails the
// test with the command's output if it does not succeed. Ambient GIT_* environment
// variables are filtered out to keep the suite hermetic.
func mustRunGit(t *testing.T, args ...string) {
t.Helper()
cmd := exec.Command(binary, args...) //nolint:gosec
env := []string{}
for _, entry := range os.Environ() {
key := strings.ToUpper(strings.SplitN(entry, "=", 2)[0])
if key != "GIT_DIR" && key != "GIT_WORK_TREE" {
env = append(env, entry)
}
}
cmd.Env = env
output, err := cmd.CombinedOutput()
require.Nil(t, err, "git %v failed: %s", args, string(output))
}
func evalTestPath(t *testing.T, path string) string {
t.Helper()
resolved, err := filepath.EvalSymlinks(path)
require.Nil(t, err)
return resolved
}
func TestGetWorktree(t *testing.T) {
t.Parallel()
t.Run("standard layout", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false)
worktree, err := repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, tmpDir), worktree)
})
t.Run("standard layout, SHA-256", func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
worktree, err := repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, tmpDir), worktree)
})
t.Run("loaded from subdirectory", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
CreateTestGitRepository(t, tmpDir, false)
subDir := filepath.Join(tmpDir, "subdir")
require.Nil(t, os.Mkdir(subDir, 0o755))
repo, err := LoadRepository(subDir)
require.Nil(t, err)
worktree, err := repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, tmpDir), worktree)
})
t.Run("detached git dir", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
worktreeDir := filepath.Join(tmpDir, "worktree")
gitDirPath := filepath.Join(tmpDir, "storage.git")
mustRunGit(t, "init", "--separate-git-dir", gitDirPath, worktreeDir)
repo, err := LoadRepository(worktreeDir)
require.Nil(t, err)
// the legacy TrimSuffix approach resolves to <tmp>/storage, which does
// not exist; GetWorktree must resolve to the actual worktree instead
worktree, err := repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, worktreeDir), worktree)
})
t.Run("explicit relative core.worktree", func(t *testing.T) {
// Git interprets relative core.worktree values relative to the
// GIT_DIR; submodules are configured this way. The repository is
// constructed directly so that no load-time discovery takes
// precedence and the core.worktree branch is exercised.
t.Parallel()
tmpDir := t.TempDir()
worktreeDir := filepath.Join(tmpDir, "worktree")
gitDirPath := filepath.Join(tmpDir, "modules", "repo.git")
require.Nil(t, os.MkdirAll(filepath.Dir(gitDirPath), 0o755))
mustRunGit(t, "init", "--separate-git-dir", gitDirPath, worktreeDir)
repo := &Repository{gitDirPath: gitDirPath}
_, _, err := repo.executor("config", "core.worktree", "../../worktree").execute()
require.Nil(t, err)
worktree, err := repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, worktreeDir), worktree)
})
t.Run("stale core.worktree", func(t *testing.T) {
// a core.worktree value pointing at a directory that is no longer
// valid must be ignored rather than trusted
t.Parallel()
tmpDir := t.TempDir()
worktreeDir := filepath.Join(tmpDir, "worktree")
gitDirPath := filepath.Join(tmpDir, "storage.git")
require.Nil(t, os.MkdirAll(filepath.Dir(gitDirPath), 0o755))
mustRunGit(t, "init", "--separate-git-dir", gitDirPath, worktreeDir)
repo, err := LoadRepository(worktreeDir)
require.Nil(t, err)
// point core.worktree at a nonexistent location
_, _, err = repo.executor("config", "core.worktree", filepath.Join(tmpDir, "gone")).execute()
require.Nil(t, err)
worktree, err := repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, worktreeDir), worktree)
})
t.Run("gitdir file pointing to another repository", func(t *testing.T) {
// a $GIT_DIR/gitdir entry that points at a different repository must
// not redirect worktree resolution
t.Parallel()
tmpDir := t.TempDir()
gitDirPath := filepath.Join(tmpDir, ".git")
mustRunGit(t, "init", tmpDir)
otherRepoPath := filepath.Join(tmpDir, "other")
mustRunGit(t, "init", otherRepoPath)
repo, err := LoadRepository(tmpDir)
require.Nil(t, err)
require.Nil(t, os.WriteFile(filepath.Join(gitDirPath, "gitdir"), []byte(filepath.Join(otherRepoPath, ".git")+"\n"), 0o600))
// the record is ignored and resolution falls back to the parent-of-.git
// layout for this repository
worktree, err := repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, tmpDir), worktree)
})
t.Run("stale linked worktree record", func(t *testing.T) {
// a $GIT_DIR/gitdir entry that points nowhere must not be trusted
tmpDir := t.TempDir()
gitDirPath := filepath.Join(tmpDir, ".git")
mustRunGit(t, "init", tmpDir)
repo := &Repository{gitDirPath: gitDirPath}
require.Nil(t, os.WriteFile(filepath.Join(gitDirPath, "gitdir"), []byte("/does/not/exist/.git\n"), 0o600))
// fall back to the standard layout instead of trusting the record
worktree, err := repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, tmpDir), worktree)
})
t.Run("linked worktree", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
mainRepoPath := filepath.Join(tmpDir, "main")
linkedRepoPath := filepath.Join(tmpDir, "linked")
repo := CreateTestGitRepository(t, mainRepoPath, false)
// `git worktree add` requires at least one commit
require.Nil(t, os.WriteFile(filepath.Join(mainRepoPath, "README.md"), []byte("test"), 0o600))
_, err := repo.executor("add", ".").executeString()
require.Nil(t, err)
_, err = repo.executor("commit", "--no-gpg-sign", "-m", "initial commit").executeString()
require.Nil(t, err)
_, err = repo.executor("worktree", "add", linkedRepoPath, "-b", "feature").executeString()
require.Nil(t, err)
linkedRepo, err := LoadRepository(linkedRepoPath)
require.Nil(t, err)
// linked worktrees do not have a GIT_DIR ending in .git, so IsBare
// must not classify them as bare
assert.False(t, linkedRepo.IsBare())
worktree, err := linkedRepo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, linkedRepoPath), worktree)
// the main repository still resolves to its own worktree
worktree, err = repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, mainRepoPath), worktree)
})
t.Run("linked worktree, SHA-256", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
mainRepoPath := filepath.Join(tmpDir, "main")
linkedRepoPath := filepath.Join(tmpDir, "linked")
repo := CreateTestGitRepository(t, mainRepoPath, false, WithSHA256Format())
require.Nil(t, os.WriteFile(filepath.Join(mainRepoPath, "README.md"), []byte("test"), 0o600))
_, err := repo.executor("add", ".").executeString()
require.Nil(t, err)
_, err = repo.executor("commit", "--no-gpg-sign", "-m", "initial commit").executeString()
require.Nil(t, err)
_, err = repo.executor("worktree", "add", linkedRepoPath, "-b", "feature").executeString()
require.Nil(t, err)
// loading a linked worktree reads the repository config from the
// common Git directory via commondir
linkedRepo, err := LoadRepository(linkedRepoPath)
require.Nil(t, err)
assert.Equal(t, ObjectFormatSHA256, linkedRepo.GetObjectFormat())
worktree, err := linkedRepo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, linkedRepoPath), worktree)
})
t.Run("status in linked worktree", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
mainRepoPath := filepath.Join(tmpDir, "main")
linkedRepoPath := filepath.Join(tmpDir, "linked")
repo := CreateTestGitRepository(t, mainRepoPath, false)
require.Nil(t, os.WriteFile(filepath.Join(mainRepoPath, "README.md"), []byte("test"), 0o600))
_, err := repo.executor("add", ".").executeString()
require.Nil(t, err)
_, err = repo.executor("commit", "--no-gpg-sign", "-m", "initial commit").executeString()
require.Nil(t, err)
_, err = repo.executor("worktree", "add", linkedRepoPath, "-b", "feature").executeString()
require.Nil(t, err)
untrackedFile := filepath.Join(linkedRepoPath, "untracked.txt")
require.Nil(t, os.WriteFile(untrackedFile, []byte("test"), 0o600))
linkedRepo, err := LoadRepository(linkedRepoPath)
require.Nil(t, err)
statuses, err := linkedRepo.Status()
require.Nil(t, err)
assert.Len(t, statuses, 1)
status, has := statuses["untracked.txt"]
require.True(t, has)
assert.True(t, status.Untracked())
})
t.Run("bare repository", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, true)
assert.True(t, repo.IsBare())
_, err := repo.GetWorktree()
assert.ErrorIs(t, err, ErrNoWorktree)
// Status on a bare repository must surface a clear error rather than
// running git inside an unrelated directory
_, err = repo.Status()
assert.ErrorIs(t, err, ErrNoWorktree)
})
t.Run("bare repository named with .git suffix", func(t *testing.T) {
// the name-based heuristic misclassified these as non-bare
t.Parallel()
tmpDir := t.TempDir()
bareDirPath := filepath.Join(tmpDir, "bare.git")
repo := setupRepository(t, bareDirPath, true, ObjectFormatSHA1)
assert.True(t, repo.IsBare())
_, err := repo.GetWorktree()
assert.ErrorIs(t, err, ErrNoWorktree)
})
t.Run("fallback without LoadRepository", func(t *testing.T) {
// repositories constructed directly only carry their GIT_DIR, so
// resolution falls back to Git's standard layout
t.Parallel()
tmpDir := t.TempDir()
mustRunGit(t, "init", tmpDir)
repo := &Repository{gitDirPath: filepath.Join(tmpDir, ".git")}
worktree, err := repo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, tmpDir), worktree)
})
t.Run("loaded from subdirectory of linked worktree", func(t *testing.T) {
// discovery must work when invoked from anywhere inside the linked
// worktree, matching how git rev-parse resolves the toplevel
t.Parallel()
tmpDir := t.TempDir()
mainRepoPath := filepath.Join(tmpDir, "main")
linkedRepoPath := filepath.Join(tmpDir, "linked")
repo := CreateTestGitRepository(t, mainRepoPath, false)
require.Nil(t, os.WriteFile(filepath.Join(mainRepoPath, "README.md"), []byte("test"), 0o600))
_, err := repo.executor("add", ".").executeString()
require.Nil(t, err)
_, err = repo.executor("commit", "--no-gpg-sign", "-m", "initial commit").executeString()
require.Nil(t, err)
_, err = repo.executor("worktree", "add", linkedRepoPath, "-b", "feature").executeString()
require.Nil(t, err)
nestedDir := filepath.Join(linkedRepoPath, "nested")
require.Nil(t, os.Mkdir(nestedDir, 0o755))
linkedRepo, err := LoadRepository(nestedDir)
require.Nil(t, err)
worktree, err := linkedRepo.GetWorktree()
require.Nil(t, err)
assert.Equal(t, evalTestPath(t, linkedRepoPath), worktree)
})
}