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 }