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`.
261 lines
5.6 KiB
Go
261 lines
5.6 KiB
Go
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)
|
|
}
|
|
}
|