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:
463
cmd/cmd_test.go
Normal file
463
cmd/cmd_test.go
Normal file
@@ -0,0 +1,463 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.membo.co.uk/dtomlinson/gitlocal/internal/config"
|
||||
"git.membo.co.uk/dtomlinson/gitlocal/internal/git"
|
||||
"git.membo.co.uk/dtomlinson/gitlocal/internal/testutil"
|
||||
)
|
||||
|
||||
// setupTestConfig creates a temporary home directory and config for testing
|
||||
func setupTestConfig(t *testing.T) (cleanup func()) {
|
||||
t.Helper()
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
tempHome := t.TempDir()
|
||||
os.Setenv("HOME", tempHome)
|
||||
|
||||
return func() {
|
||||
os.Setenv("HOME", originalHome)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertSingleRepo(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
repoDir := testutil.CreateTempGitRepoWithRemote(t, "git@github.com:test/repo.git")
|
||||
|
||||
// Run convert command
|
||||
convertCmd.Flags().Set("recursive", "false")
|
||||
convertCmd.Flags().Set("dry-run", "false")
|
||||
err := runConvert(convertCmd, []string{repoDir})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("convert command failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify .git was renamed to .gitlocal
|
||||
if git.IsGitRepo(repoDir) {
|
||||
t.Error("expected .git to be converted")
|
||||
}
|
||||
|
||||
if !git.IsGitLocalRepo(repoDir) {
|
||||
t.Error("expected .gitlocal to exist")
|
||||
}
|
||||
|
||||
// Verify config was updated
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != 1 {
|
||||
t.Fatalf("expected 1 repo in config, got %d", len(cfg.Repos))
|
||||
}
|
||||
|
||||
if cfg.Repos[0].Path != repoDir {
|
||||
t.Errorf("expected path %s, got %s", repoDir, cfg.Repos[0].Path)
|
||||
}
|
||||
|
||||
if cfg.Repos[0].OriginalRemote != "git@github.com:test/repo.git" {
|
||||
t.Errorf("expected remote to be saved in config, got %s", cfg.Repos[0].OriginalRemote)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDryRun(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
repoDir := testutil.CreateTempGitRepo(t)
|
||||
|
||||
// Run convert with dry-run
|
||||
dryRun = true
|
||||
defer func() { dryRun = false }()
|
||||
|
||||
convertCmd.Flags().Set("dry-run", "true")
|
||||
err := runConvert(convertCmd, []string{repoDir})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("convert dry-run failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify nothing was actually converted
|
||||
if !git.IsGitRepo(repoDir) {
|
||||
t.Error("expected .git to still exist (dry-run)")
|
||||
}
|
||||
|
||||
if git.IsGitLocalRepo(repoDir) {
|
||||
t.Error("expected .gitlocal to not exist (dry-run)")
|
||||
}
|
||||
|
||||
// Verify config was not updated
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != 0 {
|
||||
t.Errorf("expected config to be empty (dry-run), got %d repos", len(cfg.Repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertRecursive(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
rootDir, nestedDirs := testutil.CreateNestedRepoStructure(t)
|
||||
|
||||
// Run convert recursively
|
||||
recursive = true
|
||||
defer func() { recursive = false }()
|
||||
|
||||
convertCmd.Flags().Set("recursive", "true")
|
||||
err := runConvert(convertCmd, []string{rootDir})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("convert recursive failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify all nested repos were converted
|
||||
for _, nestedDir := range nestedDirs {
|
||||
if git.IsGitRepo(nestedDir) {
|
||||
t.Errorf("expected %s to be converted", nestedDir)
|
||||
}
|
||||
|
||||
if !git.IsGitLocalRepo(nestedDir) {
|
||||
t.Errorf("expected %s to have .gitlocal", nestedDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify config has all repos
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != len(nestedDirs) {
|
||||
t.Errorf("expected %d repos in config, got %d", len(nestedDirs), len(cfg.Repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertAlreadyConverted(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
repoDir := testutil.CreateTempGitRepo(t)
|
||||
|
||||
// First convert
|
||||
convertCmd.Flags().Set("recursive", "false")
|
||||
err := runConvert(convertCmd, []string{repoDir})
|
||||
if err != nil {
|
||||
t.Fatalf("first convert failed: %v", err)
|
||||
}
|
||||
|
||||
// Try to convert again - should register it
|
||||
err = runConvert(convertCmd, []string{repoDir})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error when re-converting, got: %v", err)
|
||||
}
|
||||
|
||||
// Should still only have 1 entry in config
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != 1 {
|
||||
t.Errorf("expected 1 repo in config, got %d", len(cfg.Repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertNoGitRepo(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
nonGitDir := t.TempDir()
|
||||
|
||||
err := runConvert(convertCmd, []string{nonGitDir})
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected error when converting non-git directory")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "no .git directory") {
|
||||
t.Errorf("expected 'no .git directory' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevertSingleRepo(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
repoDir := testutil.CreateTempGitRepo(t)
|
||||
|
||||
// Convert first
|
||||
convertCmd.Flags().Set("recursive", "false")
|
||||
if err := runConvert(convertCmd, []string{repoDir}); err != nil {
|
||||
t.Fatalf("convert failed: %v", err)
|
||||
}
|
||||
|
||||
// Now revert
|
||||
err := runRevert(revertCmd, []string{repoDir})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("revert command failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify .gitlocal was renamed back to .git
|
||||
if !git.IsGitRepo(repoDir) {
|
||||
t.Error("expected .git to exist after revert")
|
||||
}
|
||||
|
||||
if git.IsGitLocalRepo(repoDir) {
|
||||
t.Error("expected .gitlocal to be gone after revert")
|
||||
}
|
||||
|
||||
// Verify config was updated
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != 0 {
|
||||
t.Errorf("expected config to be empty after revert, got %d repos", len(cfg.Repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevertAll(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create and convert multiple repos
|
||||
repo1 := testutil.CreateTempGitRepo(t)
|
||||
repo2 := testutil.CreateTempGitRepo(t)
|
||||
repo3 := testutil.CreateTempGitRepo(t)
|
||||
|
||||
for _, repo := range []string{repo1, repo2, repo3} {
|
||||
convertCmd.Flags().Set("recursive", "false")
|
||||
if err := runConvert(convertCmd, []string{repo}); err != nil {
|
||||
t.Fatalf("convert failed for %s: %v", repo, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all were converted
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
if len(cfg.Repos) != 3 {
|
||||
t.Fatalf("expected 3 repos in config, got %d", len(cfg.Repos))
|
||||
}
|
||||
|
||||
// Revert all
|
||||
revertAll = true
|
||||
defer func() { revertAll = false }()
|
||||
|
||||
revertCmd.Flags().Set("all", "true")
|
||||
err = runRevert(revertCmd, []string{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("revert --all failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify all were reverted
|
||||
for _, repo := range []string{repo1, repo2, repo3} {
|
||||
if !git.IsGitRepo(repo) {
|
||||
t.Errorf("expected %s to have .git after revert --all", repo)
|
||||
}
|
||||
|
||||
if git.IsGitLocalRepo(repo) {
|
||||
t.Errorf("expected %s to not have .gitlocal after revert --all", repo)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify config is empty
|
||||
cfg, err = config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != 0 {
|
||||
t.Errorf("expected config to be empty after revert --all, got %d repos", len(cfg.Repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevertNoGitLocal(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
nonGitDir := t.TempDir()
|
||||
|
||||
err := runRevert(revertCmd, []string{nonGitDir})
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected error when reverting directory without .gitlocal")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "no .gitlocal directory") {
|
||||
t.Errorf("expected 'no .gitlocal directory' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create and convert a repo
|
||||
repoDir := testutil.CreateTempGitRepoWithRemote(t, "git@github.com:test/repo.git")
|
||||
convertCmd.Flags().Set("recursive", "false")
|
||||
if err := runConvert(convertCmd, []string{repoDir}); err != nil {
|
||||
t.Fatalf("convert failed: %v", err)
|
||||
}
|
||||
|
||||
// Capture output
|
||||
var buf bytes.Buffer
|
||||
originalStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err := runStatus(statusCmd, []string{})
|
||||
|
||||
w.Close()
|
||||
os.Stdout = originalStdout
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("status command failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify output contains repo info
|
||||
if !strings.Contains(output, repoDir) {
|
||||
t.Errorf("expected output to contain repo path %s", repoDir)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "git@github.com:test/repo.git") {
|
||||
t.Error("expected output to contain remote URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusEmpty(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
// Capture output
|
||||
var buf bytes.Buffer
|
||||
originalStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err := runStatus(statusCmd, []string{})
|
||||
|
||||
w.Close()
|
||||
os.Stdout = originalStdout
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("status command failed: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "No converted repositories") {
|
||||
t.Error("expected 'No converted repositories' message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCurrentDirectory(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
repoDir := testutil.CreateTempGitRepo(t)
|
||||
|
||||
// Change to repo directory
|
||||
originalWd, _ := os.Getwd()
|
||||
os.Chdir(repoDir)
|
||||
defer os.Chdir(originalWd)
|
||||
|
||||
// Run convert without arguments (should use current directory)
|
||||
convertCmd.Flags().Set("recursive", "false")
|
||||
err := runConvert(convertCmd, []string{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("convert current directory failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify conversion
|
||||
if !git.IsGitLocalRepo(repoDir) {
|
||||
t.Error("expected current directory to be converted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationFullWorkflow(t *testing.T) {
|
||||
cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a root repo with nested repos
|
||||
rootDir, nestedDirs := testutil.CreateNestedRepoStructure(t)
|
||||
|
||||
// Step 1: Convert all recursively
|
||||
recursive = true
|
||||
defer func() { recursive = false }()
|
||||
convertCmd.Flags().Set("recursive", "true")
|
||||
|
||||
if err := runConvert(convertCmd, []string{rootDir}); err != nil {
|
||||
t.Fatalf("recursive convert failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify all converted
|
||||
for _, nestedDir := range nestedDirs {
|
||||
if !git.IsGitLocalRepo(nestedDir) {
|
||||
t.Errorf("expected %s to be converted", nestedDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Check status
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != len(nestedDirs) {
|
||||
t.Errorf("expected %d repos in config, got %d", len(nestedDirs), len(cfg.Repos))
|
||||
}
|
||||
|
||||
// Step 3: Revert one repo
|
||||
revertAll = false
|
||||
if err := runRevert(revertCmd, []string{nestedDirs[0]}); err != nil {
|
||||
t.Fatalf("revert single repo failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify it was reverted
|
||||
if !git.IsGitRepo(nestedDirs[0]) {
|
||||
t.Error("expected reverted repo to have .git")
|
||||
}
|
||||
|
||||
// Step 4: Revert all remaining
|
||||
revertAll = true
|
||||
revertCmd.Flags().Set("all", "true")
|
||||
if err := runRevert(revertCmd, []string{}); err != nil {
|
||||
t.Fatalf("revert all failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify all reverted
|
||||
for _, nestedDir := range nestedDirs {
|
||||
if !git.IsGitRepo(nestedDir) {
|
||||
t.Errorf("expected %s to be reverted", nestedDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify config is empty
|
||||
cfg, err = config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != 0 {
|
||||
t.Errorf("expected empty config, got %d repos", len(cfg.Repos))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user