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:
260
internal/config/config_test.go
Normal file
260
internal/config/config_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadConfigNotExists(t *testing.T) {
|
||||
// Override config path to use temp file
|
||||
originalHome := os.Getenv("HOME")
|
||||
tempDir := t.TempDir()
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error when config doesn't exist, got: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Version != ConfigVersion {
|
||||
t.Errorf("expected version %s, got %s", ConfigVersion, cfg.Version)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != 0 {
|
||||
t.Errorf("expected empty repos, got %d repos", len(cfg.Repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigExists(t *testing.T) {
|
||||
originalHome := os.Getenv("HOME")
|
||||
tempDir := t.TempDir()
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
// Create a config file
|
||||
configPath := filepath.Join(tempDir, ConfigFile)
|
||||
configContent := `version: "1"
|
||||
repos:
|
||||
- path: /test/path/1
|
||||
converted_at: 2026-04-07T14:30:00Z
|
||||
original_remote: git@github.com:user/repo1.git
|
||||
original_branch: main
|
||||
- path: /test/path/2
|
||||
converted_at: 2026-04-07T15:00:00Z
|
||||
original_remote: git@github.com:user/repo2.git
|
||||
original_branch: develop
|
||||
`
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Version != "1" {
|
||||
t.Errorf("expected version 1, got %s", cfg.Version)
|
||||
}
|
||||
|
||||
if len(cfg.Repos) != 2 {
|
||||
t.Fatalf("expected 2 repos, got %d", len(cfg.Repos))
|
||||
}
|
||||
|
||||
if cfg.Repos[0].Path != "/test/path/1" {
|
||||
t.Errorf("expected path /test/path/1, got %s", cfg.Repos[0].Path)
|
||||
}
|
||||
|
||||
if cfg.Repos[0].OriginalRemote != "git@github.com:user/repo1.git" {
|
||||
t.Errorf("expected remote git@github.com:user/repo1.git, got %s", cfg.Repos[0].OriginalRemote)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveConfig(t *testing.T) {
|
||||
originalHome := os.Getenv("HOME")
|
||||
tempDir := t.TempDir()
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
cfg := &Config{
|
||||
Version: ConfigVersion,
|
||||
Repos: []Repo{
|
||||
{
|
||||
Path: "/test/repo",
|
||||
ConvertedAt: time.Now(),
|
||||
OriginalRemote: "git@github.com:test/repo.git",
|
||||
OriginalBranch: "main",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := cfg.Save(); err != nil {
|
||||
t.Fatalf("failed to save config: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, ConfigFile)
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
t.Fatalf("config file was not created")
|
||||
}
|
||||
|
||||
// Load it back to verify
|
||||
loadedCfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load saved config: %v", err)
|
||||
}
|
||||
|
||||
if len(loadedCfg.Repos) != 1 {
|
||||
t.Fatalf("expected 1 repo, got %d", len(loadedCfg.Repos))
|
||||
}
|
||||
|
||||
if loadedCfg.Repos[0].Path != "/test/repo" {
|
||||
t.Errorf("expected path /test/repo, got %s", loadedCfg.Repos[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRepo(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Version: ConfigVersion,
|
||||
Repos: []Repo{},
|
||||
}
|
||||
|
||||
repo1 := Repo{
|
||||
Path: "/test/repo1",
|
||||
ConvertedAt: time.Now(),
|
||||
OriginalRemote: "git@github.com:test/repo1.git",
|
||||
OriginalBranch: "main",
|
||||
}
|
||||
|
||||
cfg.AddRepo(repo1)
|
||||
|
||||
if len(cfg.Repos) != 1 {
|
||||
t.Fatalf("expected 1 repo, got %d", len(cfg.Repos))
|
||||
}
|
||||
|
||||
if cfg.Repos[0].Path != "/test/repo1" {
|
||||
t.Errorf("expected path /test/repo1, got %s", cfg.Repos[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRepoReplaceExisting(t *testing.T) {
|
||||
now := time.Now()
|
||||
later := now.Add(1 * time.Hour)
|
||||
|
||||
cfg := &Config{
|
||||
Version: ConfigVersion,
|
||||
Repos: []Repo{
|
||||
{
|
||||
Path: "/test/repo",
|
||||
ConvertedAt: now,
|
||||
OriginalRemote: "git@github.com:test/old.git",
|
||||
OriginalBranch: "main",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add same path again with different data
|
||||
repo := Repo{
|
||||
Path: "/test/repo",
|
||||
ConvertedAt: later,
|
||||
OriginalRemote: "git@github.com:test/new.git",
|
||||
OriginalBranch: "develop",
|
||||
}
|
||||
|
||||
cfg.AddRepo(repo)
|
||||
|
||||
if len(cfg.Repos) != 1 {
|
||||
t.Fatalf("expected 1 repo (replaced), got %d", len(cfg.Repos))
|
||||
}
|
||||
|
||||
if cfg.Repos[0].OriginalRemote != "git@github.com:test/new.git" {
|
||||
t.Errorf("expected new remote, got %s", cfg.Repos[0].OriginalRemote)
|
||||
}
|
||||
|
||||
if cfg.Repos[0].OriginalBranch != "develop" {
|
||||
t.Errorf("expected branch develop, got %s", cfg.Repos[0].OriginalBranch)
|
||||
}
|
||||
|
||||
if !cfg.Repos[0].ConvertedAt.Equal(later) {
|
||||
t.Errorf("expected later timestamp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveRepo(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Version: ConfigVersion,
|
||||
Repos: []Repo{
|
||||
{Path: "/test/repo1"},
|
||||
{Path: "/test/repo2"},
|
||||
{Path: "/test/repo3"},
|
||||
},
|
||||
}
|
||||
|
||||
cfg.RemoveRepo("/test/repo2")
|
||||
|
||||
if len(cfg.Repos) != 2 {
|
||||
t.Fatalf("expected 2 repos, got %d", len(cfg.Repos))
|
||||
}
|
||||
|
||||
for _, repo := range cfg.Repos {
|
||||
if repo.Path == "/test/repo2" {
|
||||
t.Errorf("repo2 should have been removed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveRepoNotFound(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Version: ConfigVersion,
|
||||
Repos: []Repo{
|
||||
{Path: "/test/repo1"},
|
||||
},
|
||||
}
|
||||
|
||||
cfg.RemoveRepo("/test/nonexistent")
|
||||
|
||||
if len(cfg.Repos) != 1 {
|
||||
t.Fatalf("expected 1 repo, got %d", len(cfg.Repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRepo(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Version: ConfigVersion,
|
||||
Repos: []Repo{
|
||||
{
|
||||
Path: "/test/repo1",
|
||||
OriginalRemote: "git@github.com:test/repo1.git",
|
||||
},
|
||||
{
|
||||
Path: "/test/repo2",
|
||||
OriginalRemote: "git@github.com:test/repo2.git",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
repo := cfg.FindRepo("/test/repo1")
|
||||
if repo == nil {
|
||||
t.Fatalf("expected to find repo1")
|
||||
}
|
||||
|
||||
if repo.OriginalRemote != "git@github.com:test/repo1.git" {
|
||||
t.Errorf("expected repo1 remote, got %s", repo.OriginalRemote)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRepoNotFound(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Version: ConfigVersion,
|
||||
Repos: []Repo{
|
||||
{Path: "/test/repo1"},
|
||||
},
|
||||
}
|
||||
|
||||
repo := cfg.FindRepo("/test/nonexistent")
|
||||
if repo != nil {
|
||||
t.Errorf("expected nil for nonexistent repo, got %v", repo)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user