github.com/ndau/noms@v1.0.5/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/ndau/noms/cmd/util"
    19  	"github.com/ndau/noms/go/config"
    20  	"github.com/ndau/noms/go/d"
    21  	"github.com/ndau/noms/go/datas"
    22  	"github.com/ndau/noms/go/diff"
    23  	"github.com/ndau/noms/go/spec"
    24  	"github.com/ndau/noms/go/types"
    25  	"github.com/ndau/noms/go/util/datetime"
    26  	"github.com/ndau/noms/go/util/functions"
    27  	"github.com/ndau/noms/go/util/outputpager"
    28  	"github.com/ndau/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/ndau/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  }