go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/client/cmd/gitiles/log.go (about)

     1  // Copyright 2017 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"os"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/maruel/subcommands"
    26  
    27  	"github.com/golang/protobuf/jsonpb"
    28  
    29  	"go.chromium.org/luci/auth"
    30  	"go.chromium.org/luci/common/api/gitiles"
    31  	"go.chromium.org/luci/common/errors"
    32  	gitilespb "go.chromium.org/luci/common/proto/gitiles"
    33  	"go.chromium.org/luci/common/retry"
    34  	"go.chromium.org/luci/common/retry/transient"
    35  	"go.chromium.org/luci/grpc/grpcutil"
    36  )
    37  
    38  func cmdLog(authOpts auth.Options) *subcommands.Command {
    39  	return &subcommands.Command{
    40  		UsageLine: "log <options> repository-url committish",
    41  		ShortDesc: "prints commits based on a repo and committish",
    42  		LongDesc: `Prints commits based on a repo and committish.
    43  
    44  This should be equivalent of a "git log <committish>" call in that repository.`,
    45  		CommandRun: func() subcommands.CommandRun {
    46  			c := logRun{}
    47  			c.commonFlags.Init(authOpts)
    48  			c.Flags.IntVar(&c.limit, "limit", 0, "Limit the number of log entries returned.")
    49  			c.Flags.BoolVar(&c.withTreeDiff, "with-tree-diff", false, "Return 'TreeDiff' information in the returned commits.")
    50  			c.Flags.StringVar(
    51  				&c.jsonOutput,
    52  				"json-output",
    53  				"",
    54  				"Path to write operation results to. "+
    55  					"Output is JSON array of JSONPB commits.")
    56  			return &c
    57  		},
    58  	}
    59  }
    60  
    61  type logRun struct {
    62  	commonFlags
    63  	limit        int
    64  	withTreeDiff bool
    65  	jsonOutput   string
    66  }
    67  
    68  func (c *logRun) Parse(a subcommands.Application, args []string) error {
    69  	switch err := c.commonFlags.Parse(); {
    70  	case err != nil:
    71  		return err
    72  	case len(args) != 2:
    73  		return errors.New("exactly 2 position arguments are expected")
    74  	default:
    75  		return nil
    76  	}
    77  }
    78  
    79  func (c *logRun) main(a subcommands.Application, args []string) error {
    80  	ctx := c.defaultFlags.MakeLoggingContext(os.Stderr)
    81  
    82  	host, project, err := gitiles.ParseRepoURL(args[0])
    83  	if err != nil {
    84  		return errors.Annotate(err, "invalid repo URL %q", args[0]).Err()
    85  	}
    86  
    87  	req := &gitilespb.LogRequest{
    88  		Project:  project,
    89  		PageSize: int32(c.limit),
    90  		TreeDiff: c.withTreeDiff,
    91  	}
    92  
    93  	switch commits := strings.SplitN(args[1], "..", 2); len(commits) {
    94  	case 0:
    95  		return errors.New("committish is required")
    96  	case 1:
    97  		req.Committish = commits[0]
    98  	case 2:
    99  		req.ExcludeAncestorsOf = commits[0]
   100  		req.Committish = commits[1]
   101  	default:
   102  		panic("impossible")
   103  	}
   104  
   105  	authCl, err := c.createAuthClient()
   106  	if err != nil {
   107  		return err
   108  	}
   109  	g, err := gitiles.NewRESTClient(authCl, host, true)
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	var res *gitilespb.LogResponse
   115  	if err := retry.Retry(ctx, transient.Only(retry.Default), func() error {
   116  		var err error
   117  		res, err = g.Log(ctx, req)
   118  		return grpcutil.WrapIfTransient(err)
   119  	}, nil); err != nil {
   120  		return err
   121  	}
   122  
   123  	if c.jsonOutput == "" {
   124  		for _, c := range res.Log {
   125  			fmt.Printf("commit %s\n", c.Id)
   126  			fmt.Printf("Author: %s <%s>\n", c.Author.Name, c.Author.Email)
   127  			t := c.Author.Time.AsTime()
   128  			fmt.Printf("Date:   %s\n\n", t.Format(time.UnixDate))
   129  			for _, l := range strings.Split(c.Message, "\n") {
   130  				fmt.Printf("    %s\n", l)
   131  			}
   132  		}
   133  		return nil
   134  	}
   135  
   136  	out := os.Stdout
   137  	if c.jsonOutput != "-" {
   138  		out, err = os.Create(c.jsonOutput)
   139  		if err != nil {
   140  			return err
   141  		}
   142  		defer out.Close()
   143  	}
   144  
   145  	// Note: for historical reasons, output format is JSON array, so
   146  	// marshal individual commits to JSONPB and then marshall all that
   147  	// as a JSON array.
   148  	arr := make([]json.RawMessage, len(res.Log))
   149  	m := &jsonpb.Marshaler{}
   150  	for i, c := range res.Log {
   151  		buf := &bytes.Buffer{} // cannot reuse
   152  		if err := m.Marshal(buf, c); err != nil {
   153  			return errors.Annotate(err, "could not marshal commit %q to JSONPB", c.Id).Err()
   154  		}
   155  		arr[i] = json.RawMessage(buf.Bytes())
   156  	}
   157  
   158  	data, err := json.MarshalIndent(arr, "", "  ")
   159  	if err != nil {
   160  		return err
   161  	}
   162  	_, err = out.Write(data)
   163  	return err
   164  }
   165  
   166  func (c *logRun) Run(a subcommands.Application, args []string, _ subcommands.Env) int {
   167  	if err := c.Parse(a, args); err != nil {
   168  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   169  		return 1
   170  	}
   171  	if err := c.main(a, args); err != nil {
   172  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   173  		return 1
   174  	}
   175  	return 0
   176  }