Skip to content

Commit

Permalink
feat(logbook): add lib methods & plumbing commands for logbook
Browse files Browse the repository at this point in the history
  • Loading branch information
b5 committed Oct 12, 2019
1 parent 8a3b006 commit 6a9ae8f
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 31 deletions.
118 changes: 116 additions & 2 deletions cmd/log.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"bytes"
"encoding/json"
"fmt"

util "github.com/qri-io/apiutil"
Expand All @@ -16,9 +18,9 @@ func NewLogCommand(f Factory, ioStreams ioes.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "log",
Aliases: []string{"history"},
Short: "Show log of dataset history",
Short: "Show log of dataset commits",
Long: `
` + "`qri log`" + ` prints a list of changes to a dataset over time. Each entry in the log is a
` + "`qri log`" + ` lists dataset commits over time. Each entry in the log is a
snapshot of a dataset taken at the moment it was saved that keeps exact details
about how that dataset looked at at that point in time.
Expand Down Expand Up @@ -97,3 +99,115 @@ func (o *LogOptions) Run() error {
printItems(o.Out, items, page.Offset())
return nil
}

// NewLogbookCommand creates a `qri logbook` cobra command
func NewLogbookCommand(f Factory, ioStreams ioes.IOStreams) *cobra.Command {
o := &LogbookOptions{IOStreams: ioStreams}
cmd := &cobra.Command{
Use: "logbook",
Short: "Show a detailed list of changes on a dataset name",
Example: ` show log for the dataset bob/precip:
$ qri logbook bob/precip`,
Long: `Logbooks are records of changes to a dataset. The logbook is more detailed
than a dataset history, recording the steps taken construct a dataset history
without including dataset data. Logbooks can be synced with other users.
The logbook command shows entries for a dataset, from newest to oldest.`,
Annotations: map[string]string{
"group": "dataset",
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := o.Complete(f, args); err != nil {
return err
}
if o.Raw {
return o.RawLogs()
}
return o.Logbook()
},
}

// cmd.Flags().StringVarP(&o.Format, "format", "f", "", "set output format [json]")
cmd.Flags().IntVar(&o.PageSize, "page-size", 25, "page size of results, default 25")
cmd.Flags().IntVar(&o.Page, "page", 1, "page number of results, default 1")
cmd.Flags().BoolVar(&o.Raw, "raw", false, "full logbook in raw JSON format. overrides all other flags")

return cmd
}

// LogbookOptions encapsulates state for the log command
type LogbookOptions struct {
ioes.IOStreams

PageSize int
Page int
Refs *RefSelect
Raw bool

LogRequests *lib.LogRequests
}

// Complete adds any missing configuration that can only be added just before calling Run
func (o *LogbookOptions) Complete(f Factory, args []string) (err error) {
if o.Raw {
if len(args) != 0 {
return fmt.Errorf("can't use dataset reference. the raw flag shows the entire logbook")
}
} else {
if o.Refs, err = GetCurrentRefSelect(f, args, 1); err != nil {
return err
}
}

o.LogRequests, err = f.LogRequests()
return
}

// Logbook executes the Logbook command
func (o *LogbookOptions) Logbook() error {
printRefSelect(o.Out, o.Refs)

// convert Page and PageSize to Limit and Offset
page := util.NewPage(o.Page, o.PageSize)

p := &lib.RefListParams{
Ref: o.Refs.Ref(),
Limit: page.Limit(),
Offset: page.Offset(),
}

res := []lib.LogEntry{}
if err := o.LogRequests.Logbook(p, &res); err != nil {
if err == repo.ErrEmptyRef {
return lib.NewError(err, "please provide a dataset reference")
}
return err
}

// print items in reverse
items := make([]fmt.Stringer, len(res))
j := len(items)
for _, r := range res {
j--
items[j] = logEntryStringer(r)
}

printItems(o.Out, items, page.Offset())
return nil
}

// RawLogs executes the rawlogs variant of the logbook command
func (o *LogbookOptions) RawLogs() error {
var res interface{}
if err := o.LogRequests.RawLogs(&lib.RawLogsParams{}, &res); err != nil {
return err
}

data, err := json.Marshal(res)
if err != nil {
return err
}

printToPager(o.Out, bytes.NewBuffer(data))
return nil
}
40 changes: 40 additions & 0 deletions cmd/log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cmd

import (
"context"
"testing"
)

func TestLogbookCommand(t *testing.T) {
r := NewTestRepoRoot(t, "qri_test_logbook")
defer r.Delete()

ctx, done := context.WithCancel(context.Background())
defer done()

cmdR := r.CreateCommandRunner(ctx)
err := executeCommand(cmdR, "qri save --body=testdata/movies/body_ten.csv me/test_movies")
if err != nil {
t.Fatal(err.Error())
}

cmdR = r.CreateCommandRunner(ctx)
if err = executeCommand(cmdR, "qri save --body=testdata/movies/body_thirty.csv me/test_movies"); err != nil {
t.Fatal(err)
}

cmdR = r.CreateCommandRunner(ctx)
if err = executeCommand(cmdR, "qri logbook me/test_movies --raw"); err == nil {
t.Error("expected using a ref and the raw flag to error")
}

cmdR = r.CreateCommandRunner(ctx)
if err = executeCommand(cmdR, "qri logbook --raw"); err != nil {
t.Fatal(err)
}

cmdR = r.CreateCommandRunner(ctx)
if err = executeCommand(cmdR, "qri logbook me/test_movies"); err != nil {
t.Fatal(err)
}
}
1 change: 1 addition & 0 deletions cmd/qri.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ https://github.com/qri-io/qri/issues`,
NewInitCommand(opt, ioStreams),
NewListCommand(opt, ioStreams),
NewLogCommand(opt, ioStreams),
NewLogbookCommand(opt, ioStreams),
NewPublishCommand(opt, ioStreams),
NewPeersCommand(opt, ioStreams),
NewRegistryCommand(opt, ioStreams),
Expand Down
14 changes: 14 additions & 0 deletions cmd/stringers.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,17 @@ func oneLiner(str string, maxLen int) string {
}
return str
}

type logEntryStringer lib.LogEntry

func (s logEntryStringer) String() string {
title := color.New(color.FgGreen, color.Bold).SprintFunc()
ts := color.New(color.Faint).SprintFunc()

return fmt.Sprintf("%s\t%s\t%s\t%s\n",
ts(s.Timestamp.Format(time.RFC3339)),
title(s.Author),
title(s.Action),
s.Note,
)
}
2 changes: 2 additions & 0 deletions fsi/init.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fsi

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -124,6 +125,7 @@ func (fsi *FSI) InitDataset(p InitParams) (name string, err error) {
return "", err
}

err = fsi.repo.Logbook().WriteNameInit(context.TODO(), name)
return name, err
}

Expand Down
7 changes: 5 additions & 2 deletions lib/datasets.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import (
"github.com/qri-io/qfs"
"github.com/qri-io/qri/actions"
"github.com/qri-io/qri/base"
"github.com/qri-io/qri/dsref"
"github.com/qri-io/qri/fsi"
"github.com/qri-io/qri/p2p"
"github.com/qri-io/qri/repo"
"github.com/qri-io/qri/dsref"
)

// DatasetRequests encapsulates business logic for working with Datasets on Qri
Expand Down Expand Up @@ -611,7 +611,10 @@ func (r *DatasetRequests) Remove(p *RemoveParams, res *RemoveResponse) error {
}
res.NumDeleted = p.Revision.Gen

return nil
// TODO (b5) - this should be moved down into the action
err = r.inst.Repo().Logbook().WriteVersionDelete(ctx, repo.ConvertToDsref(ref), res.NumDeleted)

return err
}

// AddParams encapsulates parameters to the add command
Expand Down
49 changes: 49 additions & 0 deletions lib/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/rpc"

"github.com/qri-io/qri/actions"
"github.com/qri-io/qri/logbook"
"github.com/qri-io/qri/p2p"
"github.com/qri-io/qri/repo"
)
Expand Down Expand Up @@ -70,3 +71,51 @@ func (r *LogRequests) Log(params *LogParams, res *[]repo.DatasetRef) (err error)
*res, err = actions.DatasetLog(ctx, r.node, ref, params.Limit, params.Offset)
return
}

// RefListParams encapsulatess parameters for requests to a single reference
// that will produce a paginated result
type RefListParams struct {
// String value of a reference
Ref string
// Pagination Parameters
Offset, Limit int
}

// LogEntry is a record in a log of operations on a dataset
type LogEntry = logbook.LogEntry

// Logbook lists log entries for actions taken on a given dataset
func (r *LogRequests) Logbook(p *RefListParams, res *[]LogEntry) error {
if r.cli != nil {
return r.cli.Call("LogRequests.Logbook", p, res)
}
ctx := context.TODO()

ref, err := repo.ParseDatasetRef(p.Ref)
if err != nil {
return err
}
if err = repo.CanonicalizeDatasetRef(r.node.Repo, &ref); err != nil {
return err
}

book := r.node.Repo.Logbook()
*res, err = book.LogEntries(ctx, repo.ConvertToDsref(ref), p.Offset, p.Limit)
return err
}

// RawLogsParams enapsulates parameters for the RawLogs methods
type RawLogsParams struct {
// no options yet
}

// RawLogs encodes the full logbook as human-oriented json
func (r *LogRequests) RawLogs(p *RawLogsParams, res *interface{}) (err error) {
if r.cli != nil {
return r.cli.Call("LogRequests.RawLogs", p, res)
}
ctx := context.TODO()

*res = r.node.Repo.Logbook().RawLogs(ctx)
return err
}
47 changes: 47 additions & 0 deletions lib/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package lib

import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/qri-io/qri/config"
"github.com/qri-io/qri/p2p"
"github.com/qri-io/qri/repo"
Expand Down Expand Up @@ -66,3 +68,48 @@ func TestHistoryRequestsLog(t *testing.T) {
}
}
}

func TestHistoryRequestsLogEntries(t *testing.T) {
mr, refs, err := testrepo.NewTestRepoWithHistory()
if err != nil {
t.Fatalf("error allocating test repo: %s", err.Error())
return
}

node, err := p2p.NewQriNode(mr, config.DefaultP2PForTesting())
if err != nil {
t.Fatal(err.Error())
}

firstRef := refs[0].String()
req := NewLogRequests(node, nil)

if err = req.Logbook(&RefListParams{}, nil); err == nil {
t.Errorf("expected empty reference param to error")
}

res := []LogEntry{}
if err = req.Logbook(&RefListParams{Ref: firstRef, Limit: 30}, &res); err != nil {
t.Fatal(err)
}

result := make([]string, len(res))
for i := range res {
// set response times to zero for consistent results
res[i].Timestamp = time.Time{}
result[i] = res[i].String()
}

expect := []string{
`12:00AM peer init `,
`12:00AM peer save initial commit`,
`12:00AM peer save initial commit`,
`12:00AM peer save initial commit`,
`12:00AM peer save initial commit`,
`12:00AM peer save initial commit`,
}

if diff := cmp.Diff(expect, result); diff != "" {
t.Errorf("result mismatch (-want +got):\n%s", diff)
}
}
Loading

0 comments on commit 6a9ae8f

Please sign in to comment.