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  }