...

Source file src/edge-infra.dev/hack/build/ci/leaf/cmd/leaf.go

Documentation: edge-infra.dev/hack/build/ci/leaf/cmd

     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  			// Ensure only <= 1 output category is selected
    71  			if outputTargets && outputTargetsFile {
    72  				return fmt.Errorf("select only one output type")
    73  			}
    74  
    75  			// Set to outputTargets if none set
    76  			if !outputTargets && !outputTargetsFile {
    77  				outputTargets = true
    78  			}
    79  
    80  			// Ensure we are running from repository root both when binary is invoked
    81  			// directly and via `bazel run`
    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  			//nolint:nestif
   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