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:
2026-04-11 14:48:01 +01:00
parent c9867f410f
commit b5f1495680
16 changed files with 2263 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
package scanner
import (
"os"
"path/filepath"
"git.membo.co.uk/dtomlinson/gitlocal/internal/git"
)
// FindNestedGitRepos recursively scans rootPath for nested .git and .gitlocal directories
// and returns a list of absolute paths to directories containing .git or .gitlocal
// It skips the root repo's .git/.gitlocal directory if one exists
func FindNestedGitRepos(rootPath string) ([]string, error) {
var repos []string
// Get absolute path
absRoot, err := filepath.Abs(rootPath)
if err != nil {
return nil, err
}
// Check if root itself is a git repo (either .git or .gitlocal)
rootIsGitRepo := git.IsGitRepo(absRoot) || git.IsGitLocalRepo(absRoot)
rootGitPath := filepath.Join(absRoot, git.GitDir)
rootGitLocalPath := filepath.Join(absRoot, git.GitLocalDir)
err = filepath.Walk(absRoot, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip if not a directory
if !info.IsDir() {
return nil
}
// Skip if this is the root's .git or .gitlocal directory
if rootIsGitRepo && (path == rootGitPath || path == rootGitLocalPath) {
return filepath.SkipDir
}
// Check if this directory is named .git or .gitlocal
if info.Name() == git.GitDir || info.Name() == git.GitLocalDir {
// Get the parent directory (the actual repo path)
repoPath := filepath.Dir(path)
repos = append(repos, repoPath)
// Skip descending into .git/.gitlocal directory
return filepath.SkipDir
}
return nil
})
if err != nil {
return nil, err
}
return repos, nil
}