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`.
220 lines
5.1 KiB
Go
220 lines
5.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.membo.co.uk/dtomlinson/gitlocal/internal/config"
|
|
"git.membo.co.uk/dtomlinson/gitlocal/internal/git"
|
|
"git.membo.co.uk/dtomlinson/gitlocal/internal/scanner"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
recursive bool
|
|
dryRun bool
|
|
)
|
|
|
|
var convertCmd = &cobra.Command{
|
|
Use: "convert [path]",
|
|
Short: "Convert .git to .gitlocal",
|
|
Long: `Convert renames .git to .gitlocal in the specified directory.
|
|
If no path is provided, uses the current directory.
|
|
|
|
Use --recursive to scan and convert all nested git repositories.`,
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: runConvert,
|
|
}
|
|
|
|
func init() {
|
|
convertCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Recursively convert all nested repos")
|
|
convertCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be converted without making changes")
|
|
}
|
|
|
|
func runConvert(cmd *cobra.Command, args []string) error {
|
|
// Determine target path
|
|
targetPath := "."
|
|
if len(args) > 0 {
|
|
targetPath = args[0]
|
|
}
|
|
|
|
// Get absolute path
|
|
absPath, err := filepath.Abs(targetPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get absolute path: %w", err)
|
|
}
|
|
|
|
// Load config
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
}
|
|
|
|
if recursive {
|
|
return convertRecursive(absPath, cfg)
|
|
}
|
|
|
|
return convertSingle(absPath, cfg)
|
|
}
|
|
|
|
func convertSingle(path string, cfg *config.Config) error {
|
|
// Check if already converted
|
|
if git.IsGitLocalRepo(path) {
|
|
// Check if already in config
|
|
if existingRepo := cfg.FindRepo(path); existingRepo != nil {
|
|
fmt.Printf("⊘ Already converted and tracked: %s\n", path)
|
|
return nil
|
|
}
|
|
|
|
// Add to config
|
|
gitLocalPath := filepath.Join(path, git.GitLocalDir)
|
|
remoteURL := git.GetRemoteURL(gitLocalPath)
|
|
branch := git.GetCurrentBranch(gitLocalPath)
|
|
|
|
cfg.AddRepo(config.Repo{
|
|
Path: path,
|
|
ConvertedAt: time.Now(),
|
|
OriginalRemote: remoteURL,
|
|
OriginalBranch: branch,
|
|
})
|
|
|
|
if err := cfg.Save(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
fmt.Printf("✓ Registered already-converted repo: %s\n", path)
|
|
if remoteURL != "" {
|
|
fmt.Printf(" Remote: %s\n", remoteURL)
|
|
}
|
|
if branch != "" {
|
|
fmt.Printf(" Branch: %s\n", branch)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if !git.IsGitRepo(path) {
|
|
return fmt.Errorf("no .git directory found at %s", path)
|
|
}
|
|
|
|
if dryRun {
|
|
fmt.Printf("[DRY RUN] Would convert: %s\n", path)
|
|
return nil
|
|
}
|
|
|
|
// Get git info before converting
|
|
gitPath := filepath.Join(path, git.GitDir)
|
|
remoteURL := git.GetRemoteURL(gitPath)
|
|
branch := git.GetCurrentBranch(gitPath)
|
|
|
|
// Convert
|
|
if err := git.Convert(path); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update config
|
|
cfg.AddRepo(config.Repo{
|
|
Path: path,
|
|
ConvertedAt: time.Now(),
|
|
OriginalRemote: remoteURL,
|
|
OriginalBranch: branch,
|
|
})
|
|
|
|
if err := cfg.Save(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
fmt.Printf("✓ Converted: %s\n", path)
|
|
if remoteURL != "" {
|
|
fmt.Printf(" Remote: %s\n", remoteURL)
|
|
}
|
|
if branch != "" {
|
|
fmt.Printf(" Branch: %s\n", branch)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func convertRecursive(rootPath string, cfg *config.Config) error {
|
|
repos, err := scanner.FindNestedGitRepos(rootPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to scan for nested repos: %w", err)
|
|
}
|
|
|
|
if len(repos) == 0 {
|
|
fmt.Println("No nested git repositories found")
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("Found %d nested git repositories\n\n", len(repos))
|
|
|
|
converted := 0
|
|
registered := 0
|
|
for _, repoPath := range repos {
|
|
if git.IsGitLocalRepo(repoPath) {
|
|
// Check if already in config
|
|
if existingRepo := cfg.FindRepo(repoPath); existingRepo != nil {
|
|
fmt.Printf("⊘ Skipped (already converted and tracked): %s\n", repoPath)
|
|
continue
|
|
}
|
|
|
|
// Register already-converted repo
|
|
gitLocalPath := filepath.Join(repoPath, git.GitLocalDir)
|
|
remoteURL := git.GetRemoteURL(gitLocalPath)
|
|
branch := git.GetCurrentBranch(gitLocalPath)
|
|
|
|
cfg.AddRepo(config.Repo{
|
|
Path: repoPath,
|
|
ConvertedAt: time.Now(),
|
|
OriginalRemote: remoteURL,
|
|
OriginalBranch: branch,
|
|
})
|
|
|
|
fmt.Printf("✓ Registered already-converted: %s\n", repoPath)
|
|
registered++
|
|
continue
|
|
}
|
|
|
|
if dryRun {
|
|
fmt.Printf("[DRY RUN] Would convert: %s\n", repoPath)
|
|
continue
|
|
}
|
|
|
|
// Get git info before converting
|
|
gitPath := filepath.Join(repoPath, git.GitDir)
|
|
remoteURL := git.GetRemoteURL(gitPath)
|
|
branch := git.GetCurrentBranch(gitPath)
|
|
|
|
// Convert
|
|
if err := git.Convert(repoPath); err != nil {
|
|
fmt.Fprintf(os.Stderr, "✗ Failed to convert %s: %v\n", repoPath, err)
|
|
continue
|
|
}
|
|
|
|
// Update config
|
|
cfg.AddRepo(config.Repo{
|
|
Path: repoPath,
|
|
ConvertedAt: time.Now(),
|
|
OriginalRemote: remoteURL,
|
|
OriginalBranch: branch,
|
|
})
|
|
|
|
fmt.Printf("✓ Converted: %s\n", repoPath)
|
|
converted++
|
|
}
|
|
|
|
if !dryRun && (converted > 0 || registered > 0) {
|
|
if err := cfg.Save(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
fmt.Printf("\nSuccessfully converted %d repositories", converted)
|
|
if registered > 0 {
|
|
fmt.Printf(", registered %d already-converted", registered)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
return nil
|
|
}
|