mirror of
https://github.com/taigrr/mg.git
synced 2026-04-02 03:28:42 -07:00
Compare commits
13 Commits
v0.0.5
...
cd/dep-upd
| Author | SHA1 | Date | |
|---|---|---|---|
| 348b87702d | |||
| 9d4ac7bc47 | |||
| a4044ba47a | |||
| e579599876 | |||
| 9ad9412a8c | |||
| 50f5bc897d | |||
| 0f925f852e | |||
| cacdbf673f | |||
|
|
991985ab4e | ||
| 417cf943fa | |||
| 1030a8f3a9 | |||
| f0c6f3906a | |||
| 01c736b54e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
cmd/mg/mg
|
cmd/mg/mg
|
||||||
|
.crush
|
||||||
|
|||||||
234
AGENTS.md
Normal file
234
AGENTS.md
Normal 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
1
cmd/mg/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
main
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
git "github.com/go-git/go-git/v5"
|
git "github.com/go-git/go-git/v5"
|
||||||
@@ -37,17 +38,22 @@ var (
|
|||||||
mutex := sync.Mutex{}
|
mutex := sync.Mutex{}
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(len(conf.Repos))
|
wg.Add(len(conf.Repos))
|
||||||
for i := 0; i < jobs; i++ {
|
cloneFunc := func() {
|
||||||
go func() {
|
|
||||||
for repo := range repoChan {
|
for repo := range repoChan {
|
||||||
_, err := git.PlainOpenWithOptions(repo.Remote, &(git.PlainOpenOptions{DetectDotGit: true}))
|
_, err := git.PlainOpenWithOptions(repo.Path, &(git.PlainOpenOptions{DetectDotGit: true}))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Printf("already cloned: %s\n", repo.Path)
|
log.Printf("already cloned: %s\n", repo.Path)
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
alreadyCloned++
|
alreadyCloned++
|
||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
|
wg.Done()
|
||||||
|
continue
|
||||||
} else if err == git.ErrRepositoryNotExists {
|
} else if err == git.ErrRepositoryNotExists {
|
||||||
log.Printf("attempting clone: %s\n", repo)
|
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{
|
_, err = git.PlainClone(repo.Path, false, &git.CloneOptions{
|
||||||
URL: repo.Remote,
|
URL: repo.Remote,
|
||||||
})
|
})
|
||||||
@@ -59,24 +65,28 @@ var (
|
|||||||
wg.Done()
|
wg.Done()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Printf("successfully cloned %s\n", repo)
|
fmt.Printf("successfully cloned %s\n", repo.Path)
|
||||||
wg.Done()
|
wg.Done()
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
errs = append(errs, RepoError{Error: err, Repo: repo.Path})
|
errs = append(errs, RepoError{Error: err, Repo: repo.Path})
|
||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
log.Printf("clone failed for %s: %v\n", repo, err)
|
log.Printf("clone failed for %s: %v\n", repo.Path, err)
|
||||||
wg.Done()
|
wg.Done()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
for i := 0; i < jobs; i++ {
|
||||||
|
go cloneFunc()
|
||||||
|
}
|
||||||
|
fmt.Println(len(conf.Repos))
|
||||||
for _, repo := range conf.Repos {
|
for _, repo := range conf.Repos {
|
||||||
repoChan <- repo
|
repoChan <- repo
|
||||||
}
|
}
|
||||||
close(repoChan)
|
close(repoChan)
|
||||||
|
fmt.Println("waiting...")
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
log.Printf("error pulling %s: %s\n", err.Repo, err.Error)
|
log.Printf("error pulling %s: %s\n", err.Repo, err.Error)
|
||||||
|
|||||||
@@ -27,5 +27,6 @@ func GetConfig() parse.MGConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
conf.ExpandPaths()
|
||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,90 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
git "github.com/go-git/go-git/v5"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fetchCmd = &cobra.Command{
|
var fetchCmd = &cobra.Command{
|
||||||
Use: "fetch",
|
Use: "fetch",
|
||||||
Short: "fetch all git repos without merging",
|
Short: "fetch all git repos without merging",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
fmt.Println("fetch called")
|
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() {
|
func init() {
|
||||||
rootCmd.AddCommand(fetchCmd)
|
rootCmd.AddCommand(fetchCmd)
|
||||||
|
fetchCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "number of jobs to run in parallel")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ var (
|
|||||||
jobs int
|
jobs int
|
||||||
pullCmd = &cobra.Command{
|
pullCmd = &cobra.Command{
|
||||||
Use: "pull",
|
Use: "pull",
|
||||||
Short: "add current path to list of repos",
|
Short: "update all git repos specified in config",
|
||||||
Run: func(_ *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
type RepoError struct {
|
type RepoError struct {
|
||||||
Error error
|
Error error
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ var registerCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
path = newPath.Filesystem.Root()
|
path = newPath.Filesystem.Root()
|
||||||
|
|
||||||
for _, v := range conf.Repos {
|
for _, v := range conf.Repos {
|
||||||
if v.Path == path {
|
if v.Path == path {
|
||||||
fmt.Printf("repo %s already registered\n", path)
|
fmt.Printf("repo %s already registered\n", path)
|
||||||
|
|||||||
@@ -2,14 +2,44 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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
|
// rootCmd represents the base command when called without any subcommands
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "mg",
|
Use: "mg",
|
||||||
Short: "go replacement for myrepos which only supports git repos",
|
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.
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
|||||||
@@ -2,19 +2,156 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
git "github.com/go-git/go-git/v5"
|
||||||
"github.com/spf13/cobra"
|
"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{
|
var statusCmd = &cobra.Command{
|
||||||
Use: "status",
|
Use: "status",
|
||||||
Short: "get the combined git status for all git repos",
|
Short: "get the combined git status for all git repos",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
fmt.Println("status called")
|
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() {
|
func init() {
|
||||||
rootCmd.AddCommand(statusCmd)
|
rootCmd.AddCommand(statusCmd)
|
||||||
|
statusCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "number of jobs to run in parallel")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
// unregisterCmd represents the unregister command
|
// unregisterCmd represents the unregister command
|
||||||
var unregisterCmd = &cobra.Command{
|
var unregisterCmd = &cobra.Command{
|
||||||
Use: "unregister",
|
Use: "unregister",
|
||||||
Short: "add current path to list of repos",
|
Short: "remove current path from list of repos",
|
||||||
Run: func(_ *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
conf := GetConfig()
|
conf := GetConfig()
|
||||||
path, err := os.Getwd()
|
path, err := os.Getwd()
|
||||||
|
|||||||
39
go.mod
39
go.mod
@@ -1,34 +1,33 @@
|
|||||||
module github.com/taigrr/mg
|
module github.com/taigrr/mg
|
||||||
|
|
||||||
go 1.21
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-git/go-git/v5 v5.11.0
|
github.com/go-git/go-git/v5 v5.17.0
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.10.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
|
github.com/ProtonMail/go-crypto v1.4.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.7 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.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/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/sergi/go-diff v1.3.1 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // 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
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.17.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/mod v0.14.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/sys v0.16.0 // indirect
|
|
||||||
golang.org/x/tools v0.16.1 // indirect
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
153
go.sum
153
go.sum
@@ -1,48 +1,48 @@
|
|||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
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/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
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 h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
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 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
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 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
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 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-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
|
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
|
||||||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
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 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -50,97 +50,58 @@ 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
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/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
|
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||||
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.5/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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
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 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
|
||||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
|
||||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
|
||||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-20190902080502-41f04d3bba15/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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errAlreadyRegistered = os.ErrExist
|
var errAlreadyRegistered = os.ErrExist
|
||||||
@@ -117,6 +118,28 @@ func ParseMGConfig(b []byte) (MGConfig, error) {
|
|||||||
return config, err
|
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 {
|
func (m MGConfig) Save() error {
|
||||||
mgConf := os.Getenv("MGCONFIG")
|
mgConf := os.Getenv("MGCONFIG")
|
||||||
if mgConf == "" {
|
if mgConf == "" {
|
||||||
@@ -133,7 +156,15 @@ func (m MGConfig) Save() error {
|
|||||||
}
|
}
|
||||||
mgConf = filepath.Join(confDir, "mgconfig")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
530
parse/mgconf_test.go
Normal file
530
parse/mgconf_test.go
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,9 +32,9 @@ func (m MRConfig) ToMGConfig() MGConfig {
|
|||||||
mgconf := MGConfig(m)
|
mgconf := MGConfig(m)
|
||||||
for i, repo := range mgconf.Repos {
|
for i, repo := range mgconf.Repos {
|
||||||
checkout := repo.Remote
|
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'
|
// git clone 'git@bitbucket.org:taigrr/mg.git' 'mg'
|
||||||
remote := strings.TrimPrefix(checkout, "git clone '")
|
remote := after
|
||||||
sp := strings.Split(remote, "' '")
|
sp := strings.Split(remote, "' '")
|
||||||
remote = sp[0]
|
remote = sp[0]
|
||||||
mgconf.Repos[i].Remote = remote
|
mgconf.Repos[i].Remote = remote
|
||||||
@@ -44,8 +44,7 @@ func (m MRConfig) ToMGConfig() MGConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadMRConfig loads the mrconfig file from the user's home directory
|
// LoadMRConfig loads the mrconfig file from the user's home directory
|
||||||
// and returns a MRConfig struct
|
// and returns a MRConfig struct with all repos and aliases from the [DEFAULT] section
|
||||||
// TODO: load aliases into map instead of hardcoded Unregister prop
|
|
||||||
func LoadMRConfig() (MRConfig, error) {
|
func LoadMRConfig() (MRConfig, error) {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -107,16 +106,8 @@ func LoadMRConfig() (MRConfig, error) {
|
|||||||
config.Repos[length].Remote = split[1]
|
config.Repos[length].Remote = split[1]
|
||||||
|
|
||||||
case "default":
|
case "default":
|
||||||
|
// Load all DEFAULT section aliases into the map
|
||||||
// TODO load text into Aliases map instead of hardcoded Unregister prop
|
config.Aliases[split[0]] = split[1]
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user