package cmd import ( "context" "fmt" "log" "os" "regexp" "strings" "github.com/peterbourgon/ff/v3" "edge-infra.dev/pkg/lib/cli/rags" "edge-infra.dev/pkg/lib/cli/sink" "edge-infra.dev/hack/build/ci" "edge-infra.dev/pkg/lib/build/bazel" "edge-infra.dev/pkg/lib/cli/sh" "edge-infra.dev/pkg/tools/hack/bazelx" ) var ( bzlx = &bazelx.Bazelx{} commitRangeRe = regexp.MustCompile(`[\d\w/]+[.]{2}[\d\w/]+`) ) type inputType int const ( isFileList inputType = iota isCommitRange ) var ( outputTargets bool outputTargetsFile bool excludeManualTags bool ) func New() *sink.Command { cmd := &sink.Command{ Use: "leaf ", Short: "Get a list of targets based on a git diff range or list of files", 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"`, Options: []ff.Option{ff.WithEnvVarNoPrefix()}, Extensions: []sink.Extension{bzlx}, Flags: []*rags.Rag{ { Name: "targets", Short: "t", Usage: "Output the list of targets to stdout", Value: &rags.Bool{Var: &outputTargets}, Category: "output", }, { Name: "targets-file", Short: "f", Usage: "Output the path of the file containing the queried targets", Value: &rags.Bool{Var: &outputTargetsFile}, Category: "output", }, { Name: "exclude-manual-tags", Short: "m", Usage: "Exclude manual tagged labels", Value: &rags.Bool{Var: &excludeManualTags, Default: false}, }, }, Exec: func(_ context.Context, r sink.Run) error { // Ensure only <= 1 output category is selected if outputTargets && outputTargetsFile { return fmt.Errorf("select only one output type") } // Set to outputTargets if none set if !outputTargets && !outputTargetsFile { outputTargets = true } // Ensure we are running from repository root both when binary is invoked // directly and via `bazel run` if err := bazel.ChangeDirToRepoRoot(); err != nil { return err } var inputType inputType var commitRange string switch len(r.Args()) { case 0: return fmt.Errorf("pass either a space separated list of file names or a commit..range") case 1: if matchedStr := commitRangeRe.MatchString(r.Args()[0]); matchedStr { commitRange = r.Args()[0] inputType = isCommitRange break } inputType = isFileList default: inputType = isFileList } var err error session := sh.NewInDir(ci.RepoRoot) var labelSet string switch inputType { case isCommitRange: labelSet, err = ci.GitDiffToBazelLabels(r.Log, session, commitRange) case isFileList: labelSet, err = ci.FilesToBazelLabels(r.Log, r.Args()) default: return fmt.Errorf("unknown inputType") } if err != nil { return err } if len(labelSet) == 0 { r.Log.Info("No changes to targets that Bazel knows about have changed") return nil } targets, err := ci.QuerySet(r.Log, excludeManualTags, labelSet) if err != nil { return fmt.Errorf("error querying set: %w", err) } targetsOutStr := strings.Join(targets, "\n") //nolint:nestif if outputTargets { fmt.Fprintf(r.Out(), "%s\n", targetsOutStr) } else if outputTargetsFile { tempFile, err := os.CreateTemp("", "leaf-targets") if err != nil { return fmt.Errorf("error creating leaf targets file: %w", err) } targetsOutStr := fmt.Sprintf("%s\n", targetsOutStr) if _, err := tempFile.Write([]byte(targetsOutStr)); err == nil { fmt.Fprintf(r.Out(), "%s\n", tempFile.Name()) } else { return fmt.Errorf("error writing file %s", tempFile.Name()) } if err := tempFile.Close(); err != nil { return fmt.Errorf("error closing tempfile") } } r.Log.Info("Done.") return nil }, } return cmd } func Run(ctx context.Context) error { c := New() if err := c.ParseAndRun(ctx, os.Args[1:]); err != nil { log.Fatal(err) return err } return nil }