Introduces the `gitlocal` command-line tool for managing nested Git repositories. Includes the following main commands: - `convert`: Renames `.git` to `.gitlocal`, allowing a parent repository to ignore the converted child repository. Supports recursive scanning and dry-run options. Tracks converted repositories in a global configuration. - `revert`: Restores `.gitlocal` to `.git`. Includes an option to revert all tracked repositories. - `status`: Displays a list of all repositories currently tracked by `gitlocal`, showing their path, conversion time, and original remote/branch. Establishes internal modules for Git operations, configuration management, and recursive repository scanning. Adds a comprehensive test suite covering core command logic and utility functions. Initializes Go module and basic project `.gitignore`.
226 lines
5.5 KiB
Go
226 lines
5.5 KiB
Go
package scanner
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
|
|
"git.membo.co.uk/dtomlinson/gitlocal/internal/git"
|
|
"git.membo.co.uk/dtomlinson/gitlocal/internal/testutil"
|
|
)
|
|
|
|
func TestFindNestedGitRepos(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T) (rootDir string, expectedPaths []string)
|
|
expectedCount int
|
|
}{
|
|
{
|
|
name: "no nested repos",
|
|
setup: func(t *testing.T) (string, []string) {
|
|
dir := testutil.CreateTempGitRepo(t)
|
|
return dir, []string{}
|
|
},
|
|
expectedCount: 0,
|
|
},
|
|
{
|
|
name: "nested repos structure",
|
|
setup: func(t *testing.T) (string, []string) {
|
|
rootDir, nestedDirs := testutil.CreateNestedRepoStructure(t)
|
|
return rootDir, nestedDirs
|
|
},
|
|
expectedCount: 3,
|
|
},
|
|
{
|
|
name: "non-git directory",
|
|
setup: func(t *testing.T) (string, []string) {
|
|
dir := t.TempDir()
|
|
// Create subdirectories without .git
|
|
os.MkdirAll(filepath.Join(dir, "subdir1"), 0755)
|
|
os.MkdirAll(filepath.Join(dir, "subdir2", "deep"), 0755)
|
|
return dir, []string{}
|
|
},
|
|
expectedCount: 0,
|
|
},
|
|
{
|
|
name: "mixed nested repos and regular dirs",
|
|
setup: func(t *testing.T) (string, []string) {
|
|
dir := testutil.CreateTempGitRepo(t)
|
|
|
|
// Create one nested git repo
|
|
nested1 := filepath.Join(dir, "nested1")
|
|
os.MkdirAll(nested1, 0755)
|
|
testutil.CreateTempGitRepo(t) // This creates in temp dir, we need to manually init
|
|
// Let's use a helper to init git in nested1
|
|
initGitInDir(t, nested1)
|
|
|
|
// Create regular directories
|
|
os.MkdirAll(filepath.Join(dir, "regular"), 0755)
|
|
os.MkdirAll(filepath.Join(dir, "another", "deep"), 0755)
|
|
|
|
return dir, []string{nested1}
|
|
},
|
|
expectedCount: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rootDir, expectedPaths := tt.setup(t)
|
|
|
|
repos, err := FindNestedGitRepos(rootDir)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(repos) != tt.expectedCount {
|
|
t.Errorf("expected %d repos, got %d: %v", tt.expectedCount, len(repos), repos)
|
|
}
|
|
|
|
if tt.expectedCount > 0 {
|
|
// Sort both slices for comparison
|
|
sort.Strings(repos)
|
|
sort.Strings(expectedPaths)
|
|
|
|
for i, expected := range expectedPaths {
|
|
if i >= len(repos) {
|
|
t.Errorf("missing expected repo: %s", expected)
|
|
continue
|
|
}
|
|
if repos[i] != expected {
|
|
t.Errorf("expected repo %s, got %s", expected, repos[i])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindNestedGitReposSkipsRootGit(t *testing.T) {
|
|
rootDir := testutil.CreateTempGitRepo(t)
|
|
|
|
// Create nested repos
|
|
nested1 := filepath.Join(rootDir, "nested1")
|
|
os.MkdirAll(nested1, 0755)
|
|
initGitInDir(t, nested1)
|
|
|
|
repos, err := FindNestedGitRepos(rootDir)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Should find nested1 but not the root
|
|
if len(repos) != 1 {
|
|
t.Fatalf("expected 1 nested repo, got %d", len(repos))
|
|
}
|
|
|
|
if repos[0] != nested1 {
|
|
t.Errorf("expected %s, got %s", nested1, repos[0])
|
|
}
|
|
}
|
|
|
|
func TestFindNestedGitReposWithGitLocal(t *testing.T) {
|
|
rootDir := testutil.CreateTempGitRepo(t)
|
|
|
|
// Create nested repo with .gitlocal
|
|
nested1 := filepath.Join(rootDir, "nested1")
|
|
os.MkdirAll(nested1, 0755)
|
|
initGitInDir(t, nested1)
|
|
// Convert to .gitlocal
|
|
if err := git.Convert(nested1); err != nil {
|
|
t.Fatalf("failed to convert: %v", err)
|
|
}
|
|
|
|
// Create nested repo with .git
|
|
nested2 := filepath.Join(rootDir, "nested2")
|
|
os.MkdirAll(nested2, 0755)
|
|
initGitInDir(t, nested2)
|
|
|
|
repos, err := FindNestedGitRepos(rootDir)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Should find both
|
|
if len(repos) != 2 {
|
|
t.Fatalf("expected 2 nested repos, got %d", len(repos))
|
|
}
|
|
|
|
sort.Strings(repos)
|
|
expectedPaths := []string{nested1, nested2}
|
|
sort.Strings(expectedPaths)
|
|
|
|
for i, expected := range expectedPaths {
|
|
if repos[i] != expected {
|
|
t.Errorf("expected repo %s, got %s", expected, repos[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindNestedGitReposWithRootGitLocal(t *testing.T) {
|
|
rootDir := testutil.CreateTempGitRepo(t)
|
|
|
|
// Convert root to .gitlocal
|
|
if err := git.Convert(rootDir); err != nil {
|
|
t.Fatalf("failed to convert root: %v", err)
|
|
}
|
|
|
|
// Create nested repo
|
|
nested1 := filepath.Join(rootDir, "nested1")
|
|
os.MkdirAll(nested1, 0755)
|
|
initGitInDir(t, nested1)
|
|
|
|
repos, err := FindNestedGitRepos(rootDir)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Should find nested1 but not the root .gitlocal
|
|
if len(repos) != 1 {
|
|
t.Fatalf("expected 1 nested repo, got %d", len(repos))
|
|
}
|
|
|
|
if repos[0] != nested1 {
|
|
t.Errorf("expected %s, got %s", nested1, repos[0])
|
|
}
|
|
}
|
|
|
|
func TestFindNestedGitReposDeepNesting(t *testing.T) {
|
|
rootDir := testutil.CreateTempGitRepo(t)
|
|
|
|
// Create deeply nested structure
|
|
deep := filepath.Join(rootDir, "a", "b", "c", "d", "nested")
|
|
os.MkdirAll(deep, 0755)
|
|
initGitInDir(t, deep)
|
|
|
|
repos, err := FindNestedGitRepos(rootDir)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(repos) != 1 {
|
|
t.Fatalf("expected 1 nested repo, got %d", len(repos))
|
|
}
|
|
|
|
if repos[0] != deep {
|
|
t.Errorf("expected %s, got %s", deep, repos[0])
|
|
}
|
|
}
|
|
|
|
// Helper function to initialize git in a directory
|
|
func initGitInDir(t *testing.T, dir string) {
|
|
t.Helper()
|
|
|
|
// We can't use testutil.CreateTempGitRepo since it creates a new temp dir
|
|
// So we manually init git here
|
|
gitDir := filepath.Join(dir, git.GitDir)
|
|
if err := os.MkdirAll(gitDir, 0755); err != nil {
|
|
t.Fatalf("failed to create .git directory: %v", err)
|
|
}
|
|
|
|
// Create minimal git structure
|
|
// For the scanner, we just need the .git directory to exist
|
|
// The scanner doesn't validate the git repo structure
|
|
}
|