1 package cmd
2
3 import (
4 "context"
5 "fmt"
6 "log"
7 "os"
8 "regexp"
9 "strings"
10
11 "github.com/peterbourgon/ff/v3"
12
13 "edge-infra.dev/pkg/lib/cli/rags"
14 "edge-infra.dev/pkg/lib/cli/sink"
15
16 "edge-infra.dev/hack/build/ci"
17 "edge-infra.dev/pkg/lib/build/bazel"
18 "edge-infra.dev/pkg/lib/cli/sh"
19 "edge-infra.dev/pkg/tools/hack/bazelx"
20 )
21
22 var (
23 bzlx = &bazelx.Bazelx{}
24 commitRangeRe = regexp.MustCompile(`[\d\w/]+[.]{2}[\d\w/]+`)
25 )
26
27 type inputType int
28
29 const (
30 isFileList inputType = iota
31 isCommitRange
32 )
33
34 var (
35 outputTargets bool
36 outputTargetsFile bool
37 excludeManualTags bool
38 )
39
40 func New() *sink.Command {
41 cmd := &sink.Command{
42 Use: "leaf <commit..range OR space separated list of files>",
43 Short: "Get a list of targets based on a git diff range or list of files",
44 Long: `Leaf returns a list of bazel targets based on a git diff i.e. "leaf origin/master..HEAD" or a space separated list of files i.e. "leaf main.go pkg/leaf.go build/ci.go"`,
45 Options: []ff.Option{ff.WithEnvVarNoPrefix()},
46 Extensions: []sink.Extension{bzlx},
47 Flags: []*rags.Rag{
48 {
49 Name: "targets",
50 Short: "t",
51 Usage: "Output the list of targets to stdout",
52 Value: &rags.Bool{Var: &outputTargets},
53 Category: "output",
54 },
55 {
56 Name: "targets-file",
57 Short: "f",
58 Usage: "Output the path of the file containing the queried targets",
59 Value: &rags.Bool{Var: &outputTargetsFile},
60 Category: "output",
61 },
62 {
63 Name: "exclude-manual-tags",
64 Short: "m",
65 Usage: "Exclude manual tagged labels",
66 Value: &rags.Bool{Var: &excludeManualTags, Default: false},
67 },
68 },
69 Exec: func(_ context.Context, r sink.Run) error {
70
71 if outputTargets && outputTargetsFile {
72 return fmt.Errorf("select only one output type")
73 }
74
75
76 if !outputTargets && !outputTargetsFile {
77 outputTargets = true
78 }
79
80
81
82 if err := bazel.ChangeDirToRepoRoot(); err != nil {
83 return err
84 }
85
86 var inputType inputType
87 var commitRange string
88 switch len(r.Args()) {
89 case 0:
90 return fmt.Errorf("pass either a space separated list of file names or a commit..range")
91 case 1:
92 if matchedStr := commitRangeRe.MatchString(r.Args()[0]); matchedStr {
93 commitRange = r.Args()[0]
94 inputType = isCommitRange
95 break
96 }
97 inputType = isFileList
98 default:
99 inputType = isFileList
100 }
101
102 var err error
103 session := sh.NewInDir(ci.RepoRoot)
104
105 var labelSet string
106 switch inputType {
107 case isCommitRange:
108 labelSet, err = ci.GitDiffToBazelLabels(r.Log, session, commitRange)
109 case isFileList:
110 labelSet, err = ci.FilesToBazelLabels(r.Log, r.Args())
111 default:
112 return fmt.Errorf("unknown inputType")
113 }
114
115 if err != nil {
116 return err
117 }
118 if len(labelSet) == 0 {
119 r.Log.Info("No changes to targets that Bazel knows about have changed")
120 return nil
121 }
122
123 targets, err := ci.QuerySet(r.Log, excludeManualTags, labelSet)
124 if err != nil {
125 return fmt.Errorf("error querying set: %w", err)
126 }
127
128 targetsOutStr := strings.Join(targets, "\n")
129
130
131 if outputTargets {
132 fmt.Fprintf(r.Out(), "%s\n", targetsOutStr)
133 } else if outputTargetsFile {
134 tempFile, err := os.CreateTemp("", "leaf-targets")
135 if err != nil {
136 return fmt.Errorf("error creating leaf targets file: %w", err)
137 }
138 targetsOutStr := fmt.Sprintf("%s\n", targetsOutStr)
139 if _, err := tempFile.Write([]byte(targetsOutStr)); err == nil {
140 fmt.Fprintf(r.Out(), "%s\n", tempFile.Name())
141 } else {
142 return fmt.Errorf("error writing file %s", tempFile.Name())
143 }
144 if err := tempFile.Close(); err != nil {
145 return fmt.Errorf("error closing tempfile")
146 }
147 }
148
149 r.Log.Info("Done.")
150
151 return nil
152 },
153 }
154
155 return cmd
156 }
157
158 func Run(ctx context.Context) error {
159 c := New()
160 if err := c.ParseAndRun(ctx, os.Args[1:]); err != nil {
161 log.Fatal(err)
162 return err
163 }
164
165 return nil
166 }
167
View as plain text