Adds initial gitlocal CLI and core functionality
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`.
This commit is contained in:
372
internal/git/git_test.go
Normal file
372
internal/git/git_test.go
Normal file
@@ -0,0 +1,372 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.membo.co.uk/dtomlinson/gitlocal/internal/testutil"
|
||||
)
|
||||
|
||||
func TestIsGitRepo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T) string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "valid git repo",
|
||||
setup: func(t *testing.T) string {
|
||||
return testutil.CreateTempGitRepo(t)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not a git repo",
|
||||
setup: func(t *testing.T) string {
|
||||
return t.TempDir()
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "has .gitlocal but not .git",
|
||||
setup: func(t *testing.T) string {
|
||||
dir := t.TempDir()
|
||||
gitLocalDir := filepath.Join(dir, GitLocalDir)
|
||||
if err := os.Mkdir(gitLocalDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .gitlocal: %v", err)
|
||||
}
|
||||
return dir
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path := tt.setup(t)
|
||||
result := IsGitRepo(path)
|
||||
if result != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGitLocalRepo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T) string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "has .gitlocal",
|
||||
setup: func(t *testing.T) string {
|
||||
dir := t.TempDir()
|
||||
gitLocalDir := filepath.Join(dir, GitLocalDir)
|
||||
if err := os.Mkdir(gitLocalDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .gitlocal: %v", err)
|
||||
}
|
||||
return dir
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has .git but not .gitlocal",
|
||||
setup: func(t *testing.T) string {
|
||||
return testutil.CreateTempGitRepo(t)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "has neither",
|
||||
setup: func(t *testing.T) string {
|
||||
return t.TempDir()
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path := tt.setup(t)
|
||||
result := IsGitLocalRepo(path)
|
||||
if result != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRemoteURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T) string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "repo with remote",
|
||||
setup: func(t *testing.T) string {
|
||||
dir := testutil.CreateTempGitRepoWithRemote(t, "git@github.com:test/repo.git")
|
||||
return filepath.Join(dir, GitDir)
|
||||
},
|
||||
expected: "git@github.com:test/repo.git",
|
||||
},
|
||||
{
|
||||
name: "repo without remote",
|
||||
setup: func(t *testing.T) string {
|
||||
dir := testutil.CreateTempGitRepo(t)
|
||||
return filepath.Join(dir, GitDir)
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "invalid git dir",
|
||||
setup: func(t *testing.T) string {
|
||||
return "/nonexistent/.git"
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gitDir := tt.setup(t)
|
||||
result := GetRemoteURL(gitDir)
|
||||
if result != tt.expected {
|
||||
t.Errorf("expected %q, got %q", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentBranch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T) string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "default branch (master or main)",
|
||||
setup: func(t *testing.T) string {
|
||||
dir := testutil.CreateTempGitRepo(t)
|
||||
return filepath.Join(dir, GitDir)
|
||||
},
|
||||
// Git might use master or main depending on config
|
||||
expected: "", // We'll check it's non-empty instead
|
||||
},
|
||||
{
|
||||
name: "custom branch",
|
||||
setup: func(t *testing.T) string {
|
||||
dir := testutil.CreateTempGitRepoWithBranch(t, "develop")
|
||||
return filepath.Join(dir, GitDir)
|
||||
},
|
||||
expected: "develop",
|
||||
},
|
||||
{
|
||||
name: "invalid git dir",
|
||||
setup: func(t *testing.T) string {
|
||||
return "/nonexistent/.git"
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gitDir := tt.setup(t)
|
||||
result := GetCurrentBranch(gitDir)
|
||||
|
||||
if tt.name == "default branch (master or main)" {
|
||||
// Just check it returned something
|
||||
if result == "" {
|
||||
t.Errorf("expected non-empty branch name, got empty string")
|
||||
}
|
||||
} else if result != tt.expected {
|
||||
t.Errorf("expected %q, got %q", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T) string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid git repo",
|
||||
setup: func(t *testing.T) string {
|
||||
return testutil.CreateTempGitRepo(t)
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "no .git directory",
|
||||
setup: func(t *testing.T) string {
|
||||
return t.TempDir()
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: ".gitlocal already exists",
|
||||
setup: func(t *testing.T) string {
|
||||
dir := testutil.CreateTempGitRepo(t)
|
||||
gitLocalDir := filepath.Join(dir, GitLocalDir)
|
||||
if err := os.Mkdir(gitLocalDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .gitlocal: %v", err)
|
||||
}
|
||||
return dir
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path := tt.setup(t)
|
||||
err := Convert(path)
|
||||
|
||||
if tt.expectErr && err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
|
||||
if !tt.expectErr && err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if !tt.expectErr {
|
||||
// Verify .git was renamed to .gitlocal
|
||||
gitPath := filepath.Join(path, GitDir)
|
||||
gitLocalPath := filepath.Join(path, GitLocalDir)
|
||||
|
||||
if _, err := os.Stat(gitPath); !os.IsNotExist(err) {
|
||||
t.Errorf(".git directory still exists")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(gitLocalPath); os.IsNotExist(err) {
|
||||
t.Errorf(".gitlocal directory does not exist")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevert(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T) string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid .gitlocal directory",
|
||||
setup: func(t *testing.T) string {
|
||||
dir := testutil.CreateTempGitRepo(t)
|
||||
// Convert to .gitlocal
|
||||
if err := Convert(dir); err != nil {
|
||||
t.Fatalf("failed to convert: %v", err)
|
||||
}
|
||||
return dir
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "no .gitlocal directory",
|
||||
setup: func(t *testing.T) string {
|
||||
return t.TempDir()
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: ".git already exists",
|
||||
setup: func(t *testing.T) string {
|
||||
dir := testutil.CreateTempGitRepo(t)
|
||||
gitLocalDir := filepath.Join(dir, GitLocalDir)
|
||||
if err := os.Mkdir(gitLocalDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .gitlocal: %v", err)
|
||||
}
|
||||
return dir
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path := tt.setup(t)
|
||||
err := Revert(path)
|
||||
|
||||
if tt.expectErr && err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
|
||||
if !tt.expectErr && err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if !tt.expectErr {
|
||||
// Verify .gitlocal was renamed to .git
|
||||
gitPath := filepath.Join(path, GitDir)
|
||||
gitLocalPath := filepath.Join(path, GitLocalDir)
|
||||
|
||||
if _, err := os.Stat(gitLocalPath); !os.IsNotExist(err) {
|
||||
t.Errorf(".gitlocal directory still exists")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(gitPath); os.IsNotExist(err) {
|
||||
t.Errorf(".git directory does not exist")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertRevertRoundTrip(t *testing.T) {
|
||||
dir := testutil.CreateTempGitRepoWithRemote(t, "git@github.com:test/repo.git")
|
||||
|
||||
// Initial state - should have .git
|
||||
if !IsGitRepo(dir) {
|
||||
t.Fatal("expected .git to exist initially")
|
||||
}
|
||||
|
||||
// Convert
|
||||
if err := Convert(dir); err != nil {
|
||||
t.Fatalf("convert failed: %v", err)
|
||||
}
|
||||
|
||||
// Should have .gitlocal, not .git
|
||||
if IsGitRepo(dir) {
|
||||
t.Error("expected .git to be gone after convert")
|
||||
}
|
||||
if !IsGitLocalRepo(dir) {
|
||||
t.Error("expected .gitlocal to exist after convert")
|
||||
}
|
||||
|
||||
// Remote should still be accessible via .gitlocal
|
||||
gitLocalPath := filepath.Join(dir, GitLocalDir)
|
||||
remote := GetRemoteURL(gitLocalPath)
|
||||
if remote != "git@github.com:test/repo.git" {
|
||||
t.Errorf("expected remote to be preserved, got %q", remote)
|
||||
}
|
||||
|
||||
// Revert
|
||||
if err := Revert(dir); err != nil {
|
||||
t.Fatalf("revert failed: %v", err)
|
||||
}
|
||||
|
||||
// Should have .git again, not .gitlocal
|
||||
if !IsGitRepo(dir) {
|
||||
t.Error("expected .git to exist after revert")
|
||||
}
|
||||
if IsGitLocalRepo(dir) {
|
||||
t.Error("expected .gitlocal to be gone after revert")
|
||||
}
|
||||
|
||||
// Remote should still be accessible
|
||||
gitPath := filepath.Join(dir, GitDir)
|
||||
remote = GetRemoteURL(gitPath)
|
||||
if remote != "git@github.com:test/repo.git" {
|
||||
t.Errorf("expected remote to be preserved after round trip, got %q", remote)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user