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:
219
cmd/convert.go
Normal file
219
cmd/convert.go
Normal file
@@ -0,0 +1,219 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user