diff --git a/README.md b/README.md index 1e4f986..06ddbb8 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ COMMANDS: commit Record changes daemon Starts a client init Create a repo + log Print repo history merge Merge commits status Print changes tag List, create, or delete tags diff --git a/cmd/multi/branch.go b/cmd/multi/branch.go index 820d50e..3f1ac5f 100644 --- a/cmd/multi/branch.go +++ b/cmd/multi/branch.go @@ -60,7 +60,7 @@ func branchAction(c *cli.Context) error { } var reply rpc.BranchReply - if err = client.Call(action, &args, &reply); err != nil { + if err := client.Call(action, &args, &reply); err != nil { return err } diff --git a/cmd/multi/checkout.go b/cmd/multi/checkout.go index a21a943..9f17135 100644 --- a/cmd/multi/checkout.go +++ b/cmd/multi/checkout.go @@ -49,7 +49,7 @@ func checkoutAction(c *cli.Context) error { } var reply rpc.CheckoutReply - if err = client.Call("Service.Checkout", &args, &reply); err != nil { + if err := client.Call("Service.Checkout", &args, &reply); err != nil { return err } diff --git a/cmd/multi/clone.go b/cmd/multi/clone.go index f620e38..9df2dfb 100644 --- a/cmd/multi/clone.go +++ b/cmd/multi/clone.go @@ -63,7 +63,7 @@ func cloneAction(c *cli.Context) error { } var reply rpc.CloneReply - if err = client.Call("Service.Clone", &args, &reply); err != nil { + if err := client.Call("Service.Clone", &args, &reply); err != nil { return err } diff --git a/cmd/multi/commit.go b/cmd/multi/commit.go index d7431a0..d04b39c 100644 --- a/cmd/multi/commit.go +++ b/cmd/multi/commit.go @@ -51,7 +51,7 @@ func commitAction(c *cli.Context) error { } var reply rpc.CommitReply - if err = client.Call("Service.Commit", &args, &reply); err != nil { + if err := client.Call("Service.Commit", &args, &reply); err != nil { return err } diff --git a/cmd/multi/init.go b/cmd/multi/init.go index c376025..6bc6e16 100644 --- a/cmd/multi/init.go +++ b/cmd/multi/init.go @@ -41,7 +41,7 @@ func initAction(c *cli.Context) error { } var reply rpc.InitReply - if err = client.Call("Service.Init", &args, &reply); err != nil { + if err := client.Call("Service.Init", &args, &reply); err != nil { return err } diff --git a/cmd/multi/log.go b/cmd/multi/log.go new file mode 100644 index 0000000..96abc78 --- /dev/null +++ b/cmd/multi/log.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "os" + + "github.com/multiverse-vcs/go-multiverse/rpc" + "github.com/urfave/cli/v2" +) + +const logDateFormat = "Mon Jan 02 15:04:05 2006 -0700" + +var logCommand = &cli.Command{ + Action: logAction, + Name: "log", + Usage: "Print repo history", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "limit", + Aliases: []string{"l"}, + Usage: "Log limit", + Value: -1, + }, + }, +} + +func logAction(c *cli.Context) error { + cwd, err := os.Getwd() + if err != nil { + return err + } + + config, err := LoadConfig(cwd) + if err != nil { + return err + } + + client, err := rpc.NewClient() + if err != nil { + return err + } + + args := rpc.LogArgs{ + Name: config.Name, + Branch: config.Branch, + Limit: c.Int("limit"), + } + + var reply rpc.LogReply + if err := client.Call("Service.Log", &args, &reply); err != nil { + return err + } + + for i, c := range reply.Commits { + fmt.Printf("commit %s\n", reply.IDs[i].String()) + fmt.Printf("Date: %s\n", c.Date.Format(logDateFormat)) + fmt.Printf("\n\t%s\n\n", c.Message) + } + + return nil +} diff --git a/cmd/multi/main.go b/cmd/multi/main.go index 8fa3760..5e334fe 100644 --- a/cmd/multi/main.go +++ b/cmd/multi/main.go @@ -21,6 +21,7 @@ var app = &cli.App{ commitCommand, daemonCommand, initCommand, + logCommand, mergeCommand, statusCommand, tagCommand, diff --git a/cmd/multi/merge.go b/cmd/multi/merge.go index cd724c7..7839d5c 100644 --- a/cmd/multi/merge.go +++ b/cmd/multi/merge.go @@ -49,7 +49,7 @@ func mergeAction(c *cli.Context) error { } var reply rpc.MergeReply - if err = client.Call("Service.Merge", &args, &reply); err != nil { + if err := client.Call("Service.Merge", &args, &reply); err != nil { return err } diff --git a/cmd/multi/status.go b/cmd/multi/status.go index adf58b6..ff0a793 100644 --- a/cmd/multi/status.go +++ b/cmd/multi/status.go @@ -43,7 +43,7 @@ func statusAction(c *cli.Context) error { } var reply rpc.StatusReply - if err = client.Call("Service.Status", &args, &reply); err != nil { + if err := client.Call("Service.Status", &args, &reply); err != nil { return err } diff --git a/cmd/multi/tag.go b/cmd/multi/tag.go index 03bc3ef..85b959c 100644 --- a/cmd/multi/tag.go +++ b/cmd/multi/tag.go @@ -60,7 +60,7 @@ func tagAction(c *cli.Context) error { } var reply rpc.TagReply - if err = client.Call(action, &args, &reply); err != nil { + if err := client.Call(action, &args, &reply); err != nil { return err } diff --git a/core/merge_base.go b/core/merge_base.go index a50c759..77dc8a1 100644 --- a/core/merge_base.go +++ b/core/merge_base.go @@ -9,8 +9,13 @@ import ( // MergeBase returns the best common ancestor of local and remote. func MergeBase(ctx context.Context, dag ipld.DAGService, local, remote cid.Cid) (cid.Cid, error) { - history, err := Walk(ctx, dag, local, nil) - if err != nil { + history := make(map[string]bool) + visit := func(id cid.Cid) bool { + history[id.KeyString()] = true + return true + } + + if err := Walk(ctx, dag, local, visit); err != nil { return cid.Cid{}, err } @@ -26,7 +31,7 @@ func MergeBase(ctx context.Context, dag ipld.DAGService, local, remote cid.Cid) // find the least common ancestor by searching // for commits that are in both local and remote // and that are also independent from each other - cb := func(id cid.Cid) bool { + visit = func(id cid.Cid) bool { if err0 != nil { return false } @@ -42,7 +47,7 @@ func MergeBase(ctx context.Context, dag ipld.DAGService, local, remote cid.Cid) return false } - if _, err := Walk(ctx, dag, remote, cb); err != nil { + if err := Walk(ctx, dag, remote, visit); err != nil { return cid.Cid{}, err } @@ -55,14 +60,15 @@ func IsAncestor(ctx context.Context, dag ipld.DAGService, parent, child cid.Cid) return false, nil } - cb := func(id cid.Cid) bool { - return id != child + var match bool + visit := func(id cid.Cid) bool { + match = (id == child) + return !match } - history, err := Walk(ctx, dag, parent, cb) - if err != nil { + if err := Walk(ctx, dag, parent, visit); err != nil { return false, err } - return history[child.KeyString()], nil + return match, nil } diff --git a/core/status_test.go b/core/status_test.go index 8ddeb2b..08d6431 100644 --- a/core/status_test.go +++ b/core/status_test.go @@ -64,5 +64,3 @@ func TestStatus(t *testing.T) { t.Fatalf("unexpected change type") } } - - diff --git a/core/walk.go b/core/walk.go index d74da03..28393c8 100644 --- a/core/walk.go +++ b/core/walk.go @@ -9,11 +9,8 @@ import ( "github.com/multiverse-vcs/go-multiverse/data" ) -// WalkFun is called for each commit visited by walk. -type WalkFun func(cid.Cid) bool - // Walk performs a depth first traversal of parent commits starting at the given id. -func Walk(ctx context.Context, dag ipld.DAGService, id cid.Cid, cb WalkFun) (map[string]bool, error) { +func Walk(ctx context.Context, dag ipld.DAGService, id cid.Cid, visit func(cid.Cid) bool) error { getLinks := func(ctx context.Context, id cid.Cid) ([]*ipld.Link, error) { commit, err := data.GetCommit(ctx, dag, id) if err != nil { @@ -23,19 +20,15 @@ func Walk(ctx context.Context, dag ipld.DAGService, id cid.Cid, cb WalkFun) (map return commit.ParentLinks(), nil } - history := make(map[string]bool) - visit := func(id cid.Cid) bool { - if history[id.KeyString()] { + seen := make(map[string]bool) + wrap := func(id cid.Cid) bool { + if seen[id.KeyString()] { return false } - history[id.KeyString()] = true - if cb != nil { - return cb(id) - } - - return true + seen[id.KeyString()] = true + return visit(id) } - return history, merkledag.Walk(ctx, getLinks, id, visit) + return merkledag.Walk(ctx, getLinks, id, wrap) } diff --git a/core/walk_test.go b/core/walk_test.go index 72d01a3..165f8b6 100644 --- a/core/walk_test.go +++ b/core/walk_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/ipfs/go-cid" "github.com/ipfs/go-merkledag/dagutils" "github.com/multiverse-vcs/go-multiverse/data" "github.com/multiverse-vcs/go-multiverse/unixfs" @@ -30,20 +31,25 @@ func TestWalk(t *testing.T) { t.Fatalf("failed to commit") } - history, err := Walk(ctx, dag, idB, nil) - if err != nil { + var ids []cid.Cid + cb := func(id cid.Cid) bool { + ids = append(ids, id) + return true + } + + if err := Walk(ctx, dag, idB, cb); err != nil { t.Fatalf("failed to walk") } - if len(history) != 2 { + if len(ids) != 2 { t.Fatalf("cids do not match") } - if !history[idA.KeyString()] { + if ids[0] != idB { t.Errorf("cids do not match") } - if !history[idB.KeyString()] { + if ids[1] != idA { t.Errorf("cids do not match") } } diff --git a/data/store.go b/data/store.go index 42c808d..c1f31dd 100644 --- a/data/store.go +++ b/data/store.go @@ -53,4 +53,4 @@ func (s *Store) Keys() ([]string, error) { } return keys, nil -} \ No newline at end of file +} diff --git a/rpc/branch_test.go b/rpc/branch_test.go index 8053f70..5c4ca71 100644 --- a/rpc/branch_test.go +++ b/rpc/branch_test.go @@ -2,6 +2,7 @@ package rpc import ( "context" + "io/ioutil" "testing" "github.com/ipfs/go-datastore" @@ -9,14 +10,6 @@ import ( "github.com/multiverse-vcs/go-multiverse/peer" ) -var repositoryJSON = []byte(`{ - "name": "test", - "branches": { - "default": {"/": "bagaybqabciqeutn2u7n3zuk5b4ykgfwpkekb7ctgnlwik5zfr6bcukvknj2jtpa"} - }, - "tags": {} -}`) - func TestListBranches(t *testing.T) { ctx := context.Background() @@ -25,26 +18,31 @@ func TestListBranches(t *testing.T) { mock, err := peer.Mock(ctx, dstore) if err != nil { - t.Fatalf("failed to create peer") + t.Fatal("failed to create peer") + } + + json, err := ioutil.ReadFile("testdata/repository.json") + if err != nil { + t.Fatal("failed to read json") } - repo, err := data.RepositoryFromJSON(repositoryJSON) + repo, err := data.RepositoryFromJSON(json) if err != nil { - t.Fatalf("failed to parse repository json") + t.Fatal("failed to parse repository json") } id, err := data.AddRepository(ctx, mock, repo) if err != nil { - t.Fatalf("failed to create repository %s", err) + t.Fatal("failed to create repository") } if err := store.PutCid(repo.Name, id); err != nil { - t.Fatalf("failed to put cid in store") + t.Fatal("failed to put cid in store") } client, err := connect(mock, store) if err != nil { - t.Fatalf("failed to connect to rpc server") + t.Fatal("failed to connect to rpc server") } args := BranchArgs{ @@ -53,14 +51,14 @@ func TestListBranches(t *testing.T) { var reply BranchReply if err := client.Call("Service.ListBranches", &args, &reply); err != nil { - t.Fatalf("failed to call rpc: %s", err) + t.Fatal("failed to call rpc") } if len(reply.Branches) != 1 { - t.Errorf("unexpected branches") + t.Error("unexpected branches") } if reply.Branches["default"] != repo.Branches["default"] { - t.Errorf("unexpected branches") + t.Error("unexpected branches") } } diff --git a/rpc/init.go b/rpc/init.go index 6abe5ea..52d47b1 100644 --- a/rpc/init.go +++ b/rpc/init.go @@ -14,7 +14,7 @@ type InitArgs struct { } // InitReply contains the reply. -type InitReply struct {} +type InitReply struct{} // Init creates a new empty repository. func (s *Service) Init(args *InitArgs, reply *InitReply) error { diff --git a/rpc/log.go b/rpc/log.go new file mode 100644 index 0000000..adc415e --- /dev/null +++ b/rpc/log.go @@ -0,0 +1,74 @@ +package rpc + +import ( + "context" + "errors" + + "github.com/ipfs/go-cid" + "github.com/multiverse-vcs/go-multiverse/core" + "github.com/multiverse-vcs/go-multiverse/data" +) + +// LogArgs contains the args. +type LogArgs struct { + // Name is the repo name + Name string + // Branch is the name of the repo branch. + Branch string + // Limit is the number of commits to log. + Limit int +} + +// LogReply contains the reply. +type LogReply struct { + IDs []cid.Cid + Commits []*data.Commit +} + +// Log returns the changes between the working directory and repo head. +func (s *Service) Log(args *LogArgs, reply *LogReply) error { + ctx := context.Background() + + id, err := s.store.GetCid(args.Name) + if err != nil { + return err + } + + repo, err := data.GetRepository(ctx, s.client, id) + if err != nil { + return err + } + + head, ok := repo.Branches[args.Branch] + if !ok { + return errors.New("branch does not exist") + } + + var ids []cid.Cid + visit := func(id cid.Cid) bool { + if args.Limit > -1 && len(ids) >= args.Limit { + return false + } + + ids = append(ids, id) + return true + } + + if err := core.Walk(ctx, s.client, head, visit); err != nil { + return err + } + + var commits []*data.Commit + for _, id := range ids { + commit, err := data.GetCommit(ctx, s.client, id) + if err != nil { + return err + } + + commits = append(commits, commit) + } + + reply.IDs = ids + reply.Commits = commits + return nil +} diff --git a/rpc/log_test.go b/rpc/log_test.go new file mode 100644 index 0000000..f71f253 --- /dev/null +++ b/rpc/log_test.go @@ -0,0 +1,85 @@ +package rpc + +import ( + "context" + "io/ioutil" + "testing" + + "github.com/ipfs/go-datastore" + "github.com/multiverse-vcs/go-multiverse/data" + "github.com/multiverse-vcs/go-multiverse/peer" +) + +func TestLog(t *testing.T) { + ctx := context.Background() + + dstore := datastore.NewMapDatastore() + store := data.NewStore(dstore) + + mock, err := peer.Mock(ctx, dstore) + if err != nil { + t.Fatal("failed to create peer") + } + + json, err := ioutil.ReadFile("testdata/repository.json") + if err != nil { + t.Fatal("failed to read json") + } + + repo, err := data.RepositoryFromJSON(json) + if err != nil { + t.Fatal("failed to parse repository json") + } + + id, err := data.AddRepository(ctx, mock, repo) + if err != nil { + t.Fatal("failed to add repository") + } + + if err := store.PutCid(repo.Name, id); err != nil { + t.Fatal("failed to put cid in store") + } + + json, err = ioutil.ReadFile("testdata/commit.json") + if err != nil { + t.Fatal("failed to read json") + } + + commit, err := data.CommitFromJSON(json) + if err != nil { + t.Fatal("failed to parse commit json") + } + + head, err := data.AddCommit(ctx, mock, commit) + if err != nil { + t.Fatal("failed to add commit") + } + + client, err := connect(mock, store) + if err != nil { + t.Fatal("failed to connect to rpc server") + } + + args := LogArgs{ + Name: repo.Name, + Branch: "default", + Limit: 1, + } + + var reply LogReply + if err := client.Call("Service.Log", &args, &reply); err != nil { + t.Fatal("failed to call rpc") + } + + if len(reply.IDs) != 1 { + t.Fatal("unexpected ids") + } + + if reply.IDs[0] != head { + t.Error("unexpected log id") + } + + if len(reply.Commits) != 1 { + t.Fatal("unexpected commits") + } +} diff --git a/rpc/rpc_test.go b/rpc/rpc_test.go index df9afe7..288ac08 100644 --- a/rpc/rpc_test.go +++ b/rpc/rpc_test.go @@ -25,7 +25,7 @@ func connect(client *peer.Client, store *data.Store) (*rpc.Client, error) { return nil, err } - defer listener.Close() + //defer listener.Close() go server.Accept(listener) return rpc.Dial("tcp", listener.Addr().String()) diff --git a/rpc/testdata/commit.json b/rpc/testdata/commit.json new file mode 100644 index 0000000..5342518 --- /dev/null +++ b/rpc/testdata/commit.json @@ -0,0 +1,7 @@ +{ + "id": "bafyreieo2mhnqyqntenwyndzxoovw5nhbpit727kjrl3mjbyb5nv6zs2pu", + "date": "2020-10-25T15:26:12.168056-07:00", + "message": "init", + "parents": null, + "tree": {"/": "QmQycvPQd5tAVP4Xx1dp1Yfb9tmjKQAa5uxPoTfUQr9tFZ"} +} \ No newline at end of file diff --git a/rpc/testdata/repository.json b/rpc/testdata/repository.json new file mode 100644 index 0000000..971d480 --- /dev/null +++ b/rpc/testdata/repository.json @@ -0,0 +1,7 @@ +{ + "name": "test", + "branches": { + "default": {"/": "bafyreieo2mhnqyqntenwyndzxoovw5nhbpit727kjrl3mjbyb5nv6zs2pu"} + }, + "tags": {} +} \ No newline at end of file diff --git a/web/commits.go b/web/commits.go index a320b72..827a489 100644 --- a/web/commits.go +++ b/web/commits.go @@ -53,7 +53,7 @@ func (s *Server) Commits(w http.ResponseWriter, req *http.Request) error { return true } - if _, err := core.Walk(ctx, s.client, head, visit); err != nil { + if err := core.Walk(ctx, s.client, head, visit); err != nil { return err }