blob: 5382924f7e26d5758cb33444bf7f6e0be9ccbfe2 [file] [log] [blame]
// Copyright 2019 The LUCI Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
bb ""
luciflag ""
pb ""
func cmdLS(p Params) *subcommands.Command {
return &subcommands.Command{
UsageLine: `ls [flags] [PATH [PATH...]]`,
ShortDesc: "lists builds",
LongDesc: doc(`
Lists builds.
Listed builds are sorted by creation time, descending.
A PATH argument can be one of
- "<project>"
- "<project>/<bucket>"
- "<project>/<bucket>/<builder>"
Multiple PATHs are connected with logical OR.
Printed builds are deduplicated.
CommandRun: func() subcommands.CommandRun {
r := &lsRun{}
r.clsFlag.Register(&r.Flags, doc(`
CL URLs that builds must be associated with.
This flag is mutually exclusive with flag: -predicate.
Example: list builds of CL 1539021.
bb ls -cl
r.tagsFlag.Register(&r.Flags, doc(`
Tags that builds must have. Can be specified multiple times.
All tags must be present.
This flag is mutually exclusive with flag: -predicate.
Example: list builds with tags "a:1" and "b:2".
bb ls -t a:1 -t b:2
r.Flags.BoolVar(&r.includeExperimental, "exp", false, doc(`
Print experimental builds too.
This flag is mutually exclusive with flag: -predicate.
r.experimentsFlag.Register(&r.Flags, doc(`
Experiments that the builds must (or must not) have.
This flag is mutually exclusive with flag: -predicate
Can be specified multiple times. All provided values must match.
Each value is in the form of `+"`[+-]experiment_name`"+`, where "+" indiciates "must have"
and "-" indicates "must not have".
As a special case, "+luci.non_production" implies "-exp=true".
Example: list builds with expirement luci.non_production:
bb ls -ex +luci.non_production
Well-known experiments:
* `+bb.ExperimentNonProduction+`
* `+bb.ExperimentBBCanarySoftware+`
* `+bb.ExperimentBBAgent+`
* `+bb.ExperimentRecipePY3+`
* `+bb.ExperimentUseRealms+`
r.Flags.Var(StatusFlag(&r.status), "status",
fmt.Sprintf("Build status. Valid values: %s.\n"+
"This flag is mutually exclusive with flag: -predicate.",
strings.Join(statusFlagValuesName, ", ")))
r.Flags.IntVar(&r.limit, "n", 0, doc(`
Limit the number of builds to print. If 0, then unlimited.
Can be passed as "-<number>", e.g. "ls -10".
r.Flags.BoolVar(&r.noPager, "nopage", false, doc(`
Disable paging.
r.Flags.Var(luciflag.MessageSliceFlag(&r.predicates), "predicate", doc(`
BuildPredicate that all builds in the response should match.
Predicate is expressed in JSON format of BuildPredicate proto message:
Multiple predicates are supported and connected with logical OR.
This flag is mutally exclusive with -cl, -t, -exp, -status flags and
any PATH arguments
Example: list builds that is either with tag "a:1" or of builder "a/b/c"
bb ls \
-predicate '{"tags":[{"key":"a","value":"1"}]}' \
-predicate '{"builder":{"project":"a","bucket":"b","builder":"c"}}'`))
return r
// flagsMEWithPredicate defines a set of flags that are mutually exclusive with predicate flag
var flagsMEWithPredicate = stringset.NewFromSlice("cl", "exp", "status", "t")
const defaultPageSize = 100
type lsRun struct {
predicates []*pb.BuildPredicate
status pb.Status
includeExperimental bool
limit int
noPager bool
func (r *lsRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
ctx := cli.GetContext(a, r, env)
if err := r.initClients(ctx); err != nil {
return r.done(ctx, err)
if r.limit < 0 {
return r.done(ctx, fmt.Errorf("-n value must be non-negative"))
reqs, err := r.parseSearchRequests(ctx, args)
if err != nil {
return r.done(ctx, err)
disableColors := r.noColor || shouldDisableColors()
listBuilds := func(ctx context.Context, out io.WriteCloser) int {
buildC := make(chan *pb.Build)
errC := make(chan error)
go func() {
err := protoutil.Search(ctx, buildC, r.client, reqs...)
errC <- err
p := newPrinter(out, disableColors, time.Now)
count := 0
for b := range buildC {
r.printBuild(p, b, count == 0)
if count == r.limit {
return 0
if err := <-errC; err != nil && err != context.Canceled {
return r.done(ctx, err)
return 0
if r.noPager {
return listBuilds(ctx, os.Stdout)
return pager.Main(ctx, listBuilds)
// parseSearchRequests converts flags and arguments to search requests.
func (r *lsRun) parseSearchRequests(ctx context.Context, args []string) ([]*pb.SearchBuildsRequest, error) {
predicates, err := r.parseBuildPredicates(ctx, args)
if err != nil {
return nil, err
var pageSize int
if pageSize = defaultPageSize; r.limit > 0 && r.limit < pageSize {
pageSize = r.limit
fields, err := r.FieldMask()
if err != nil {
return nil, err
for i, p := range fields.Paths {
fields.Paths[i] = "builds.*." + p
ret := make([]*pb.SearchBuildsRequest, len(predicates))
for i, predicate := range predicates {
ret[i] = &pb.SearchBuildsRequest{
Predicate: predicate,
PageSize: int32(pageSize),
// Creating defensive copy. Search method do mutate the field mask
// append next_page_token
Fields: proto.Clone(fields).(*field_mask.FieldMask),
return ret, nil
// parseBuildPredicates converts flags and args to a slice of pb.BuildPredicate.
func (r *lsRun) parseBuildPredicates(ctx context.Context, args []string) ([]*pb.BuildPredicate, error) {
if len(r.predicates) > 0 {
// Get all flags that have been set with value provided on the command line.
setFlags := stringset.New(r.Flags.NFlag())
r.Flags.Visit(func(flag *flag.Flag) {
switch invalidFlags := setFlags.Intersect(flagsMEWithPredicate); {
case invalidFlags.Len() > 0:
return nil, fmt.Errorf("-predicate flag is mutually exclusive with flags: %q", invalidFlags.ToSortedSlice())
case len(args) > 0:
return nil, fmt.Errorf("-predicate flag is mutually exclusive with positional arguments")
return r.predicates, nil
basePredicate := &pb.BuildPredicate{
Tags: r.Tags(),
Status: r.status,
IncludeExperimental: r.includeExperimental,
Experiments: r.experimentsFlag.experimentsFlat(),
if r.experimentsFlag.experiments[bb.ExperimentNonProduction] {
basePredicate.IncludeExperimental = true
var err error
if basePredicate.GerritChanges, err = r.clsFlag.retrieveCLs(ctx, r.httpClient, kRequirePatchset); err != nil {
return nil, err
if len(args) == 0 {
return []*pb.BuildPredicate{basePredicate}, nil
// PATH arguments
predicates := make([]*pb.BuildPredicate, len(args))
for i, path := range args {
predicates[i] = proto.Clone(basePredicate).(*pb.BuildPredicate)
if predicates[i].Builder, err = r.parsePath(path); err != nil {
return nil, fmt.Errorf("invalid path %q: %s", path, err)
return predicates, nil
func (r *lsRun) parsePath(path string) (*pb.BuilderID, error) {
bid := &pb.BuilderID{}
switch parts := strings.Split(path, "/"); len(parts) {
case 3:
bid.Builder = parts[2]
case 2:
bid.Bucket = parts[1]
case 1:
bid.Project = parts[0]
return nil, fmt.Errorf("got %d components, want 1-3", len(parts))
return bid, nil