github.com/attic-labs/noms@v0.0.0-20210827224422-e5fa29d95e8b/cmd/noms/noms_log.go (about) 1 // Copyright 2016 Attic Labs, Inc. All rights reserved. 2 // Licensed under the Apache License, version 2.0: 3 // http://www.apache.org/licenses/LICENSE-2.0 4 5 package main 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "math" 13 "os" 14 "strings" 15 "time" 16 17 "github.com/attic-labs/kingpin" 18 "github.com/attic-labs/noms/cmd/util" 19 "github.com/attic-labs/noms/go/config" 20 "github.com/attic-labs/noms/go/d" 21 "github.com/attic-labs/noms/go/datas" 22 "github.com/attic-labs/noms/go/diff" 23 "github.com/attic-labs/noms/go/spec" 24 "github.com/attic-labs/noms/go/types" 25 "github.com/attic-labs/noms/go/util/datetime" 26 "github.com/attic-labs/noms/go/util/functions" 27 "github.com/attic-labs/noms/go/util/outputpager" 28 "github.com/attic-labs/noms/go/util/writers" 29 "github.com/mgutz/ansi" 30 ) 31 32 const parallelism = 16 33 34 type opts struct { 35 useColor bool 36 maxLines int 37 maxCommits int 38 oneline bool 39 showGraph bool 40 showValue bool 41 path string 42 tz *time.Location 43 } 44 45 func nomsLog(noms *kingpin.Application) (*kingpin.CmdClause, util.KingpinHandler) { 46 var o opts 47 var color int 48 var tzName string 49 50 cmd := noms.Command("log", "Lists the history of changes to a path -- see Spelling Values at https://github.com/attic-labs/noms/blob/master/doc/spelling.md.") 51 cmd.Flag("color", "set to 1 to force color on, 0 to force off").Default("-1").IntVar(&color) 52 53 cmd.Flag("max-lines", "max number of lines to show per commit (-1 for all lines)").Default("9").IntVar(&o.maxLines) 54 cmd.Flag("max-commits", "max number of commits to display (0 for all commits)").Short('n').Default("0").IntVar(&o.maxCommits) 55 cmd.Flag("oneline", "show a summary of each commit on a single line").BoolVar(&o.oneline) 56 cmd.Flag("graph", "show ascii-based commit hierarchy on left side of output").BoolVar(&o.showGraph) 57 cmd.Flag("show-value", "show commit value rather than diff information").BoolVar(&o.showValue) 58 cmd.Flag("tz", "display formatted date comments in specified timezone, must be: local or utc").Default("local").StringVar(&tzName) 59 60 cmd.Arg("value", "dataset or value to display history for").Required().StringVar(&o.path) 61 62 outputpager.RegisterOutputpagerFlags(cmd) 63 64 return cmd, func(input string) int { 65 o.useColor = shouldUseColor(color) 66 cfg := config.NewResolver() 67 68 o.tz, _ = locationFromTimezoneArg(tzName, nil) 69 datetime.RegisterHRSCommenter(o.tz) 70 71 resolved := cfg.ResolvePathSpec(o.path) 72 sp, err := spec.ForPath(resolved) 73 d.CheckErrorNoUsage(err) 74 defer sp.Close() 75 76 pinned, ok := sp.Pin() 77 if !ok { 78 fmt.Fprintf(os.Stderr, "Cannot resolve spec: %s\n", o.path) 79 return 1 80 } 81 defer pinned.Close() 82 database := pinned.GetDatabase() 83 84 absPath := pinned.Path 85 path := absPath.Path 86 if len(path) == 0 { 87 path = types.MustParsePath(".value") 88 } 89 90 origCommit, ok := database.ReadValue(absPath.Hash).(types.Struct) 91 if !ok || !datas.IsCommit(origCommit) { 92 d.CheckError(fmt.Errorf("%s does not reference a Commit object", path)) 93 } 94 95 iter := NewCommitIterator(database, origCommit) 96 displayed := 0 97 if o.maxCommits <= 0 { 98 o.maxCommits = math.MaxInt32 99 } 100 101 bytesChan := make(chan chan []byte, parallelism) 102 103 var done = false 104 105 go func() { 106 for ln, ok := iter.Next(); !done && ok && displayed < o.maxCommits; ln, ok = iter.Next() { 107 ch := make(chan []byte) 108 bytesChan <- ch 109 110 go func(ch chan []byte, node LogNode) { 111 buff := &bytes.Buffer{} 112 printCommit(node, path, buff, database, o) 113 ch <- buff.Bytes() 114 }(ch, ln) 115 116 displayed++ 117 } 118 close(bytesChan) 119 }() 120 121 pgr := outputpager.Start() 122 defer pgr.Stop() 123 124 for ch := range bytesChan { 125 commitBuff := <-ch 126 _, err := io.Copy(pgr.Writer, bytes.NewReader(commitBuff)) 127 if err != nil { 128 done = true 129 for range bytesChan { 130 // drain the output 131 } 132 } 133 } 134 135 return 0 136 } 137 } 138 139 // Prints the information for one commit in the log, including ascii graph on left side of commits if 140 // -graph arg is true. 141 func printCommit(node LogNode, path types.Path, w io.Writer, db datas.Database, o opts) (err error) { 142 maxMetaFieldNameLength := func(commit types.Struct) int { 143 maxLen := 0 144 if m, ok := commit.MaybeGet(datas.MetaField); ok { 145 meta := m.(types.Struct) 146 types.TypeOf(meta).Desc.(types.StructDesc).IterFields(func(name string, t *types.Type, optional bool) { 147 maxLen = max(maxLen, len(name)) 148 }) 149 } 150 return maxLen 151 } 152 153 hashStr := node.commit.Hash().String() 154 if o.useColor { 155 hashStr = ansi.Color("commit "+hashStr, "red+h") 156 } 157 158 maxFieldNameLen := maxMetaFieldNameLength(node.commit) 159 160 parentLabel := "Parent" 161 parentValue := "None" 162 parents := commitRefsFromSet(node.commit.Get(datas.ParentsField).(types.Set)) 163 if len(parents) > 1 { 164 pstrings := make([]string, len(parents)) 165 for i, p := range parents { 166 pstrings[i] = p.TargetHash().String() 167 } 168 parentLabel = "Merge" 169 parentValue = strings.Join(pstrings, " ") 170 } else if len(parents) == 1 { 171 parentValue = parents[0].TargetHash().String() 172 } 173 174 if o.oneline { 175 parentStr := fmt.Sprintf("%s %s", parentLabel+":", parentValue) 176 fmt.Fprintf(w, "%s (%s)\n", hashStr, parentStr) 177 return 178 } 179 180 maxFieldNameLen = max(maxFieldNameLen, len(parentLabel)) 181 parentStr := fmt.Sprintf("%-*s %s", maxFieldNameLen+1, parentLabel+":", parentValue) 182 fmt.Fprintf(w, "%s%s\n", genGraph(node, 0, o), hashStr) 183 fmt.Fprintf(w, "%s%s\n", genGraph(node, 1, o), parentStr) 184 lineno := 1 185 186 if o.maxLines != 0 { 187 lineno, err = writeMetaLines(node, o.maxLines, lineno, maxFieldNameLen, w, o) 188 if err != nil && err != writers.MaxLinesErr { 189 fmt.Fprintf(w, "error: %s\n", err) 190 return 191 } 192 193 if o.showValue { 194 _, err = writeCommitLines(node, path, o.maxLines, lineno, w, db, o) 195 } else { 196 _, err = writeDiffLines(node, path, db, o.maxLines, lineno, w, o) 197 } 198 } 199 return 200 } 201 202 // Generates ascii graph chars to display on the left side of the commit info if -graph arg is true. 203 func genGraph(node LogNode, lineno int, o opts) string { 204 if !o.showGraph { 205 return "" 206 } 207 208 // branchCount is the number of branches that we need to graph for this commit and determines the 209 // length of prefix string. The string will change from line to line to indicate whether the new 210 // branches are getting created or currently displayed branches need to be merged with other branches. 211 // Normally we want the maximum number of branches so we have enough room to display them all, however 212 // if node.Shrunk() is true, we only need to display the minimum number of branches. 213 branchCount := max(node.startingColCount, node.endingColCount) 214 if node.Shrunk() { 215 branchCount = min(node.startingColCount, node.endingColCount) 216 } 217 218 // Create the basic prefix string indicating the number of branches that are being tracked. 219 p := strings.Repeat("| ", max(branchCount, 1)) 220 buf := []rune(p) 221 222 // The first line of a commit has a '*' in the graph to indicate what branch it resides in. 223 if lineno == 0 { 224 if node.Expanding() { 225 buf[(branchCount-1)*2] = ' ' 226 } 227 buf[node.col*2] = '*' 228 return string(buf) 229 } 230 231 // If expanding, change all the '|' chars to '\' chars after the inserted branch 232 if node.Expanding() && lineno == 1 { 233 for i := node.newCols[0]; i < branchCount; i++ { 234 buf[(i*2)-1] = '\\' 235 buf[i*2] = ' ' 236 } 237 } 238 239 // if one branch is getting folded into another, show '/' where necessary to indicate that. 240 if node.Shrinking() { 241 foldingDistance := node.foldedCols[1] - node.foldedCols[0] 242 ch := ' ' 243 if lineno < foldingDistance+1 { 244 ch = '/' 245 } 246 for _, col := range node.foldedCols[1:] { 247 buf[(col*2)-1] = ch 248 buf[(col * 2)] = ' ' 249 } 250 } 251 252 return string(buf) 253 } 254 255 func writeMetaLines(node LogNode, maxLines, lineno, maxLabelLen int, w io.Writer, o opts) (int, error) { 256 if m, ok := node.commit.MaybeGet(datas.MetaField); ok { 257 genPrefix := func(w *writers.PrefixWriter) []byte { 258 return []byte(genGraph(node, int(w.NumLines), o)) 259 } 260 meta := m.(types.Struct) 261 mlw := &writers.MaxLineWriter{Dest: w, MaxLines: uint32(maxLines), NumLines: uint32(lineno)} 262 pw := &writers.PrefixWriter{Dest: mlw, PrefixFunc: genPrefix, NeedsPrefix: true, NumLines: uint32(lineno)} 263 err := d.Try(func() { 264 types.TypeOf(meta).Desc.(types.StructDesc).IterFields(func(fieldName string, t *types.Type, optional bool) { 265 v := meta.Get(fieldName) 266 fmt.Fprintf(pw, "%-*s", maxLabelLen+2, strings.Title(fieldName)+":") 267 // Encode dates as formatted string if this is a top-level meta 268 // field of type datetime.DateTimeType 269 if types.TypeOf(v).Equals(datetime.DateTimeType) { 270 var dt datetime.DateTime 271 dt.UnmarshalNoms(v) 272 fmt.Fprintln(pw, dt.In(o.tz).Format(time.RFC3339)) 273 } else { 274 types.WriteEncodedValue(pw, v) 275 } 276 fmt.Fprintln(pw) 277 }) 278 }) 279 return int(pw.NumLines), err 280 } 281 return lineno, nil 282 } 283 284 func writeCommitLines(node LogNode, path types.Path, maxLines, lineno int, w io.Writer, db datas.Database, o opts) (lineCnt int, err error) { 285 genPrefix := func(pw *writers.PrefixWriter) []byte { 286 return []byte(genGraph(node, int(pw.NumLines)+1, o)) 287 } 288 mlw := &writers.MaxLineWriter{Dest: w, MaxLines: uint32(maxLines), NumLines: uint32(lineno)} 289 pw := &writers.PrefixWriter{Dest: mlw, PrefixFunc: genPrefix, NeedsPrefix: true, NumLines: uint32(lineno)} 290 v := path.Resolve(node.commit, db) 291 if v == nil { 292 pw.Write([]byte("<nil>\n")) 293 } else { 294 err = types.WriteEncodedValue(pw, v) 295 mlw.MaxLines = 0 296 if err != nil { 297 d.PanicIfNotType(writers.MaxLinesErr, err) 298 pw.NeedsPrefix = true 299 pw.Write([]byte("...\n")) 300 err = nil 301 } else { 302 pw.NeedsPrefix = false 303 pw.Write([]byte("\n")) 304 } 305 if !node.lastCommit { 306 pw.NeedsPrefix = true 307 pw.Write([]byte("\n")) 308 } 309 } 310 return int(pw.NumLines), err 311 } 312 313 func writeDiffLines(node LogNode, path types.Path, db datas.Database, maxLines, lineno int, w io.Writer, o opts) (lineCnt int, err error) { 314 genPrefix := func(w *writers.PrefixWriter) []byte { 315 return []byte(genGraph(node, int(w.NumLines)+1, o)) 316 } 317 mlw := &writers.MaxLineWriter{Dest: w, MaxLines: uint32(maxLines), NumLines: uint32(lineno)} 318 pw := &writers.PrefixWriter{Dest: mlw, PrefixFunc: genPrefix, NeedsPrefix: true, NumLines: uint32(lineno)} 319 parents := node.commit.Get(datas.ParentsField).(types.Set) 320 var parent types.Value 321 if parents.Len() > 0 { 322 parent = parents.First() 323 } 324 if parent == nil { 325 _, err = fmt.Fprint(pw, "\n") 326 return 1, err 327 } 328 329 parentCommit := parent.(types.Ref).TargetValue(db).(types.Struct) 330 var old, neu types.Value 331 functions.All( 332 func() { old = path.Resolve(parentCommit, db) }, 333 func() { neu = path.Resolve(node.commit, db) }, 334 ) 335 336 // TODO: It would be better to treat this as an add or remove, but that requires generalization 337 // of some of the code in PrintDiff() because it cannot tolerate nil parameters. 338 if neu == nil { 339 fmt.Fprintf(pw, "new (#%s%s) not found\n", node.commit.Hash().String(), path.String()) 340 } 341 if old == nil { 342 fmt.Fprintf(pw, "old (#%s%s) not found\n", parentCommit.Hash().String(), path.String()) 343 } 344 345 if old != nil && neu != nil { 346 err = diff.PrintDiff(pw, old, neu, true) 347 mlw.MaxLines = 0 348 if err != nil { 349 d.PanicIfNotType(err, writers.MaxLinesErr) 350 pw.NeedsPrefix = true 351 pw.Write([]byte("...\n")) 352 err = nil 353 } 354 } 355 if !node.lastCommit { 356 pw.NeedsPrefix = true 357 pw.Write([]byte("\n")) 358 } 359 return int(pw.NumLines), err 360 } 361 362 func shouldUseColor(color int) bool { 363 if color != 1 && color != 0 { 364 return outputpager.IsStdoutTty() 365 } 366 return color == 1 367 } 368 369 func max(i, j int) int { 370 if i > j { 371 return i 372 } 373 return j 374 } 375 376 func min(i, j int) int { 377 if i < j { 378 return i 379 } 380 return j 381 } 382 383 func locationFromTimezoneArg(tz string, defaultTZ *time.Location) (*time.Location, error) { 384 switch tz { 385 case "local": 386 return time.Local, nil 387 case "utc": 388 return time.UTC, nil 389 case "": 390 return defaultTZ, nil 391 default: 392 return nil, errors.New("value must be: local or utc") 393 } 394 }