diff --git a/cmd/noms/noms.go b/cmd/noms/noms.go index 67f6b6b771..21bbbe67ba 100644 --- a/cmd/noms/noms.go +++ b/cmd/noms/noms.go @@ -23,6 +23,7 @@ var commands = []*util.Command{ nomsLog, nomsMerge, nomsMigrate, + nomsRoot, nomsServe, nomsShow, nomsSync, diff --git a/cmd/noms/noms_root.go b/cmd/noms/noms_root.go new file mode 100644 index 0000000000..13c016754f --- /dev/null +++ b/cmd/noms/noms_root.go @@ -0,0 +1,112 @@ +// Copyright 2016 Attic Labs, Inc. All rights reserved. +// Licensed under the Apache License, version 2.0: +// http://www.apache.org/licenses/LICENSE-2.0 + +package main + +import ( + "fmt" + "os" + + "strings" + + "github.com/attic-labs/noms/cmd/util" + "github.com/attic-labs/noms/go/config" + "github.com/attic-labs/noms/go/d" + "github.com/attic-labs/noms/go/datas" + "github.com/attic-labs/noms/go/hash" + "github.com/attic-labs/noms/go/types" + flag "github.com/juju/gnuflag" +) + +var nomsRoot = &util.Command{ + Run: runRoot, + UsageLine: "root ", + Short: "Get or set the current root hash of the entire database", + Long: "See Spelling Objects at https://github.com/attic-labs/noms/blob/master/doc/spelling.md for details on the database argument.", + Flags: setupRootFlags, + Nargs: 1, +} + +var updateRoot = "" + +func setupRootFlags() *flag.FlagSet { + flagSet := flag.NewFlagSet("root", flag.ExitOnError) + flagSet.StringVar(&updateRoot, "update", "", "Replaces the entire database with the one with the given hash") + return flagSet +} + +func runRoot(args []string) int { + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "Not enough arguments") + return 0 + } + + cfg := config.NewResolver() + rt, err := cfg.GetRootTracker(args[0]) + d.CheckErrorNoUsage(err) + + currRoot := rt.Root() + + if updateRoot == "" { + fmt.Println(currRoot) + return 0 + } + + if updateRoot[0] == '#' { + updateRoot = updateRoot[1:] + } + h, ok := hash.MaybeParse(updateRoot) + if !ok { + fmt.Fprintf(os.Stderr, "Invalid hash: %s\n", h.String()) + return 1 + } + + db, err := cfg.GetDatabase(args[0]) + d.CheckErrorNoUsage(err) + defer db.Close() + if !validate(db.ReadValue(h)) { + return 1 + } + + fmt.Println(`WARNING + +This operation replaces the entire database with the instance having the given +hash. The old database becomes eligible for GC. + +ANYTHING NOT SAVED WILL BE LOST + +Continue? +`) + var input string + n, err := fmt.Scanln(&input) + d.CheckErrorNoUsage(err) + if n != 1 || strings.ToLower(input) != "y" { + return 0 + } + + ok = rt.UpdateRoot(h, currRoot) + if !ok { + fmt.Fprintln(os.Stderr, "Optimistic concurrency failure") + return 1 + } + + fmt.Printf("Success. Previous root was: %s\n", currRoot) + return 0 +} + +func validate(r types.Value) bool { + rootType := types.MakeMapType(types.StringType, types.MakeRefType(types.ValueType)) + if !types.IsSubtype(rootType, r.Type()) { + fmt.Fprintf(os.Stderr, "Root of database must be %s, but you specified: %s\n", rootType.Describe(), r.Type().Describe()) + return false + } + + return r.(types.Map).Any(func(k, v types.Value) bool { + if !datas.IsRefOfCommitType(v.Type()) { + fmt.Fprintf(os.Stderr, "Invalid root map. Value for key '%s' is not a ref of commit.", string(k.(types.String))) + return false + } + return true + }) +} diff --git a/cmd/noms/noms_root_test.go b/cmd/noms/noms_root_test.go new file mode 100644 index 0000000000..c2c58dcef6 --- /dev/null +++ b/cmd/noms/noms_root_test.go @@ -0,0 +1,43 @@ +// Copyright 2016 Attic Labs, Inc. All rights reserved. +// Licensed under the Apache License, version 2.0: +// http://www.apache.org/licenses/LICENSE-2.0 + +package main + +import ( + "testing" + + "github.com/attic-labs/noms/go/spec" + "github.com/attic-labs/noms/go/types" + "github.com/attic-labs/noms/go/util/clienttest" + "github.com/attic-labs/testify/suite" +) + +func TestNomsRoot(t *testing.T) { + suite.Run(t, &nomsRootTestSuite{}) +} + +type nomsRootTestSuite struct { + clienttest.ClientTestSuite +} + +func (s *nomsRootTestSuite) TestBasic() { + datasetName := "root-get" + dsSpec := spec.CreateValueSpecString("ldb", s.LdbDir, datasetName) + sp, err := spec.ForDataset(dsSpec) + s.NoError(err) + defer sp.Close() + + ds := sp.GetDataset() + dbSpecStr := spec.CreateDatabaseSpecString("ldb", s.LdbDir) + ds, _ = ds.Database().CommitValue(ds, types.String("hello!")) + c1, _ := s.MustRun(main, []string{"root", dbSpecStr}) + s.Equal("gt8mq6r7hvccp98s2vpeu9v9ct4rhloc\n", c1) + + ds, _ = ds.Database().CommitValue(ds, types.String("goodbye")) + c2, _ := s.MustRun(main, []string{"root", dbSpecStr}) + s.Equal("8tj5ctfhbka8fag417huneepg5ji283u\n", c2) + + // TODO: Would be good to test successful --update too, but requires changes to MustRun to allow + // input because of prompt :(. +} diff --git a/go/config/resolver.go b/go/config/resolver.go index b37e0226ee..b4bd93c711 100644 --- a/go/config/resolver.go +++ b/go/config/resolver.go @@ -104,6 +104,19 @@ func (r *Resolver) GetChunkStore(str string) (chunks.ChunkStore, error) { return sp.NewChunkStore(), nil } +// Resolve string to a RootTracker. Like ResolveDatabase, but returns a RootTracker instead +func (r *Resolver) GetRootTracker(str string) (chunks.RootTracker, error) { + sp, err := spec.ForDatabase(r.verbose(str, r.ResolveDbSpec(str))) + if err != nil { + return nil, err + } + var rt chunks.RootTracker = sp.NewChunkStore() + if rt == nil { + rt = datas.NewHTTPBatchStore(sp.Spec, "") + } + return rt, nil +} + // Resolve string to a dataset. If a config is present, // - if no db prefix is present, assume the default db // - if the db prefix is an alias, replace it diff --git a/go/datas/database.go b/go/datas/database.go index f58c011f65..f92e224eaa 100644 --- a/go/datas/database.go +++ b/go/datas/database.go @@ -94,8 +94,13 @@ type Database interface { // Regardless, Datasets() is updated to match backing storage upon return. FastForward(ds Dataset, newHeadRef types.Ref) (Dataset, error) - has(h hash.Hash) bool + // validatingBatchStore returns the BatchStore used to read and write + // groups of values to the database efficiently. This interface is a low- + // level detail of the database that should infrequently be needed by + // clients. validatingBatchStore() types.BatchStore + + has(h hash.Hash) bool } func NewDatabase(cs chunks.ChunkStore) Database { diff --git a/go/datas/database_test.go b/go/datas/database_test.go index 2087cdd96e..ac82d46f4c 100644 --- a/go/datas/database_test.go +++ b/go/datas/database_test.go @@ -60,7 +60,7 @@ type RemoteDatabaseSuite struct { func (suite *RemoteDatabaseSuite) SetupTest() { suite.cs = chunks.NewTestStore() suite.makeDb = func(cs chunks.ChunkStore) Database { - hbs := newHTTPBatchStoreForTest(cs) + hbs := NewHTTPBatchStoreForTest(cs) return &RemoteDatabaseClient{newDatabaseCommon(newCachingChunkHaver(hbs), types.NewValueStore(hbs), hbs)} } suite.db = suite.makeDb(suite.cs) diff --git a/go/datas/http_batch_store.go b/go/datas/http_batch_store.go index 4f7dda7af0..7d0e4b4103 100644 --- a/go/datas/http_batch_store.go +++ b/go/datas/http_batch_store.go @@ -53,7 +53,7 @@ type httpBatchStore struct { hints types.Hints } -func newHTTPBatchStore(baseURL, auth string) *httpBatchStore { +func NewHTTPBatchStore(baseURL, auth string) *httpBatchStore { u, err := url.Parse(baseURL) d.PanicIfError(err) if u.Scheme != "http" && u.Scheme != "https" { diff --git a/go/datas/http_batch_store_test.go b/go/datas/http_batch_store_test.go index 14e21bd037..87a342f034 100644 --- a/go/datas/http_batch_store_test.go +++ b/go/datas/http_batch_store_test.go @@ -49,10 +49,10 @@ func (serv inlineServer) Do(req *http.Request) (resp *http.Response, err error) func (suite *HTTPBatchStoreSuite) SetupTest() { suite.cs = chunks.NewTestStore() - suite.store = newHTTPBatchStoreForTest(suite.cs) + suite.store = NewHTTPBatchStoreForTest(suite.cs) } -func newHTTPBatchStoreForTest(cs chunks.ChunkStore) *httpBatchStore { +func NewHTTPBatchStoreForTest(cs chunks.ChunkStore) *httpBatchStore { serv := inlineServer{httprouter.New()} serv.POST( constants.WriteValuePath, @@ -84,7 +84,7 @@ func newHTTPBatchStoreForTest(cs chunks.ChunkStore) *httpBatchStore { HandleRootGet(w, req, ps, cs) }, ) - hcs := newHTTPBatchStore("http://localhost:9000", "") + hcs := NewHTTPBatchStore("http://localhost:9000", "") hcs.httpClient = serv return hcs } @@ -102,7 +102,7 @@ func newAuthenticatingHTTPBatchStoreForTest(suite *HTTPBatchStoreSuite, hostUrl HandleRootPost(w, req, ps, suite.cs) }, ) - hcs := newHTTPBatchStore(hostUrl, "") + hcs := NewHTTPBatchStore(hostUrl, "") hcs.httpClient = serv return hcs } @@ -116,7 +116,7 @@ func newBadVersionHTTPBatchStoreForTest(suite *HTTPBatchStoreSuite) *httpBatchSt w.Header().Set(NomsVersionHeader, "BAD") }, ) - hcs := newHTTPBatchStore("http://localhost", "") + hcs := NewHTTPBatchStore("http://localhost", "") hcs.httpClient = serv return hcs } @@ -210,7 +210,7 @@ func (b *backpressureCS) PutMany(chnx []chunks.Chunk) chunks.BackpressureError { func (suite *HTTPBatchStoreSuite) TestPutChunksBackpressure() { bpcs := &backpressureCS{ChunkStore: suite.cs} - bs := newHTTPBatchStoreForTest(bpcs) + bs := NewHTTPBatchStoreForTest(bpcs) defer bs.Close() defer bpcs.Close() diff --git a/go/datas/pull_test.go b/go/datas/pull_test.go index 8ac2ca9f9e..1663470b6f 100644 --- a/go/datas/pull_test.go +++ b/go/datas/pull_test.go @@ -85,7 +85,7 @@ func (suite *RemoteToRemoteSuite) SetupTest() { } func makeRemoteDb(cs chunks.ChunkStore) Database { - hbs := newHTTPBatchStoreForTest(cs) + hbs := NewHTTPBatchStoreForTest(cs) return &RemoteDatabaseClient{newDatabaseCommon(newCachingChunkHaver(hbs), types.NewValueStore(hbs), hbs)} } diff --git a/go/datas/remote_database_client.go b/go/datas/remote_database_client.go index def95411e8..2206836e34 100644 --- a/go/datas/remote_database_client.go +++ b/go/datas/remote_database_client.go @@ -15,7 +15,7 @@ type RemoteDatabaseClient struct { } func NewRemoteDatabase(baseURL, auth string) *RemoteDatabaseClient { - httpBS := newHTTPBatchStore(baseURL, auth) + httpBS := NewHTTPBatchStore(baseURL, auth) return &RemoteDatabaseClient{newDatabaseCommon(newCachingChunkHaver(httpBS), types.NewValueStore(httpBS), httpBS)} } diff --git a/go/types/map.go b/go/types/map.go index 5b7ab31c6b..085f929162 100644 --- a/go/types/map.go +++ b/go/types/map.go @@ -247,6 +247,18 @@ func (m Map) Iter(cb mapIterCallback) { }) } +// Any returns true if cb() return true for any of the items in the map. +func (m Map) Any(cb func(k, v Value) bool) (yep bool) { + m.Iter(func(k, v Value) bool { + if cb(k, v) { + yep = true + return true + } + return false + }) + return +} + type mapIterAllCallback func(key, value Value) func (m Map) IterAll(cb mapIterAllCallback) { diff --git a/go/types/map_test.go b/go/types/map_test.go index ffd5a044e8..2f3463588a 100644 --- a/go/types/map_test.go +++ b/go/types/map_test.go @@ -808,6 +808,18 @@ func TestMapIter2(t *testing.T) { doTest(getTestRefToValueOrderMap(2, NewTestValueStore())) } +func TestMapAny(t *testing.T) { + assert := assert.New(t) + + p := func(k, v Value) bool { + return k.Equals(String("foo")) && v.Equals(String("bar")) + } + + assert.False(NewMap().Any(p)) + assert.False(NewMap(String("foo"), String("baz")).Any(p)) + assert.True(NewMap(String("foo"), String("bar")).Any(p)) +} + func TestMapIterAll(t *testing.T) { if testing.Short() { t.Skip("Skipping test in short mode.")