├── __test__ └── workspace │ ├── go.work │ ├── hello │ ├── go.mod │ └── lib │ │ └── main.go │ └── world │ ├── go.mod │ ├── go.sum │ └── main.go ├── go.mod ├── go.sum ├── lib ├── analyser │ ├── __playground__ │ │ └── workspace │ │ │ ├── go.work │ │ │ ├── hello │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ └── lib │ │ │ │ └── hello.go │ │ │ └── world │ │ │ ├── cmd │ │ │ └── main.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ └── lib │ │ │ └── ok.go │ ├── parse.go │ ├── parse_test.go │ └── primitive.go ├── git │ └── main.go ├── resolver │ ├── graph.go │ └── graph_test.go ├── runner │ ├── __playground__ │ │ ├── a │ │ │ └── hello.txt │ │ └── b │ │ │ └── world.txt │ ├── runner.go │ ├── runner_test.go │ └── task.go └── utils │ └── log.go ├── main.go └── readme.md /__test__/workspace/go.work: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgere/monogo/2fb0b4985893e3397df86bdee0d4ea728db506ab/__test__/workspace/go.work -------------------------------------------------------------------------------- /__test__/workspace/hello/go.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgere/monogo/2fb0b4985893e3397df86bdee0d4ea728db506ab/__test__/workspace/hello/go.mod -------------------------------------------------------------------------------- /__test__/workspace/hello/lib/main.go: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /__test__/workspace/world/go.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgere/monogo/2fb0b4985893e3397df86bdee0d4ea728db506ab/__test__/workspace/world/go.mod -------------------------------------------------------------------------------- /__test__/workspace/world/go.sum: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /__test__/workspace/world/main.go: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nicolasgere/monogo 2 | 3 | go 1.22.4 4 | 5 | require ( 6 | github.com/dominikbraun/graph v0.23.0 7 | github.com/urfave/cli/v2 v2.27.2 8 | ) 9 | 10 | require ( 11 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 12 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 13 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= 4 | github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= 5 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 6 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 7 | github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= 8 | github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= 9 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= 10 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= 11 | -------------------------------------------------------------------------------- /lib/analyser/__playground__/workspace/go.work: -------------------------------------------------------------------------------- 1 | go 1.22.4 2 | 3 | use ./hello 4 | 5 | use ./world 6 | -------------------------------------------------------------------------------- /lib/analyser/__playground__/workspace/hello/go.mod: -------------------------------------------------------------------------------- 1 | module hello 2 | 3 | go 1.22.4 4 | 5 | require golang.org/x/example/hello v0.0.0-20240205180059-32022caedd6a // indirect 6 | -------------------------------------------------------------------------------- /lib/analyser/__playground__/workspace/hello/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/example/hello v0.0.0-20240205180059-32022caedd6a h1:lnHwduM5X5Fb0YnIPyjyGzRY88cc0Fo6xTgaWDX2c7Q= 2 | golang.org/x/example/hello v0.0.0-20240205180059-32022caedd6a/go.mod h1:UhUKOXx5fMcLZxwL20DUrWWBBoRYG9Jvc8FiwZhRHCI= 3 | -------------------------------------------------------------------------------- /lib/analyser/__playground__/workspace/hello/lib/hello.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "golang.org/x/example/hello/reverse" 5 | ) 6 | 7 | func Hello() string { 8 | return reverse.String("Hello") 9 | } 10 | -------------------------------------------------------------------------------- /lib/analyser/__playground__/workspace/world/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | t "world/lib" 7 | ) 8 | 9 | func main() { 10 | fmt.Println(t.Toto()) 11 | } 12 | -------------------------------------------------------------------------------- /lib/analyser/__playground__/workspace/world/go.mod: -------------------------------------------------------------------------------- 1 | module world 2 | 3 | go 1.22.4 4 | -------------------------------------------------------------------------------- /lib/analyser/__playground__/workspace/world/go.sum: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/analyser/__playground__/workspace/world/lib/ok.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | hello "hello/lib" 5 | ) 6 | 7 | func Toto() string { 8 | return hello.Hello() + "toto" 9 | } 10 | -------------------------------------------------------------------------------- /lib/analyser/parse.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/dominikbraun/graph" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | func ListModule(dir string) (modules []Module, err error) { 12 | output, err := runCommand(dir, "go list -m -json") 13 | if err != nil { 14 | return 15 | } 16 | 17 | d := json.NewDecoder(strings.NewReader(output)) 18 | for d.More() { 19 | var m Module 20 | if err = d.Decode(&m); err != nil { 21 | return 22 | } 23 | modules = append(modules, m) 24 | } 25 | return 26 | } 27 | 28 | func GetDependencyForModule(dir string) (details []ModuleDetail, err error) { 29 | output, err := runCommand(dir, "go list -json ./...") 30 | if err != nil { 31 | return 32 | } 33 | 34 | d := json.NewDecoder(strings.NewReader(output)) 35 | for d.More() { 36 | var detail ModuleDetail 37 | if err = d.Decode(&detail); err != nil { 38 | return 39 | } 40 | details = append(details, detail) 41 | } 42 | return 43 | } 44 | 45 | func GetImportFromModules(details []ModuleDetail) (imports map[string]bool) { 46 | imports = make(map[string]bool) 47 | for _, m := range details { 48 | for _, i := range m.Imports { 49 | imports[ strings.Split(i, "/")[0]] = true 50 | 51 | } 52 | } 53 | return imports 54 | } 55 | 56 | func BuildDependencyGraph(modules []Module) (gr *graph.Graph[string, string], err error) { 57 | g := graph.New(graph.StringHash, graph.Directed(), graph.Acyclic()) 58 | moduleMap := make(map[string]*Module) 59 | 60 | for i := range modules { 61 | moduleMap[modules[i].Path] = &modules[i] 62 | } 63 | 64 | for _, m := range modules { 65 | var details []ModuleDetail 66 | details, err = GetDependencyForModule(m.Dir) 67 | if err != nil { 68 | return nil, err 69 | } 70 | moduleMap[m.Path].Imports = GetImportFromModules(details) 71 | 72 | if err = g.AddVertex(m.Path); err != nil { 73 | // return nil, fmt.Errorf("failed to add vertex %s: %w", m.Path, err) 74 | } 75 | } 76 | 77 | for _, m := range moduleMap { 78 | for i := range m.Imports{ 79 | _, ok := moduleMap[i] 80 | if ok { 81 | fmt.Println("ADD EDGE", i, m.Path) 82 | if err = g.AddEdge(m.Path,i ); err != nil { 83 | // // return nil, fmt.Errorf("failed to add edge %s -> %s: %w", m.Path, importPath, err) 84 | // } 85 | } 86 | } 87 | 88 | } 89 | } 90 | gr = &g 91 | return 92 | } 93 | 94 | func GetDependencyPaths(g *graph.Graph[string, string], vertex string) ([]string, error) { 95 | var dependencyPaths []string 96 | 97 | // Define a visitor function for DFS 98 | visitor := func(v string) bool { 99 | if v != vertex { // Don't include the starting vertex itself 100 | dependencyPaths = append(dependencyPaths, v) 101 | } 102 | return false // Continue traversal 103 | } 104 | 105 | // Perform DFS 106 | err := graph.DFS(*g, vertex, visitor) 107 | if err != nil { 108 | return nil, fmt.Errorf("failed to perform DFS for vertex %s: %w", vertex, err) 109 | } 110 | 111 | return dependencyPaths, nil 112 | } 113 | 114 | func runCommand(dir, command string) (output string, err error) { 115 | cmd := exec.Command("sh", "-c", command) 116 | cmd.Dir = dir 117 | var outputBytes []byte 118 | outputBytes, err = cmd.CombinedOutput() 119 | if err != nil { 120 | return "", fmt.Errorf("command execution failed: %w\nOutput: %s", err, outputBytes) 121 | } 122 | return string(outputBytes), nil 123 | } 124 | -------------------------------------------------------------------------------- /lib/analyser/parse_test.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestList(t *testing.T) { 9 | modules, err := ListModule("./__playground__/workspace/") 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | if len(modules) != 2 { 14 | t.Error(":Module missing") 15 | } 16 | for _, m := range modules { 17 | var d []ModuleDetail 18 | d, err = GetDependencyForModule(m.Dir) 19 | if err != nil { 20 | fmt.Println(m.Dir, err) 21 | } 22 | fmt.Println(GetImportFromModules(d)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/analyser/primitive.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | type Module struct { 4 | Path string `json:"Path"` 5 | Main bool `json:"Main"` 6 | Dir string `json:"Dir"` 7 | GoMod string `json:"GoMod"` 8 | GoVersion string `json:"GoVersion"` 9 | Imports map[string]bool 10 | } 11 | 12 | type ModuleDetail struct { 13 | Dir string `json:"Dir"` 14 | ImportPath string `json:"ImportPath"` 15 | Name string `json:"Name"` 16 | Target string `json:"Target"` 17 | Root string `json:"Root"` 18 | Match []string `json:"Match"` 19 | GoFiles []string `json:"GoFiles"` 20 | Imports []string `json:"Imports"` 21 | Deps []string `json:"Deps"` 22 | } 23 | -------------------------------------------------------------------------------- /lib/git/main.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | func GetAffectedRootDirectories(compareBranch string, dir string) ([]string, error) { 11 | // Get the list of changed files 12 | cmd := exec.Command("git", "diff", "--name-only", compareBranch) 13 | cmd.Dir = dir 14 | output, err := cmd.Output() 15 | if err != nil { 16 | return nil, fmt.Errorf("error executing git command: %v", err) 17 | } 18 | 19 | // Split the output into individual file paths 20 | changedFiles := strings.Split(strings.TrimSpace(string(output)), "\n") 21 | 22 | // Create a map to store unique root directories 23 | affectedRootDirs := make(map[string]bool) 24 | 25 | // Extract root directories from file paths 26 | for _, file := range changedFiles { 27 | rootDir := extractRootDirectory(file) 28 | if rootDir != "" { 29 | affectedRootDirs[rootDir] = true 30 | } 31 | } 32 | 33 | // Convert map keys to slice 34 | result := make([]string, 0, len(affectedRootDirs)) 35 | for dir := range affectedRootDirs { 36 | result = append(result, dir) 37 | } 38 | 39 | return result, nil 40 | } 41 | 42 | func extractRootDirectory(filePath string) string { 43 | parts := strings.SplitN(filePath, string(filepath.Separator), 2) 44 | if len(parts) > 0 { 45 | return parts[0] 46 | } 47 | return "" 48 | } 49 | -------------------------------------------------------------------------------- /lib/resolver/graph.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | -------------------------------------------------------------------------------- /lib/resolver/graph_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/dominikbraun/graph" 8 | ) 9 | 10 | func TestG(t *testing.T) { 11 | g := graph.New(graph.StringHash, graph.Directed(), graph.Acyclic()) 12 | _ = g.AddVertex("A") 13 | _ = g.AddVertex("B") 14 | _ = g.AddVertex("C") 15 | _ = g.AddVertex("D") 16 | _ = g.AddEdge("A", "B") 17 | _ = g.AddEdge("B", "C") 18 | err := g.AddEdge("C", "A") 19 | fmt.Println(err) 20 | _ = graph.DFS(g, "A", func(value string) bool { 21 | fmt.Println(value) 22 | return false 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /lib/runner/__playground__/a/hello.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /lib/runner/__playground__/b/world.txt: -------------------------------------------------------------------------------- 1 | world 2 | -------------------------------------------------------------------------------- /lib/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os/exec" 9 | 10 | "github.com/nicolasgere/monogo/lib/utils" 11 | ) 12 | 13 | func NewRunner(ctx context.Context, concurency int) Runner { 14 | return Runner{ 15 | semaphore: make(chan struct{}, concurency), 16 | ctx: ctx, 17 | } 18 | } 19 | 20 | type Runner struct { 21 | semaphore chan struct{} 22 | ctx context.Context 23 | } 24 | 25 | func (r *Runner) ExecCommand(cmd *exec.Cmd, tf *TaskFuture, task *Task) { 26 | r.semaphore <- struct{}{} 27 | defer func() { <-r.semaphore }() 28 | utils.LogWithTaskId(task.Id, "Run task -> "+task.Cmd, utils.INFO) 29 | pipeout, err := cmd.StdoutPipe() 30 | if err != nil { 31 | tf.Done <- TaskResult{Err: err, Status: 1} 32 | } 33 | ReaderToChan(&pipeout, tf.Stdout) 34 | pipeerr, err := cmd.StderrPipe() 35 | if err != nil { 36 | tf.Done <- TaskResult{Err: err, Status: 1} 37 | } 38 | ReaderToChan(&pipeerr, tf.Stderr) 39 | if err := cmd.Start(); err != nil { 40 | tf.Done <- TaskResult{Err: err, Status: 1} 41 | } 42 | if err := cmd.Wait(); err != nil { 43 | fmt.Println("ERROR") 44 | if exiterr, ok := err.(*exec.ExitError); ok { 45 | tf.Done <- TaskResult{Err: err, Status: exiterr.ExitCode()} 46 | } else { 47 | tf.Done <- TaskResult{Err: err, Status: exiterr.ExitCode()} 48 | } 49 | } 50 | tf.Done <- TaskResult{Err: err, Status: 0} 51 | } 52 | 53 | func (r *Runner) RunTask(task Task) (tf *TaskFuture) { 54 | cmd := exec.CommandContext(r.ctx, "sh", "-c", task.Cmd) 55 | cmd.Dir = task.Root 56 | tf = &TaskFuture{ 57 | Id: task.Id, 58 | Stdout: make(chan []byte), 59 | Stderr: make(chan []byte), 60 | Done: make(chan TaskResult, 1), 61 | } 62 | 63 | go r.ExecCommand(cmd, tf, &task) 64 | return 65 | } 66 | 67 | func (r *Runner) RunTasks(tasks []Task) (tf []*TaskFuture) { 68 | tf = make([]*TaskFuture, 0) 69 | for _, task := range tasks { 70 | tf = append(tf, r.RunTask(task)) 71 | } 72 | return 73 | } 74 | 75 | func ReaderToChan(r *io.ReadCloser, out chan []byte) { 76 | go func() { 77 | rc := *r 78 | defer close(out) 79 | defer rc.Close() 80 | scanner := bufio.NewScanner(rc) 81 | for scanner.Scan() { 82 | t := scanner.Bytes() 83 | out <- t 84 | } 85 | if err := scanner.Err(); err != nil { 86 | // Handle error (if needed) 87 | fmt.Println("Error reading:", err) 88 | } 89 | }() 90 | } 91 | -------------------------------------------------------------------------------- /lib/runner/runner_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | // func TestRunner(t *testing.T) { 10 | // r := Runner{} 11 | 12 | // task := Task{ 13 | // Cmd: "cat hello.txt", 14 | // Root: "./__playground__/a/", 15 | // } 16 | // fmt.Println("START") 17 | // fmt.Println("running task") 18 | // tf := r.RunTask(task) 19 | 20 | // for { 21 | // select { 22 | // case stdout, ok := <-tf.Stdout: 23 | // if !ok { 24 | // tf.Stdout = nil 25 | // } 26 | // fmt.Println("stdout", string(stdout)) 27 | // break 28 | // case stderr, ok := <-tf.Stderr: 29 | // if !ok { 30 | // tf.Stderr = nil 31 | // } 32 | // fmt.Println("stderr", string(stderr)) 33 | // break 34 | // case result := <-tf.Done: 35 | // fmt.Println("done") 36 | // fmt.Println(result.Status) 37 | // return 38 | // } 39 | // } 40 | // } 41 | 42 | func TestRunnerMultiple(t *testing.T) { 43 | r := Runner{} 44 | tasks := []Task{ 45 | { 46 | Id: "a", 47 | Cmd: "cat hello.txt", 48 | Root: "./__playground__/a/", 49 | }, 50 | { 51 | Id: "b", 52 | Cmd: "cat world.txt", 53 | Root: "./__playground__/b/", 54 | }, 55 | } 56 | tfs := r.RunTasks(tasks) 57 | var wg sync.WaitGroup 58 | for _, tf := range tfs { 59 | wg.Add(1) 60 | go func(tf *TaskFuture) { 61 | for { 62 | select { 63 | case stdout, ok := <-tf.Stdout: 64 | if !ok { 65 | tf.Stdout = nil 66 | } 67 | if len(stdout) == 0 { 68 | break 69 | } 70 | fmt.Println("stdout", tf.Id, string(stdout)) 71 | break 72 | case stderr, ok := <-tf.Stderr: 73 | if !ok { 74 | tf.Stderr = nil 75 | } 76 | if len(stderr) == 0 { 77 | break 78 | } 79 | fmt.Println("stderr", tf.Id, string(stderr)) 80 | break 81 | case result := <-tf.Done: 82 | fmt.Println("done", tf.Id) 83 | fmt.Println(result.Status) 84 | wg.Done() 85 | break 86 | } 87 | } 88 | }(tf) 89 | } 90 | wg.Wait() 91 | } 92 | -------------------------------------------------------------------------------- /lib/runner/task.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | type Task struct { 4 | Id string 5 | Name string 6 | Root string 7 | Cmd string 8 | Args []string 9 | } 10 | 11 | type TaskFuture struct { 12 | Stdout chan []byte 13 | Stderr chan []byte 14 | Done chan TaskResult 15 | Id string 16 | } 17 | 18 | type TaskResult struct { 19 | Err error 20 | Status int 21 | } 22 | -------------------------------------------------------------------------------- /lib/utils/log.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type LogLevel int 9 | 10 | var STARTED = time.Now() 11 | 12 | const ( 13 | DEBUG LogLevel = 0 14 | INFO LogLevel = 1 15 | WARN LogLevel = 2 16 | ERROR LogLevel = 3 17 | ) 18 | 19 | const LOG_LEVEL = INFO 20 | 21 | func LogWithTaskId(id string, msg string, level LogLevel) { 22 | Since := time.Since(STARTED) 23 | if level >= LOG_LEVEL { 24 | fmt.Printf("%.1f [%s] %s\n", Since.Seconds(), id, msg) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "syscall" 11 | 12 | analyzer "github.com/nicolasgere/monogo/lib/analyser" 13 | 14 | "github.com/nicolasgere/monogo/lib/git" 15 | "github.com/nicolasgere/monogo/lib/runner" 16 | "github.com/nicolasgere/monogo/lib/utils" 17 | "github.com/urfave/cli/v2" 18 | ) 19 | 20 | var defaultDir = "." 21 | 22 | func main() { 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | defer cancel() 25 | 26 | setupSignalHandling(cancel) 27 | 28 | r := runner.NewRunner(ctx, 3) 29 | app := createCliApp(&r) 30 | 31 | if err := app.Run(os.Args); err != nil { 32 | log.Fatal(err) 33 | } 34 | } 35 | 36 | func setupSignalHandling(cancel context.CancelFunc) { 37 | sigChan := make(chan os.Signal, 1) 38 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 39 | go func() { 40 | <-sigChan 41 | cancel() 42 | }() 43 | } 44 | 45 | func createCliApp(r *runner.Runner) *cli.App { 46 | return &cli.App{ 47 | Commands: []*cli.Command{ 48 | createCommand("install", "Install dependency for every modules", "go mod download -x", r), 49 | createCommand("fmt", "Format every modules", "go fmt ./...", r), 50 | createCommand("test", "Test every modules", "go test ./...", r), 51 | }, 52 | } 53 | } 54 | 55 | func createCommand(name, usage, cmd string, r *runner.Runner) *cli.Command { 56 | var target string 57 | var dependency bool 58 | var branch string 59 | 60 | return &cli.Command{ 61 | Name: name, 62 | Usage: usage, 63 | Flags: []cli.Flag{ 64 | &cli.StringFlag{ 65 | Name: "Path", 66 | Usage: "Path to the root directory of the project", 67 | Aliases: []string{"p"}, 68 | Destination: &defaultDir, 69 | }, 70 | &cli.StringFlag{ 71 | Name: "target", 72 | Usage: "Targuetted module", 73 | Aliases: []string{"t"}, 74 | Destination: &target, 75 | }, 76 | &cli.BoolFlag{ 77 | Name: "dependency", 78 | Usage: "Run with all dependency of the targuet. Descendent and ascendent", 79 | Aliases: []string{"d"}, 80 | Destination: &dependency, 81 | Value: false, 82 | }, 83 | &cli.StringFlag{ 84 | Name: "branch", 85 | Usage: "Compare the current branch with the master branch, and found affected modules", 86 | Aliases: []string{"b"}, 87 | Destination: &branch, 88 | }, 89 | }, 90 | Action: func(*cli.Context) error { 91 | modules, err := analyzer.ListModule(defaultDir) 92 | if err != nil { 93 | return err 94 | } 95 | allModules := make(map[string]analyzer.Module) 96 | for _, module := range modules { 97 | allModules[module.Path] = module 98 | } 99 | modulesToRun := modules 100 | if branch != "" { 101 | gitModules, err := git.GetAffectedRootDirectories("master", defaultDir) 102 | if err != nil { 103 | return err 104 | } 105 | filteredModule := make([]analyzer.Module, 0) 106 | // Filter modules from git 107 | for _, m := range modules { 108 | for _, gm := range gitModules { 109 | if m.Path == gm { 110 | filteredModule = append(filteredModule, m) 111 | } 112 | } 113 | 114 | } 115 | modulesToRun = filteredModule 116 | } 117 | 118 | if target != "" { 119 | filteredModule := make([]analyzer.Module, 0) 120 | for _, m := range modules { 121 | if m.Path == target { 122 | filteredModule = append(filteredModule, m) 123 | } 124 | } 125 | modulesToRun = filteredModule 126 | // Add dependency 127 | if dependency { 128 | dependencyModules := map[string]string{} 129 | g, err := analyzer.BuildDependencyGraph(modules) 130 | if err != nil { 131 | return err 132 | } 133 | for _, m := range modulesToRun { 134 | paths, err := analyzer.GetDependencyPaths(g, m.Path) 135 | if err != nil { 136 | return err 137 | } 138 | for _, p := range paths { 139 | dependencyModules[p] = p 140 | } 141 | } 142 | existingMap := make(map[string]bool) 143 | for _, module := range modulesToRun { 144 | existingMap[module.Path] = true 145 | } 146 | 147 | // Add new unique modules 148 | for path := range dependencyModules { 149 | if !existingMap[path] { 150 | modulesToRun = append(modulesToRun, allModules[path]) 151 | existingMap[path] = true 152 | } 153 | } 154 | } 155 | } 156 | runOnModules(defaultDir, cmd, r, modulesToRun) 157 | return nil 158 | }, 159 | } 160 | } 161 | 162 | func runOnModules(dir, cmd string, r *runner.Runner, modules []analyzer.Module) { 163 | 164 | tasks := createTasks(modules, cmd) 165 | tfs := r.RunTasks(tasks) 166 | 167 | var wg sync.WaitGroup 168 | wg.Add(len(tfs)) 169 | 170 | for _, tf := range tfs { 171 | go handleTaskFuture(tf, &wg) 172 | } 173 | 174 | wg.Wait() 175 | return 176 | } 177 | 178 | func createTasks(modules []analyzer.Module, cmd string) []runner.Task { 179 | tasks := make([]runner.Task, len(modules)) 180 | for i, module := range modules { 181 | tasks[i] = runner.Task{ 182 | Id: module.Path, 183 | Cmd: cmd, 184 | Root: module.Dir, 185 | } 186 | } 187 | return tasks 188 | } 189 | 190 | func handleTaskFuture(tf *runner.TaskFuture, wg *sync.WaitGroup) { 191 | defer wg.Done() 192 | for { 193 | select { 194 | case stdout, ok := <-tf.Stdout: 195 | handleOutput(tf.Id, stdout, ok, &tf.Stdout) 196 | case stderr, ok := <-tf.Stderr: 197 | handleOutput(tf.Id, stderr, ok, &tf.Stderr) 198 | case result := <-tf.Done: 199 | utils.LogWithTaskId(tf.Id, fmt.Sprintf("Done with status %d", result.Status), utils.INFO) 200 | return 201 | } 202 | } 203 | } 204 | 205 | func handleOutput(id string, output []byte, ok bool, channel *chan []byte) { 206 | if !ok { 207 | *channel = nil 208 | return 209 | } 210 | if len(output) > 0 { 211 | utils.LogWithTaskId(id, string(output), utils.INFO) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Monogo CLI Tool 2 | 3 | Monogo is a 0 configuration tool companion for go workspace monorepo. It is like Turborepo but for Go 4 | 5 | ## Installation 6 | 7 | To install Monogo, clone the repository and build the binary: 8 | 9 | ```sh 10 | go install github.com/nicolasgere/monogo@latest 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Commands 16 | 17 | #### `install` 18 | 19 | Install dependencies for every module. 20 | 21 | ```sh 22 | monogo install [flags] 23 | ``` 24 | 25 | #### `fmt` 26 | 27 | Format every module. 28 | 29 | ```sh 30 | monogo fmt [flags] 31 | ``` 32 | 33 | #### `test` 34 | 35 | Run tests for every module. 36 | 37 | ```sh 38 | monogo test [flags] 39 | ``` 40 | 41 | ### Flags 42 | 43 | All commands support the following flags: 44 | 45 | - `--target, -t`: Specify a targeted module. 46 | - `--dependency, -d`: Run with all dependencies of the target (both descendants and ascendants). 47 | - `--branch, -b`: Compare the current branch with the master branch and find affected modules. 48 | - `--path, -p`: Directory to run the cli in, default . 49 | --------------------------------------------------------------------------------