28 Commits

Author SHA1 Message Date
6bd40cb1d2 Merge pull request #4 from taigrr/cd/dep-updates-go126
feat(cmd): implement fetch and status commands, update deps
2026-03-07 02:43:48 -05:00
348b87702d feat(cmd): implement fetch and status commands, update deps to go1.26.1
- Implement fetch command: fetches all registered repos without merging,
  supports parallel execution via -j/--jobs flag
- Implement status command: shows uncommitted changes across all repos
  with per-file-type counts, supports parallel execution
- Update Go to 1.26.1
- Update go-git v5.16.5 -> v5.17.0, go-crypto v1.3.0 -> v1.4.0,
  go-billy v5.7.0 -> v5.8.0, x/net v0.50.0 -> v0.51.0
- Fix unregister command description (said 'add' instead of 'remove')
- Fix goimports formatting in mgconf_test.go
2026-03-07 07:03:49 +00:00
9d4ac7bc47 Merge pull request #2 from taigrr/burrow/refactor-alias-loading
refactor: dynamically load all [DEFAULT] section aliases
2026-02-23 00:04:21 -05:00
a4044ba47a Merge pull request #3 from taigrr/burrow/dep-update-feb-2026
chore(deps): update Go dependencies
2026-02-22 20:59:39 -05:00
e579599876 chore(deps): update Go dependencies
Updated:
- go-git/go-git v5.16.4 → v5.16.5
- cloudflare/circl v1.6.2 → v1.6.3
- kevinburke/ssh_config v1.4.0 → v1.6.0
- golang.org/x/crypto v0.47.0 → v0.48.0
- golang.org/x/net v0.49.0 → v0.50.0
- golang.org/x/sys v0.40.0 → v0.41.0
- Removed unused golang.org/x/mod and golang.org/x/tools
2026-02-19 08:01:55 +00:00
9ad9412a8c refactor: dynamically load all [DEFAULT] section aliases
Removed hardcoded alias handling (unregister, git_gc) in favor of
dynamically loading all key-value pairs from the [DEFAULT] section
into the Aliases map. This allows mrconfig files to define custom
aliases without code changes.

- Simplified switch statement in LoadMRConfig
- Updated function documentation
- All tests passing
2026-02-12 13:33:30 +00:00
50f5bc897d update go deps 2026-01-18 02:30:38 -05:00
0f925f852e update env var expansion logic 2026-01-18 02:29:26 -05:00
cacdbf673f update to use /Users/tai 2025-08-10 15:15:50 -07:00
Tai Groot
991985ab4e update modeperm for directory creation on clone 2024-09-16 12:12:04 -07:00
417cf943fa fixup help text 2024-09-15 13:08:02 -07:00
1030a8f3a9 add cloning 2024-09-15 13:01:55 -07:00
f0c6f3906a fix print statements 2024-02-02 16:28:41 -08:00
01c736b54e fixup clone error 2024-02-02 16:24:06 -08:00
e93a0489d3 register clone cmd 2024-02-02 13:32:06 -08:00
567ab899e0 fix conflict 2024-02-02 13:29:27 -08:00
a889092b0d update dependencies 2024-02-02 12:29:47 -08:00
1a593ad588 update go mod 2024-01-08 09:22:41 -08:00
c5310d13b0 update deps 2023-10-14 00:00:03 -07:00
256e82cca2 update deps 2023-09-08 23:06:16 -07:00
b99b1eabec add stringer for stats 2023-08-28 18:22:55 -07:00
f503d4b0eb fix import registration 2023-08-28 18:11:17 -07:00
f368381458 update crypto deps 2023-08-28 18:09:21 -07:00
a5817c554b add merge functionality to mg 2023-08-28 18:08:35 -07:00
15122346d1 upgrade go git deps 2023-08-23 16:10:14 -07:00
c2d67df8f0 mod tidy 2023-07-24 23:53:27 -07:00
84d7a73202 upgrade to fix github.com/go-git/go-git/issues/328 2023-07-24 23:52:34 -07:00
662d80fbf5 ignore built binary 2023-07-03 16:36:01 -07:00
18 changed files with 1351 additions and 116 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
cmd/mg/mg
.crush

234
AGENTS.md Normal file
View File

@@ -0,0 +1,234 @@
# AGENTS.md
Agent guide for the `mg` codebase - a Go replacement for [myrepos](https://myrepos.branchable.com/) that only supports git repos.
## Project Overview
`mg` is a CLI tool for managing multiple git repositories simultaneously. It uses `go-git/go-git` for pure Go git operations (no external git dependency required) and `spf13/cobra` for CLI structure.
### Key Features
- Parallel operations via `-j` flag
- Compatible with existing `~/.mrconfig` files (auto-migrates to `mgconfig`)
- Pure Go implementation - no external git binary needed
- Embeddable as a library
## Commands
```bash
# Build
go build ./...
# Run tests
go test ./...
# Install the binary
go install ./cmd/mg
# Run directly
go run ./cmd/mg <command>
```
## Project Structure
```
mg/
├── cmd/
│ ├── mg/
│ │ ├── main.go # Entry point
│ │ └── cmd/ # Cobra commands
│ │ ├── root.go # Root command setup
│ │ ├── common.go # Shared utilities (GetConfig)
│ │ ├── clone.go # Clone all repos (implemented)
│ │ ├── pull.go # Pull all repos (implemented)
│ │ ├── register.go # Register repo (implemented)
│ │ ├── unregister.go# Unregister repo (implemented)
│ │ ├── import.go # Merge configs (implemented)
│ │ ├── push.go # Stub
│ │ ├── fetch.go # Stub
│ │ ├── status.go # Stub
│ │ ├── diff.go # Stub
│ │ ├── commit.go # Stub
│ │ └── config.go # Stub
│ └── paths/
│ └── mrpaths.go # Utility to list repo paths from mrconfig
└── parse/
├── mgconf.go # MGConfig: JSON-based config format
├── myrepos.go # MRConfig: Parse ~/.mrconfig (INI-style)
└── myrepos_test.go # Tests (skeleton)
```
## Implementation Status
| Command | Status | Notes |
|--------------|-------------|------------------------------------|
| `clone` | Implemented | Parallel via `-j`, creates dirs |
| `pull` | Implemented | Parallel via `-j` |
| `register` | Implemented | Detects git root, stores `$HOME` |
| `unregister` | Implemented | By path or current dir |
| `import` | Implemented | Merge configs, supports stdin `-` |
| `push` | Stub | Prints "push called" |
| `fetch` | Stub | Prints "fetch called" |
| `status` | Stub | Prints "status called" |
| `diff` | Stub | Prints "diff called" |
| `commit` | Stub | Prints "commit called" |
| `config` | Stub | Prints "config called" |
## Configuration
### Config File Location
1. `$MGCONFIG` environment variable (if set)
2. `$XDG_CONFIG_HOME/mgconfig`
3. `~/.config/mgconfig`
### Config Format (JSON)
```json
{
"Repos": [
{
"Path": "$HOME/code/project",
"Remote": "git@github.com:user/project.git"
}
],
"Aliases": {}
}
```
### Migration from myrepos
If no `mgconfig` exists but `~/.mrconfig` does, `mg` auto-migrates on first run. The `MRConfig.ToMGConfig()` method handles conversion.
## Code Patterns
### Adding a New Command
1. Create `cmd/mg/cmd/<name>.go`
2. Define a `cobra.Command` variable
3. Register in `init()` via `rootCmd.AddCommand()`
4. For parallel operations, follow the pattern in `clone.go` or `pull.go`:
```go
var myCmd = &cobra.Command{
Use: "mycommand",
Short: "description",
Run: func(_ *cobra.Command, args []string) {
conf := GetConfig() // Load config with fallback to mrconfig
// Implementation...
},
}
func init() {
rootCmd.AddCommand(myCmd)
myCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "number of parallel jobs")
}
```
### Parallel Execution Pattern
Used in `clone.go` and `pull.go`:
```go
repoChan := make(chan RepoType, len(repos))
wg := sync.WaitGroup{}
mutex := sync.Mutex{}
errs := []Error{}
wg.Add(len(repos))
for i := 0; i < jobs; i++ {
go func() {
for repo := range repoChan {
// Do work
// Use mutex for shared state (errs, counters)
wg.Done()
}
}()
}
for _, repo := range repos {
repoChan <- repo
}
close(repoChan)
wg.Wait()
```
### Git Operations
Use `go-git/go-git/v5`:
```go
import git "github.com/go-git/go-git/v5"
// Open repo (detects .git in parent dirs)
r, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{DetectDotGit: true})
// Clone
_, err = git.PlainClone(path, false, &git.CloneOptions{URL: remote})
// Pull
w, _ := r.Worktree()
err = w.Pull(&git.PullOptions{})
// Check: err == git.NoErrAlreadyUpToDate
```
### Path Handling
- Paths in config use `$HOME` prefix for portability
- `GetConfig()` in `common.go` expands `$HOME` to actual home directory at runtime
- `register` command stores paths with `$HOME` prefix
## Key Types
### `parse.MGConfig`
```go
type MGConfig struct {
Repos []Repo
Aliases map[string]string
}
// Methods
func LoadMGConfig() (MGConfig, error)
func (m *MGConfig) AddRepo(path, remote string) error
func (m *MGConfig) DelRepo(path string) error
func (m *MGConfig) Merge(m2 MGConfig) (Stats, error)
func (m MGConfig) Save() error
```
### `parse.Repo`
```go
type Repo struct {
Path string
Remote string
Aliases map[string]string `json:"aliases,omitempty"`
}
```
## Dependencies
- `github.com/go-git/go-git/v5` - Pure Go git implementation
- `github.com/spf13/cobra` - CLI framework
## Testing
Tests are minimal. Only `parse/myrepos_test.go` exists with a skeleton structure:
```bash
go test ./...
```
## Known Issues / TODOs
1. Several commands are stubs (push, fetch, status, diff, commit, config)
2. `parse/mgconf.go:61` has a hint about inefficient string concatenation in a loop
3. Test coverage is minimal
4. `unregister` command short description incorrectly says "add current path" (copy-paste error)
## Error Handling Pattern
Commands typically:
1. Log errors via `log.Println(err)`
2. Exit with `os.Exit(1)` on fatal errors
3. Collect errors during parallel operations and report summary at end

1
cmd/mg/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
main

106
cmd/mg/cmd/clone.go Normal file
View File

@@ -0,0 +1,106 @@
package cmd
import (
"fmt"
"log"
"os"
"path/filepath"
"sync"
git "github.com/go-git/go-git/v5"
"github.com/spf13/cobra"
"github.com/taigrr/mg/parse"
)
// cloneCmd represents the clone command
var (
cloneCmd = &cobra.Command{
Use: "clone",
Short: "ensure all repos defined in the config are cloned",
Run: func(_ *cobra.Command, args []string) {
type RepoError struct {
Error error
Repo string
}
if jobs < 1 {
log.Println("jobs must be greater than 0")
os.Exit(1)
}
conf := GetConfig()
if len(args) > 0 {
log.Println("too many arguments")
os.Exit(1)
}
repoChan := make(chan parse.Repo, len(conf.Repos))
errs := []RepoError{}
alreadyCloned := 0
mutex := sync.Mutex{}
wg := sync.WaitGroup{}
wg.Add(len(conf.Repos))
cloneFunc := func() {
for repo := range repoChan {
_, err := git.PlainOpenWithOptions(repo.Path, &(git.PlainOpenOptions{DetectDotGit: true}))
if err == nil {
log.Printf("already cloned: %s\n", repo.Path)
mutex.Lock()
alreadyCloned++
mutex.Unlock()
wg.Done()
continue
} else if err == git.ErrRepositoryNotExists {
log.Printf("attempting clone: %s\n", repo.Path)
parentPath := filepath.Dir(repo.Path)
if _, err := os.Stat(parentPath); err != nil {
os.MkdirAll(parentPath, os.ModeDir|os.ModePerm)
}
_, err = git.PlainClone(repo.Path, false, &git.CloneOptions{
URL: repo.Remote,
})
if err != nil {
mutex.Lock()
errs = append(errs, RepoError{Error: err, Repo: repo.Path})
mutex.Unlock()
log.Printf("clone failed for %s: %v\n", repo.Path, err)
wg.Done()
continue
}
fmt.Printf("successfully cloned %s\n", repo.Path)
wg.Done()
continue
} else {
mutex.Lock()
errs = append(errs, RepoError{Error: err, Repo: repo.Path})
mutex.Unlock()
log.Printf("clone failed for %s: %v\n", repo.Path, err)
wg.Done()
continue
}
}
}
for i := 0; i < jobs; i++ {
go cloneFunc()
}
fmt.Println(len(conf.Repos))
for _, repo := range conf.Repos {
repoChan <- repo
}
close(repoChan)
fmt.Println("waiting...")
wg.Wait()
for _, err := range errs {
log.Printf("error pulling %s: %s\n", err.Repo, err.Error)
}
lenErrs := len(errs)
fmt.Println()
fmt.Printf("successfully cloned %d/%d repos\n", len(conf.Repos)-lenErrs, len(conf.Repos))
fmt.Printf("%d repos already cloned\n", alreadyCloned)
fmt.Printf("failed to clone %d/%d repos\n", lenErrs, len(conf.Repos))
},
}
)
func init() {
rootCmd.AddCommand(cloneCmd)
cloneCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "number of jobs to run in parallel")
}

View File

@@ -10,7 +10,7 @@ import (
var commitCmd = &cobra.Command{
Use: "commit",
Short: "commit all current repos with the same message",
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, args []string) {
fmt.Println("commit called")
},
}

View File

@@ -27,5 +27,6 @@ func GetConfig() parse.MGConfig {
}
}
}
conf.ExpandPaths()
return conf
}

View File

@@ -2,18 +2,90 @@ package cmd
import (
"fmt"
"log"
"os"
"sync"
git "github.com/go-git/go-git/v5"
"github.com/spf13/cobra"
)
var fetchCmd = &cobra.Command{
Use: "fetch",
Short: "fetch all git repos without merging",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("fetch called")
Run: func(_ *cobra.Command, args []string) {
type RepoError struct {
Error error
Repo string
}
if jobs < 1 {
log.Println("jobs must be greater than 0")
os.Exit(1)
}
conf := GetConfig()
if len(args) > 0 {
log.Println("too many arguments")
os.Exit(1)
}
repoChan := make(chan string, len(conf.Repos))
var (
errs []RepoError
alreadyFetched int
mutex sync.Mutex
wg sync.WaitGroup
)
wg.Add(len(conf.Repos))
for i := 0; i < jobs; i++ {
go func() {
for repo := range repoChan {
log.Printf("attempting fetch: %s\n", repo)
r, err := git.PlainOpenWithOptions(repo, &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
mutex.Lock()
errs = append(errs, RepoError{Error: err, Repo: repo})
mutex.Unlock()
log.Printf("fetch failed for %s: %v\n", repo, err)
wg.Done()
continue
}
err = r.Fetch(&git.FetchOptions{})
if err == git.NoErrAlreadyUpToDate {
mutex.Lock()
alreadyFetched++
mutex.Unlock()
fmt.Printf("repo %s: already up to date\n", repo)
wg.Done()
continue
} else if err != nil {
mutex.Lock()
errs = append(errs, RepoError{Error: err, Repo: repo})
mutex.Unlock()
log.Printf("fetch failed for %s: %v\n", repo, err)
wg.Done()
continue
}
fmt.Printf("successfully fetched %s\n", repo)
wg.Done()
}
}()
}
for _, repo := range conf.Repos {
repoChan <- repo.Path
}
close(repoChan)
wg.Wait()
for _, err := range errs {
log.Printf("error fetching %s: %s\n", err.Repo, err.Error)
}
lenErrs := len(errs)
fmt.Println()
fmt.Printf("successfully fetched %d/%d repos\n", len(conf.Repos)-lenErrs, len(conf.Repos))
fmt.Printf("%d repos already up to date\n", alreadyFetched)
fmt.Printf("failed to fetch %d/%d repos\n", lenErrs, len(conf.Repos))
},
}
func init() {
rootCmd.AddCommand(fetchCmd)
fetchCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "number of jobs to run in parallel")
}

64
cmd/mg/cmd/import.go Normal file
View File

@@ -0,0 +1,64 @@
package cmd
import (
"fmt"
"io"
"log"
"os"
"github.com/spf13/cobra"
"github.com/taigrr/mg/parse"
)
var importCmd = &cobra.Command{
Use: "import <file>",
Short: "merge a new mgconfig into the current one",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
conf := GetConfig()
if args[0] == "-" {
f, err := io.ReadAll(os.Stdin)
if err != nil {
log.Println(err)
os.Exit(1)
}
parsed, err := parse.ParseMGConfig(f)
if err != nil {
log.Println(err)
os.Exit(1)
}
stats, err := conf.Merge(parsed)
if err != nil {
log.Println(err)
os.Exit(1)
}
fmt.Println(stats)
} else {
f, err := os.ReadFile(args[0])
if err != nil {
log.Println(err)
os.Exit(1)
}
parsed, err := parse.ParseMGConfig(f)
if err != nil {
log.Println(err)
os.Exit(1)
}
stats, err := conf.Merge(parsed)
if err != nil {
log.Println(err)
os.Exit(1)
}
fmt.Println(stats)
}
err := conf.Save()
if err != nil {
log.Println(err)
os.Exit(1)
}
},
}
func init() {
rootCmd.AddCommand(importCmd)
}

View File

@@ -15,7 +15,7 @@ var (
jobs int
pullCmd = &cobra.Command{
Use: "pull",
Short: "add current path to list of repos",
Short: "update all git repos specified in config",
Run: func(_ *cobra.Command, args []string) {
type RepoError struct {
Error error

View File

@@ -52,6 +52,7 @@ var registerCmd = &cobra.Command{
os.Exit(1)
}
path = newPath.Filesystem.Root()
for _, v := range conf.Repos {
if v.Path == path {
fmt.Printf("repo %s already registered\n", path)

View File

@@ -2,14 +2,44 @@ package cmd
import (
"os"
"runtime/debug"
"github.com/spf13/cobra"
)
func getVersion() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return "dev"
}
if info.Main.Version != "" && info.Main.Version != "(devel)" {
return info.Main.Version
}
var revision, dirty string
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
revision = s.Value
case "vcs.modified":
if s.Value == "true" {
dirty = "-dirty"
}
}
}
if revision != "" {
if len(revision) > 7 {
revision = revision[:7]
}
return revision + dirty
}
return "dev"
}
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "mg",
Short: "go replacement for myrepos which only supports git repos",
Version: getVersion(),
}
// Execute adds all child commands to the root command and sets flags appropriately.

View File

@@ -2,19 +2,156 @@ package cmd
import (
"fmt"
"log"
"os"
"sort"
"sync"
git "github.com/go-git/go-git/v5"
"github.com/spf13/cobra"
)
// statusCmd represents the status command
type repoStatus struct {
Path string
Modified int
Added int
Deleted int
Renamed int
Copied int
Untrack int
Clean bool
}
var statusCmd = &cobra.Command{
Use: "status",
Short: "get the combined git status for all git repos",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("status called")
Run: func(_ *cobra.Command, args []string) {
type RepoError struct {
Error error
Repo string
}
if jobs < 1 {
log.Println("jobs must be greater than 0")
os.Exit(1)
}
conf := GetConfig()
if len(args) > 0 {
log.Println("too many arguments")
os.Exit(1)
}
repoChan := make(chan string, len(conf.Repos))
var (
errs []RepoError
statuses []repoStatus
mutex sync.Mutex
wg sync.WaitGroup
)
wg.Add(len(conf.Repos))
for i := 0; i < jobs; i++ {
go func() {
for repo := range repoChan {
r, err := git.PlainOpenWithOptions(repo, &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
mutex.Lock()
errs = append(errs, RepoError{Error: err, Repo: repo})
mutex.Unlock()
wg.Done()
continue
}
w, err := r.Worktree()
if err != nil {
mutex.Lock()
errs = append(errs, RepoError{Error: err, Repo: repo})
mutex.Unlock()
wg.Done()
continue
}
st, err := w.Status()
if err != nil {
mutex.Lock()
errs = append(errs, RepoError{Error: err, Repo: repo})
mutex.Unlock()
wg.Done()
continue
}
rs := repoStatus{Path: repo, Clean: st.IsClean()}
for _, s := range st {
code := s.Worktree
if code == git.Unmodified {
code = s.Staging
}
switch code {
case git.Modified:
rs.Modified++
case git.Added:
rs.Added++
case git.Deleted:
rs.Deleted++
case git.Renamed:
rs.Renamed++
case git.Copied:
rs.Copied++
case git.Untracked:
rs.Untrack++
}
}
mutex.Lock()
statuses = append(statuses, rs)
mutex.Unlock()
wg.Done()
}
}()
}
for _, repo := range conf.Repos {
repoChan <- repo.Path
}
close(repoChan)
wg.Wait()
sort.Slice(statuses, func(i, j int) bool {
return statuses[i].Path < statuses[j].Path
})
dirtyCount := 0
for _, rs := range statuses {
if rs.Clean {
continue
}
dirtyCount++
fmt.Printf("%s:\n", rs.Path)
if rs.Modified > 0 {
fmt.Printf(" modified: %d\n", rs.Modified)
}
if rs.Added > 0 {
fmt.Printf(" added: %d\n", rs.Added)
}
if rs.Deleted > 0 {
fmt.Printf(" deleted: %d\n", rs.Deleted)
}
if rs.Renamed > 0 {
fmt.Printf(" renamed: %d\n", rs.Renamed)
}
if rs.Copied > 0 {
fmt.Printf(" copied: %d\n", rs.Copied)
}
if rs.Untrack > 0 {
fmt.Printf(" untracked: %d\n", rs.Untrack)
}
}
for _, err := range errs {
log.Printf("error reading %s: %s\n", err.Repo, err.Error)
}
fmt.Println()
fmt.Printf("%d/%d repos have uncommitted changes\n", dirtyCount, len(conf.Repos))
if len(errs) > 0 {
fmt.Printf("failed to read %d/%d repos\n", len(errs), len(conf.Repos))
}
},
}
func init() {
rootCmd.AddCommand(statusCmd)
statusCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "number of jobs to run in parallel")
}

View File

@@ -11,7 +11,7 @@ import (
// unregisterCmd represents the unregister command
var unregisterCmd = &cobra.Command{
Use: "unregister",
Short: "add current path to list of repos",
Short: "remove current path from list of repos",
Run: func(_ *cobra.Command, args []string) {
conf := GetConfig()
path, err := os.Getwd()

32
go.mod
View File

@@ -1,27 +1,33 @@
module github.com/taigrr/mg
go 1.20
go 1.26.1
require (
github.com/go-git/go-git/v5 v5.3.0
github.com/spf13/cobra v1.7.0
github.com/go-git/go-git/v5 v5.17.0
github.com/spf13/cobra v1.10.2
)
require (
github.com/Microsoft/go-winio v0.5.2 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/go-git/go-billy/v5 v5.8.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

136
go.sum
View File

@@ -1,122 +1,114 @@
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GWc=
github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,9 +2,10 @@ package parse
import (
"encoding/json"
"io/ioutil"
"fmt"
"os"
"path/filepath"
"strings"
)
var errAlreadyRegistered = os.ErrExist
@@ -50,11 +51,44 @@ func (m *MGConfig) AddRepo(path, remote string) error {
return nil
}
type Stats struct {
Duplicates int
NewPaths []string
}
func (s Stats) String() string {
str := ""
for _, v := range s.NewPaths {
str += "Added repo " + v + "\n"
}
str += "\nAdded " + fmt.Sprintf("%d", len(s.NewPaths)) + " new repos\n"
str += "Skipped " + fmt.Sprintf("%d", s.Duplicates) + " duplicate repos"
return str
}
func (m *MGConfig) Merge(m2 MGConfig) (Stats, error) {
stats := Stats{}
for _, v := range m2.Repos {
err := m.AddRepo(v.Path, v.Remote)
switch err {
case errAlreadyRegistered:
stats.Duplicates++
continue
case nil:
stats.NewPaths = append(stats.NewPaths, v.Path)
continue
default:
return stats, err
}
}
return stats, nil
}
// LoadMGConfig loads the mgconfig file from the XDG_CONFIG_HOME directory
// or from the default location of $HOME/.config/mgconfig
// If the file is not found, an error is returned
func LoadMGConfig() (MGConfig, error) {
var config MGConfig
mgConf := os.Getenv("MGCONFIG")
if mgConf == "" {
confDir := os.Getenv("XDG_CONFIG_HOME")
@@ -74,11 +108,38 @@ func LoadMGConfig() (MGConfig, error) {
if err != nil {
return MGConfig{}, err
}
err = json.Unmarshal(file, &config)
return ParseMGConfig(file)
}
// ParseMGConfig parses the mgconfig file from a byte slice
func ParseMGConfig(b []byte) (MGConfig, error) {
var config MGConfig
err := json.Unmarshal(b, &config)
return config, err
}
// ExpandPaths expands shell variables in all repo paths using os.ExpandEnv.
// This allows paths like $HOME/code or $GOPATH/src to work cross-platform.
func (m *MGConfig) ExpandPaths() {
for i := range m.Repos {
m.Repos[i].Path = os.ExpandEnv(m.Repos[i].Path)
}
}
// CollapsePaths replaces the user's home directory with $HOME in all repo paths.
// This allows config files to be shared across machines with different home paths.
func (m *MGConfig) CollapsePaths() {
home, err := os.UserHomeDir()
if err != nil || home == "" {
return
}
for i := range m.Repos {
if strings.HasPrefix(m.Repos[i].Path, home) {
m.Repos[i].Path = "$HOME" + m.Repos[i].Path[len(home):]
}
}
}
func (m MGConfig) Save() error {
mgConf := os.Getenv("MGCONFIG")
if mgConf == "" {
@@ -95,9 +156,17 @@ func (m MGConfig) Save() error {
}
mgConf = filepath.Join(confDir, "mgconfig")
}
b, err := json.MarshalIndent(m, "", " ")
// Collapse paths before saving so config is portable
toSave := MGConfig{
Repos: make([]Repo, len(m.Repos)),
Aliases: m.Aliases,
}
copy(toSave.Repos, m.Repos)
toSave.CollapsePaths()
b, err := json.MarshalIndent(toSave, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(mgConf, b, 0o644)
return os.WriteFile(mgConf, b, 0o644)
}

530
parse/mgconf_test.go Normal file
View File

@@ -0,0 +1,530 @@
package parse
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestExpandPaths(t *testing.T) {
// Set up test environment variables
t.Setenv("HOME", "/home/testuser")
t.Setenv("GOPATH", "/home/testuser/go")
t.Setenv("CUSTOM_VAR", "/custom/path")
tests := []struct {
name string
input []Repo
expected []string
}{
{
name: "expand $HOME",
input: []Repo{
{Path: "$HOME/code/project", Remote: "git@github.com:user/project.git"},
},
expected: []string{"/home/testuser/code/project"},
},
{
name: "expand multiple variables",
input: []Repo{
{Path: "$HOME/code/project", Remote: "git@github.com:user/project.git"},
{Path: "$GOPATH/src/github.com/user/repo", Remote: "git@github.com:user/repo.git"},
},
expected: []string{
"/home/testuser/code/project",
"/home/testuser/go/src/github.com/user/repo",
},
},
{
name: "expand custom variable",
input: []Repo{
{Path: "$CUSTOM_VAR/subdir", Remote: "git@github.com:user/project.git"},
},
expected: []string{"/custom/path/subdir"},
},
{
name: "no expansion needed",
input: []Repo{
{Path: "/absolute/path/to/repo", Remote: "git@github.com:user/project.git"},
},
expected: []string{"/absolute/path/to/repo"},
},
{
name: "empty repos",
input: []Repo{},
expected: []string{},
},
{
name: "undefined variable stays as-is",
input: []Repo{
{Path: "$UNDEFINED_VAR/code", Remote: "git@github.com:user/project.git"},
},
expected: []string{"/code"}, // undefined vars expand to empty string
},
{
name: "braced variable syntax",
input: []Repo{
{Path: "${HOME}/code/project", Remote: "git@github.com:user/project.git"},
},
expected: []string{"/home/testuser/code/project"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := MGConfig{Repos: tt.input}
conf.ExpandPaths()
if len(conf.Repos) != len(tt.expected) {
t.Fatalf("expected %d repos, got %d", len(tt.expected), len(conf.Repos))
}
for i, repo := range conf.Repos {
if repo.Path != tt.expected[i] {
t.Errorf("repo %d: expected path %q, got %q", i, tt.expected[i], repo.Path)
}
}
})
}
}
func TestCollapsePaths(t *testing.T) {
// Get the actual home directory for this test
home, err := os.UserHomeDir()
if err != nil {
t.Fatalf("failed to get home directory: %v", err)
}
tests := []struct {
name string
input []Repo
expected []string
}{
{
name: "collapse home directory",
input: []Repo{
{Path: filepath.Join(home, "code/project"), Remote: "git@github.com:user/project.git"},
},
expected: []string{"$HOME/code/project"},
},
{
name: "collapse multiple paths",
input: []Repo{
{Path: filepath.Join(home, "code/project1"), Remote: "git@github.com:user/project1.git"},
{Path: filepath.Join(home, "code/project2"), Remote: "git@github.com:user/project2.git"},
},
expected: []string{
"$HOME/code/project1",
"$HOME/code/project2",
},
},
{
name: "path not under home",
input: []Repo{
{Path: "/opt/repos/project", Remote: "git@github.com:user/project.git"},
},
expected: []string{"/opt/repos/project"},
},
{
name: "mixed paths",
input: []Repo{
{Path: filepath.Join(home, "code/project"), Remote: "git@github.com:user/project.git"},
{Path: "/opt/repos/other", Remote: "git@github.com:user/other.git"},
},
expected: []string{
"$HOME/code/project",
"/opt/repos/other",
},
},
{
name: "empty repos",
input: []Repo{},
expected: []string{},
},
{
name: "home directory itself",
input: []Repo{
{Path: home, Remote: "git@github.com:user/home.git"},
},
expected: []string{"$HOME"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := MGConfig{Repos: tt.input}
conf.CollapsePaths()
if len(conf.Repos) != len(tt.expected) {
t.Fatalf("expected %d repos, got %d", len(tt.expected), len(conf.Repos))
}
for i, repo := range conf.Repos {
if repo.Path != tt.expected[i] {
t.Errorf("repo %d: expected path %q, got %q", i, tt.expected[i], repo.Path)
}
}
})
}
}
func TestExpandAndCollapse_Roundtrip(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Fatalf("failed to get home directory: %v", err)
}
// Start with $HOME-based paths (as stored in config)
original := MGConfig{
Repos: []Repo{
{Path: "$HOME/code/project1", Remote: "git@github.com:user/project1.git"},
{Path: "$HOME/go/src/github.com/user/repo", Remote: "git@github.com:user/repo.git"},
{Path: "/opt/external/repo", Remote: "git@github.com:user/external.git"},
},
}
// Expand paths (as done when loading)
conf := MGConfig{
Repos: make([]Repo, len(original.Repos)),
}
copy(conf.Repos, original.Repos)
conf.ExpandPaths()
// Verify expansion worked
expectedExpanded := []string{
filepath.Join(home, "code/project1"),
filepath.Join(home, "go/src/github.com/user/repo"),
"/opt/external/repo",
}
for i, repo := range conf.Repos {
if repo.Path != expectedExpanded[i] {
t.Errorf("after expand, repo %d: expected %q, got %q", i, expectedExpanded[i], repo.Path)
}
}
// Collapse paths (as done when saving)
conf.CollapsePaths()
// Verify we're back to the original
for i, repo := range conf.Repos {
if repo.Path != original.Repos[i].Path {
t.Errorf("after roundtrip, repo %d: expected %q, got %q", i, original.Repos[i].Path, repo.Path)
}
}
}
func TestSave_CollapsesPaths(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Fatalf("failed to get home directory: %v", err)
}
// Create a temp file for the config
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "mgconfig")
t.Setenv("MGCONFIG", configPath)
// Create config with expanded (absolute) paths
conf := MGConfig{
Repos: []Repo{
{Path: filepath.Join(home, "code/project"), Remote: "git@github.com:user/project.git"},
{Path: "/opt/external/repo", Remote: "git@github.com:user/external.git"},
},
Aliases: map[string]string{"test": "echo test"},
}
// Save should collapse paths
err = conf.Save()
if err != nil {
t.Fatalf("Save() failed: %v", err)
}
// Read back and verify paths are collapsed
data, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("failed to read saved config: %v", err)
}
var saved MGConfig
if err := json.Unmarshal(data, &saved); err != nil {
t.Fatalf("failed to parse saved config: %v", err)
}
expectedPaths := []string{
"$HOME/code/project",
"/opt/external/repo",
}
for i, repo := range saved.Repos {
if repo.Path != expectedPaths[i] {
t.Errorf("saved repo %d: expected path %q, got %q", i, expectedPaths[i], repo.Path)
}
}
// Verify original config wasn't modified
if conf.Repos[0].Path != filepath.Join(home, "code/project") {
t.Errorf("original config was modified: expected %q, got %q",
filepath.Join(home, "code/project"), conf.Repos[0].Path)
}
}
func TestParseMGConfig(t *testing.T) {
tests := []struct {
name string
input string
wantRepos int
wantErr bool
}{
{
name: "valid config",
input: `{
"Repos": [
{"Path": "$HOME/code/project", "Remote": "git@github.com:user/project.git"}
],
"Aliases": {"gc": "git gc"}
}`,
wantRepos: 1,
wantErr: false,
},
{
name: "empty config",
input: `{"Repos": [], "Aliases": {}}`,
wantRepos: 0,
wantErr: false,
},
{
name: "invalid json",
input: `{invalid}`,
wantRepos: 0,
wantErr: true,
},
{
name: "multiple repos",
input: `{
"Repos": [
{"Path": "$HOME/code/project1", "Remote": "git@github.com:user/project1.git"},
{"Path": "$HOME/code/project2", "Remote": "git@github.com:user/project2.git"}
]
}`,
wantRepos: 2,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf, err := ParseMGConfig([]byte(tt.input))
if (err != nil) != tt.wantErr {
t.Errorf("ParseMGConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && len(conf.Repos) != tt.wantRepos {
t.Errorf("ParseMGConfig() got %d repos, want %d", len(conf.Repos), tt.wantRepos)
}
})
}
}
func TestAddRepo(t *testing.T) {
tests := []struct {
name string
initial []Repo
addPath string
addRemote string
wantErr bool
wantCount int
}{
{
name: "add to empty",
initial: []Repo{},
addPath: "$HOME/code/new",
addRemote: "git@github.com:user/new.git",
wantErr: false,
wantCount: 1,
},
{
name: "add to existing",
initial: []Repo{
{Path: "$HOME/code/existing", Remote: "git@github.com:user/existing.git"},
},
addPath: "$HOME/code/new",
addRemote: "git@github.com:user/new.git",
wantErr: false,
wantCount: 2,
},
{
name: "add duplicate",
initial: []Repo{
{Path: "$HOME/code/existing", Remote: "git@github.com:user/existing.git"},
},
addPath: "$HOME/code/existing",
addRemote: "git@github.com:user/existing.git",
wantErr: true,
wantCount: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := MGConfig{Repos: tt.initial}
err := conf.AddRepo(tt.addPath, tt.addRemote)
if (err != nil) != tt.wantErr {
t.Errorf("AddRepo() error = %v, wantErr %v", err, tt.wantErr)
}
if len(conf.Repos) != tt.wantCount {
t.Errorf("AddRepo() repo count = %d, want %d", len(conf.Repos), tt.wantCount)
}
})
}
}
func TestDelRepo(t *testing.T) {
tests := []struct {
name string
initial []Repo
delPath string
wantErr bool
wantCount int
}{
{
name: "delete existing",
initial: []Repo{
{Path: "$HOME/code/project", Remote: "git@github.com:user/project.git"},
},
delPath: "$HOME/code/project",
wantErr: false,
wantCount: 0,
},
{
name: "delete from multiple",
initial: []Repo{
{Path: "$HOME/code/project1", Remote: "git@github.com:user/project1.git"},
{Path: "$HOME/code/project2", Remote: "git@github.com:user/project2.git"},
},
delPath: "$HOME/code/project1",
wantErr: false,
wantCount: 1,
},
{
name: "delete non-existent",
initial: []Repo{
{Path: "$HOME/code/project", Remote: "git@github.com:user/project.git"},
},
delPath: "$HOME/code/other",
wantErr: true,
wantCount: 1,
},
{
name: "delete from empty",
initial: []Repo{},
delPath: "$HOME/code/project",
wantErr: true,
wantCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := MGConfig{Repos: tt.initial}
err := conf.DelRepo(tt.delPath)
if (err != nil) != tt.wantErr {
t.Errorf("DelRepo() error = %v, wantErr %v", err, tt.wantErr)
}
if len(conf.Repos) != tt.wantCount {
t.Errorf("DelRepo() repo count = %d, want %d", len(conf.Repos), tt.wantCount)
}
})
}
}
func TestMerge(t *testing.T) {
tests := []struct {
name string
initial []Repo
merge []Repo
wantCount int
wantDuplicates int
wantNewPaths int
}{
{
name: "merge into empty",
initial: []Repo{},
merge: []Repo{
{Path: "$HOME/code/new", Remote: "git@github.com:user/new.git"},
},
wantCount: 1,
wantDuplicates: 0,
wantNewPaths: 1,
},
{
name: "merge with duplicates",
initial: []Repo{
{Path: "$HOME/code/existing", Remote: "git@github.com:user/existing.git"},
},
merge: []Repo{
{Path: "$HOME/code/existing", Remote: "git@github.com:user/existing.git"},
{Path: "$HOME/code/new", Remote: "git@github.com:user/new.git"},
},
wantCount: 2,
wantDuplicates: 1,
wantNewPaths: 1,
},
{
name: "merge all duplicates",
initial: []Repo{
{Path: "$HOME/code/project", Remote: "git@github.com:user/project.git"},
},
merge: []Repo{
{Path: "$HOME/code/project", Remote: "git@github.com:user/project.git"},
},
wantCount: 1,
wantDuplicates: 1,
wantNewPaths: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := MGConfig{Repos: tt.initial}
mergeConf := MGConfig{Repos: tt.merge}
stats, err := conf.Merge(mergeConf)
if err != nil {
t.Fatalf("Merge() unexpected error: %v", err)
}
if len(conf.Repos) != tt.wantCount {
t.Errorf("Merge() repo count = %d, want %d", len(conf.Repos), tt.wantCount)
}
if stats.Duplicates != tt.wantDuplicates {
t.Errorf("Merge() duplicates = %d, want %d", stats.Duplicates, tt.wantDuplicates)
}
if len(stats.NewPaths) != tt.wantNewPaths {
t.Errorf("Merge() new paths = %d, want %d", len(stats.NewPaths), tt.wantNewPaths)
}
})
}
}
func TestGetRepoPaths(t *testing.T) {
conf := MGConfig{
Repos: []Repo{
{Path: "$HOME/code/project1", Remote: "git@github.com:user/project1.git"},
{Path: "$HOME/code/project2", Remote: "git@github.com:user/project2.git"},
},
}
paths := conf.GetRepoPaths()
if len(paths) != 2 {
t.Fatalf("GetRepoPaths() returned %d paths, want 2", len(paths))
}
expected := []string{"$HOME/code/project1", "$HOME/code/project2"}
for i, path := range paths {
if path != expected[i] {
t.Errorf("GetRepoPaths()[%d] = %q, want %q", i, path, expected[i])
}
}
}

View File

@@ -3,7 +3,6 @@ package parse
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -33,9 +32,9 @@ func (m MRConfig) ToMGConfig() MGConfig {
mgconf := MGConfig(m)
for i, repo := range mgconf.Repos {
checkout := repo.Remote
if strings.HasPrefix(checkout, "git clone '") {
if after, ok := strings.CutPrefix(checkout, "git clone '"); ok {
// git clone 'git@bitbucket.org:taigrr/mg.git' 'mg'
remote := strings.TrimPrefix(checkout, "git clone '")
remote := after
sp := strings.Split(remote, "' '")
remote = sp[0]
mgconf.Repos[i].Remote = remote
@@ -45,8 +44,7 @@ func (m MRConfig) ToMGConfig() MGConfig {
}
// LoadMRConfig loads the mrconfig file from the user's home directory
// and returns a MRConfig struct
// TODO: load aliases into map instead of hardcoded Unregister prop
// and returns a MRConfig struct with all repos and aliases from the [DEFAULT] section
func LoadMRConfig() (MRConfig, error) {
home, err := os.UserHomeDir()
if err != nil {
@@ -60,7 +58,7 @@ func LoadMRConfig() (MRConfig, error) {
if s.IsDir() {
return MRConfig{}, errors.New("expected mrconfig file but got a directory")
}
f, err := ioutil.ReadFile(mrconfPath)
f, err := os.ReadFile(mrconfPath)
if err != nil {
return MRConfig{}, err
}
@@ -108,16 +106,8 @@ func LoadMRConfig() (MRConfig, error) {
config.Repos[length].Remote = split[1]
case "default":
// TODO load text into Aliases map instead of hardcoded Unregister prop
switch split[0] {
case "unregister":
config.Aliases["unregister"] = split[1]
case "git_gc":
config.Aliases["gc"] = split[1]
default:
return MRConfig{}, fmt.Errorf("unexpected argument on line %d: %s", n, line)
}
// Load all DEFAULT section aliases into the map
config.Aliases[split[0]] = split[1]
}
}
return config, nil