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