mirror of
https://github.com/taigrr/mg.git
synced 2026-04-09 14:51:55 -07:00
Compare commits
1 Commits
master
...
cd/impleme
| Author | SHA1 | Date | |
|---|---|---|---|
| 18cb0e8f65 |
27
AGENTS.md
27
AGENTS.md
@@ -44,11 +44,11 @@ mg/
|
|||||||
│ │ ├── register.go # Register repo (implemented)
|
│ │ ├── register.go # Register repo (implemented)
|
||||||
│ │ ├── unregister.go# Unregister repo (implemented)
|
│ │ ├── unregister.go# Unregister repo (implemented)
|
||||||
│ │ ├── import.go # Merge configs (implemented)
|
│ │ ├── import.go # Merge configs (implemented)
|
||||||
│ │ ├── push.go # Stub
|
│ │ ├── push.go # Push all repos (implemented)
|
||||||
│ │ ├── fetch.go # Stub
|
│ │ ├── fetch.go # Fetch all repos (implemented)
|
||||||
│ │ ├── status.go # Stub
|
│ │ ├── status.go # Status all repos (implemented)
|
||||||
│ │ ├── diff.go # Stub
|
│ │ ├── diff.go # Diff all repos (implemented)
|
||||||
│ │ ├── commit.go # Stub
|
│ │ ├── commit.go # Commit staged changes (implemented)
|
||||||
│ │ └── config.go # Stub
|
│ │ └── config.go # Stub
|
||||||
│ └── paths/
|
│ └── paths/
|
||||||
│ └── mrpaths.go # Utility to list repo paths from mrconfig
|
│ └── mrpaths.go # Utility to list repo paths from mrconfig
|
||||||
@@ -67,11 +67,11 @@ mg/
|
|||||||
| `register` | Implemented | Detects git root, stores `$HOME` |
|
| `register` | Implemented | Detects git root, stores `$HOME` |
|
||||||
| `unregister` | Implemented | By path or current dir |
|
| `unregister` | Implemented | By path or current dir |
|
||||||
| `import` | Implemented | Merge configs, supports stdin `-` |
|
| `import` | Implemented | Merge configs, supports stdin `-` |
|
||||||
| `push` | Stub | Prints "push called" |
|
| `push` | Implemented | Parallel via `-j`, pushes all refs |
|
||||||
| `fetch` | Stub | Prints "fetch called" |
|
| `fetch` | Implemented | Parallel via `-j` |
|
||||||
| `status` | Stub | Prints "status called" |
|
| `status` | Implemented | Parallel via `-j`, sorted output |
|
||||||
| `diff` | Stub | Prints "diff called" |
|
| `diff` | Implemented | Parallel via `-j`, file-level diff |
|
||||||
| `commit` | Stub | Prints "commit called" |
|
| `commit` | Implemented | Parallel via `-j`, `-m` message |
|
||||||
| `config` | Stub | Prints "config called" |
|
| `config` | Stub | Prints "config called" |
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -221,10 +221,9 @@ go test ./...
|
|||||||
|
|
||||||
## Known Issues / TODOs
|
## Known Issues / TODOs
|
||||||
|
|
||||||
1. Several commands are stubs (push, fetch, status, diff, commit, config)
|
1. `config` command is still a stub
|
||||||
2. `parse/mgconf.go:61` has a hint about inefficient string concatenation in a loop
|
2. Test coverage is minimal for cmd package (parse package has good coverage)
|
||||||
3. Test coverage is minimal
|
3. No integration tests for commands that interact with git repos
|
||||||
4. `unregister` command short description incorrectly says "add current path" (copy-paste error)
|
|
||||||
|
|
||||||
## Error Handling Pattern
|
## Error Handling Pattern
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,122 @@ 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 commitMessage string
|
||||||
|
|
||||||
// commitCmd represents the commit command
|
// commitCmd represents the commit command
|
||||||
var commitCmd = &cobra.Command{
|
var commitCmd = &cobra.Command{
|
||||||
Use: "commit",
|
Use: "commit",
|
||||||
Short: "commit all current repos with the same message",
|
Short: "commit staged changes across all repos with the same message",
|
||||||
Run: func(_ *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
fmt.Println("commit called")
|
type RepoError struct {
|
||||||
|
Error error
|
||||||
|
Repo string
|
||||||
|
}
|
||||||
|
if jobs < 1 {
|
||||||
|
log.Println("jobs must be greater than 0")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if commitMessage == "" {
|
||||||
|
log.Println("commit message is required (-m)")
|
||||||
|
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
|
||||||
|
skipped int
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// Check if there are any staged changes
|
||||||
|
hasStagedChanges := false
|
||||||
|
for _, s := range st {
|
||||||
|
if s.Staging != git.Unmodified && s.Staging != git.Untracked {
|
||||||
|
hasStagedChanges = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasStagedChanges {
|
||||||
|
mutex.Lock()
|
||||||
|
skipped++
|
||||||
|
mutex.Unlock()
|
||||||
|
fmt.Printf("repo %s: nothing staged to commit\n", repo)
|
||||||
|
wg.Done()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err = w.Commit(commitMessage, &git.CommitOptions{})
|
||||||
|
if err != nil {
|
||||||
|
mutex.Lock()
|
||||||
|
errs = append(errs, RepoError{Error: err, Repo: repo})
|
||||||
|
mutex.Unlock()
|
||||||
|
log.Printf("commit failed for %s: %v\n", repo, err)
|
||||||
|
wg.Done()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("successfully committed in %s\n", repo)
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for _, repo := range conf.Repos {
|
||||||
|
repoChan <- repo.Path
|
||||||
|
}
|
||||||
|
close(repoChan)
|
||||||
|
wg.Wait()
|
||||||
|
for _, err := range errs {
|
||||||
|
log.Printf("error committing %s: %s\n", err.Repo, err.Error)
|
||||||
|
}
|
||||||
|
lenErrs := len(errs)
|
||||||
|
committed := len(conf.Repos) - lenErrs - skipped
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("successfully committed %d/%d repos\n", committed, len(conf.Repos))
|
||||||
|
fmt.Printf("%d repos had nothing staged\n", skipped)
|
||||||
|
fmt.Printf("failed to commit %d/%d repos\n", lenErrs, len(conf.Repos))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(commitCmd)
|
rootCmd.AddCommand(commitCmd)
|
||||||
|
commitCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "number of jobs to run in parallel")
|
||||||
|
commitCmd.Flags().StringVarP(&commitMessage, "message", "m", "", "commit message")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,140 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type repoDiff struct {
|
||||||
|
Path string
|
||||||
|
Changes []string
|
||||||
|
}
|
||||||
|
|
||||||
// diffCmd represents the diff command
|
// diffCmd represents the diff command
|
||||||
var diffCmd = &cobra.Command{
|
var diffCmd = &cobra.Command{
|
||||||
Use: "diff",
|
Use: "diff",
|
||||||
Short: "compute a collective diff",
|
Short: "show uncommitted changes across all repos",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
fmt.Println("diff 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
|
||||||
|
diffs []repoDiff
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if st.IsClean() {
|
||||||
|
wg.Done()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rd := repoDiff{Path: repo}
|
||||||
|
for file, status := range st {
|
||||||
|
code := status.Worktree
|
||||||
|
if code == git.Unmodified {
|
||||||
|
code = status.Staging
|
||||||
|
}
|
||||||
|
var prefix string
|
||||||
|
switch code {
|
||||||
|
case git.Modified:
|
||||||
|
prefix = "M"
|
||||||
|
case git.Added:
|
||||||
|
prefix = "A"
|
||||||
|
case git.Deleted:
|
||||||
|
prefix = "D"
|
||||||
|
case git.Renamed:
|
||||||
|
prefix = "R"
|
||||||
|
case git.Copied:
|
||||||
|
prefix = "C"
|
||||||
|
case git.Untracked:
|
||||||
|
prefix = "?"
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rd.Changes = append(rd.Changes, fmt.Sprintf(" %s %s", prefix, file))
|
||||||
|
}
|
||||||
|
sort.Strings(rd.Changes)
|
||||||
|
mutex.Lock()
|
||||||
|
diffs = append(diffs, rd)
|
||||||
|
mutex.Unlock()
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for _, repo := range conf.Repos {
|
||||||
|
repoChan <- repo.Path
|
||||||
|
}
|
||||||
|
close(repoChan)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
sort.Slice(diffs, func(i, j int) bool {
|
||||||
|
return diffs[i].Path < diffs[j].Path
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, rd := range diffs {
|
||||||
|
fmt.Printf("%s:\n", rd.Path)
|
||||||
|
for _, change := range rd.Changes {
|
||||||
|
fmt.Println(change)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, err := range errs {
|
||||||
|
log.Printf("error reading %s: %s\n", err.Repo, err.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%d/%d repos have changes\n", len(diffs), 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(diffCmd)
|
rootCmd.AddCommand(diffCmd)
|
||||||
|
diffCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "number of jobs to run in parallel")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
git "github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,11 +15,81 @@ import (
|
|||||||
var pushCmd = &cobra.Command{
|
var pushCmd = &cobra.Command{
|
||||||
Use: "push",
|
Use: "push",
|
||||||
Short: "push all git repos",
|
Short: "push all git repos",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
fmt.Println("push 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
|
||||||
|
alreadyUpToDate 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 push: %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("push failed for %s: %v\n", repo, err)
|
||||||
|
wg.Done()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = r.Push(&git.PushOptions{
|
||||||
|
RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*"},
|
||||||
|
})
|
||||||
|
if err == git.NoErrAlreadyUpToDate {
|
||||||
|
mutex.Lock()
|
||||||
|
alreadyUpToDate++
|
||||||
|
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("push failed for %s: %v\n", repo, err)
|
||||||
|
wg.Done()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("successfully pushed %s\n", repo)
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for _, repo := range conf.Repos {
|
||||||
|
repoChan <- repo.Path
|
||||||
|
}
|
||||||
|
close(repoChan)
|
||||||
|
wg.Wait()
|
||||||
|
for _, err := range errs {
|
||||||
|
log.Printf("error pushing %s: %s\n", err.Repo, err.Error)
|
||||||
|
}
|
||||||
|
lenErrs := len(errs)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("successfully pushed %d/%d repos\n", len(conf.Repos)-lenErrs, len(conf.Repos))
|
||||||
|
fmt.Printf("%d repos already up to date\n", alreadyUpToDate)
|
||||||
|
fmt.Printf("failed to push %d/%d repos\n", lenErrs, len(conf.Repos))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(pushCmd)
|
rootCmd.AddCommand(pushCmd)
|
||||||
|
pushCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "number of jobs to run in parallel")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user