Skip to content

Commit

Permalink
fix(restore): Restore will delete components that didn’t exist in pre…
Browse files Browse the repository at this point in the history
…vious version
  • Loading branch information
dustmop committed Sep 3, 2019
1 parent 2ff6693 commit 7f64f85
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 29 deletions.
100 changes: 100 additions & 0 deletions cmd/fsi_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,106 @@ func TestRestorePreviousVersion(t *testing.T) {
}
}

// Test that restore deletes a component that didn't exist before
func TestRestoreDeleteComponent(t *testing.T) {
fr := NewFSITestRunner(t, "qri_test_restore_delete_component")
defer fr.Delete()

// First version has only a body
err := fr.ExecCommand("qri save --body=testdata/movies/body_ten.csv me/del_cmp")
if err != nil {
t.Fatalf(err.Error())
}
_ = parseRefFromSave(fr.GetCommandOutput())

fr.ChdirToRoot()

// Checkout the newly created dataset.
if err := fr.ExecCommand("qri checkout me/del_cmp"); err != nil {
t.Fatalf(err.Error())
}

workDir := fr.ChdirToWorkDir("del_cmp")

// Modify the body.json file.
modifyFileUsingStringReplace("body.csv", "Avatar", "The Avengers")

// Modify meta.json by changing the title.
if err = ioutil.WriteFile("meta.json", []byte(`{"title": "hello"}`), os.ModePerm); err != nil {
t.Fatalf(err.Error())
}

// Restore to get erase the meta component.
if err := fr.ExecCommand("qri restore meta"); err != nil {
t.Fatalf(err.Error())
}

// Verify the directory contains the files that we expect.
dirContents := listDirectory(workDir)
expectContents := []string{".qri-ref", "body.csv", "dataset.json", "schema.json"}
if diff := cmp.Diff(dirContents, expectContents); diff != "" {
t.Errorf("directory contents (-want +got):\n%s", diff)
}

// Status, check that the working directory has added files.
if err := fr.ExecCommand("qri status"); err != nil {
t.Fatalf(err.Error())
}

output := fr.GetCommandOutput()
expect := `for linked dataset [test_peer/del_cmp]
modified: body (source: body.csv)
run ` + "`qri save`" + ` to commit this dataset
`
if diff := cmpTextLines(expect, output); diff != "" {
t.Errorf("qri status (-want +got):\n%s", diff)
}
}

// Test that restore deletes a component if there was no previous version
func TestRestoreWithNoHistory(t *testing.T) {
fr := NewFSITestRunner(t, "qri_test_restore_no_history")
defer fr.Delete()

workDir := fr.CreateAndChdirToWorkDir("new_folder")

// Init as a linked directory.
if err := fr.ExecCommand("qri init --name new_folder --format csv"); err != nil {
t.Fatalf(err.Error())
}

// Restore to get erase the meta component.
if err := fr.ExecCommand("qri restore meta"); err != nil {
t.Fatalf(err.Error())
}

// Verify the directory contains the files that we expect.
dirContents := listDirectory(workDir)
expectContents := []string{".qri-ref", "body.csv", "schema.json"}
if diff := cmp.Diff(dirContents, expectContents); diff != "" {
t.Errorf("directory contents (-want +got):\n%s", diff)
}

// Status, check that the working directory has added files.
if err := fr.ExecCommand("qri status"); err != nil {
t.Fatalf(err.Error())
}

output := fr.GetCommandOutput()
expect := `for linked dataset [test_peer/new_folder]
add: schema (source: schema.json)
add: body (source: body.csv)
run ` + "`qri save`" + ` to commit this dataset
`
if diff := cmpTextLines(expect, output); diff != "" {
t.Errorf("qri status (-want +got):\n%s", diff)
}
}

func parseRefFromSave(output string) string {
pos := strings.Index(output, "saved: ")
ref := output[pos+7:]
Expand Down
4 changes: 2 additions & 2 deletions cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NewStatusCommand(f Factory, ioStreams ioes.IOStreams) *cobra.Command {
type StatusOptions struct {
ioes.IOStreams

Refs *RefSelect
Refs *RefSelect
ShowMtime bool

FSIMethods *lib.FSIMethods
Expand Down Expand Up @@ -95,7 +95,7 @@ func (o *StatusOptions) Run() (err error) {
if o.ShowMtime && !si.Mtime.IsZero() {
padding := ""
if len(line) < ColumnPositionForMtime {
padding = strings.Repeat(" ", ColumnPositionForMtime - len(line))
padding = strings.Repeat(" ", ColumnPositionForMtime-len(line))
}
line = fmt.Sprintf("%s%s%s", line, padding, si.Mtime.Format("2006-01-02 15:04:05"))
}
Expand Down
12 changes: 12 additions & 0 deletions fsi/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,15 @@ func WriteComponents(ds *dataset.Dataset, dirPath string) error {

return nil
}

// DeleteComponents removes the list of named components from the given directory
func DeleteComponents(removeList []string, fileMap map[string]FileStat, dirPath string) error {
for _, comp := range removeList {
removeFile := fileMap[comp].Path
// TODO(dlong): Collect errors and return them all, instead of bailing immediately
if err := os.Remove(removeFile); err != nil {
return err
}
}
return nil
}
14 changes: 7 additions & 7 deletions fsi/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ var (

// StatusItem is a component that has status representation on the filesystem
type StatusItem struct {
SourceFile string `json:"sourceFile"`
Component string `json:"component"`
Type string `json:"type"`
Message string `json:"message"`
SourceFile string `json:"sourceFile"`
Component string `json:"component"`
Type string `json:"type"`
Message string `json:"message"`
Mtime time.Time `json:"mtime"`
}

Expand Down Expand Up @@ -333,13 +333,13 @@ func (fsi *FSI) StatusAtVersion(refStr string) (changes []StatusItem, err error)

fileMap := make(map[string]FileStat)
if next.Meta != nil {
fileMap["meta"] = FileStat{Path:"meta"}
fileMap["meta"] = FileStat{Path: "meta"}
}
if next.BodyPath != "" || next.BodyFile() != nil {
fileMap["body"] = FileStat{Path:"body"}
fileMap["body"] = FileStat{Path: "body"}
}
if next.Structure != nil && next.Structure.Schema != nil {
fileMap["schema"] = FileStat{Path:"schema"}
fileMap["schema"] = FileStat{Path: "schema"}
}
return fsi.CalculateStateTransition(prev, next, fileMap, nil)
}
Expand Down
75 changes: 55 additions & 20 deletions lib/fsi.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ func (m *FSIMethods) Restore(p *RestoreParams, out *string) (err error) {
if err != nil {
return fmt.Errorf("'%s' is not a valid dataset reference", p.Ref)
}
if err = repo.CanonicalizeDatasetRef(m.inst.node.Repo, &ref); err != nil {
err = repo.CanonicalizeDatasetRef(m.inst.node.Repo, &ref)
if err != nil && err != repo.ErrNoHistory {
return
}

Expand All @@ -227,47 +228,81 @@ func (m *FSIMethods) Restore(p *RestoreParams, out *string) (err error) {
// TODO(dlong): Perhaps disallow empty Dir (without FSIPath override), since relative
// paths cause problems. Test using `qri connect`.

ds, err := dsfs.LoadDataset(m.inst.node.Repo.Store(), ref.Path)
if err != nil {
return fmt.Errorf("loading dataset: %s", err)
// Read the previous version of the dataset from the repo
var ds *dataset.Dataset
if ref.Path == "" {
ds = &dataset.Dataset{}
} else {
ds, err = dsfs.LoadDataset(m.inst.node.Repo.Store(), ref.Path)
if err != nil {
return fmt.Errorf("loading dataset: %s", err)
}
if err = base.OpenDataset(m.inst.node.Repo.Filesystem(), ds); err != nil {
return
}
}

if err = base.OpenDataset(m.inst.node.Repo.Filesystem(), ds); err != nil {
return
current, currFileMap, _, err := fsi.ReadDir(p.Dir)
if err != nil {
return err
}

removeComponents := []string{}

var history dataset.Dataset
history.Structure = &dataset.Structure{}
history.Structure.Format = ds.Structure.Format
if ds.Structure != nil {
history.Structure.Format = ds.Structure.Format
} else {
// TODO(dlong): This assumes we have a version-less working directory, created by
// `qri init`, which by default starts with a body.csv file.
history.Structure.Format = "csv"
}
if p.Component == "" {
// Entire dataset.
history.Assign(ds)
} else if p.Component == "meta" {
// Meta component.
history.Meta = &dataset.Meta{}
history.Meta.Assign(ds.Meta)
if current.Meta != nil && !current.Meta.IsEmpty() && (ds.Meta == nil || ds.Meta.IsEmpty()) {
removeComponents = append(removeComponents, "meta")
}
} else if p.Component == "schema" || p.Component == "structure.schema" {
// Schema is not a "real" component, is short for the structure's schema.
history.Structure.Schema = ds.Structure.Schema
} else if p.Component == "body" {
// Body of the dataset.
df, err := dataset.ParseDataFormatString(history.Structure.Format)
if err != nil {
return err
if ds.Structure != nil {
history.Structure.Schema = ds.Structure.Schema
}
fcfg, err := dataset.ParseFormatConfigMap(df, map[string]interface{}{})
if err != nil {
return err
if len(current.Structure.Schema) > 0 && (ds.Structure == nil || len(ds.Structure.Schema) == 0) {
removeComponents = append(removeComponents, "schema")
}
bufData, err := actions.GetBody(m.inst.node, ds, df, fcfg, -1, -1, true)
if err != nil {
return err
} else if p.Component == "body" {
// Body of the dataset.
// This check for ref.Path is equivilant to making sure there's a previous version.
if ref.Path != "" {
df, err := dataset.ParseDataFormatString(history.Structure.Format)
if err != nil {
return err
}
fcfg, err := dataset.ParseFormatConfigMap(df, map[string]interface{}{})
if err != nil {
return err
}
bufData, err := actions.GetBody(m.inst.node, ds, df, fcfg, -1, -1, true)
if err != nil {
return err
}
history.SetBodyFile(qfs.NewMemfileBytes("body", bufData))
} else {
removeComponents = append(removeComponents, "body")
}
history.SetBodyFile(qfs.NewMemfileBytes("body", bufData))
} else {
return fmt.Errorf("Unknown component name \"%s\"", p.Component)
}

// Delete components that exist in the working directory but did not exist in previous version.
fsi.DeleteComponents(removeComponents, currFileMap, p.Dir)

// Write components of the dataset to the working directory.
return fsi.WriteComponents(&history, p.Dir)
}
Expand Down

0 comments on commit 7f64f85

Please sign in to comment.