Skip to content

Commit

Permalink
Add noms commit --merge
Browse files Browse the repository at this point in the history
Add optional merging functionality to noms commit.
noms commit --merge <database> <left-dataset-name> <right-dataset-name>

The command above will look in the given Database for the two named
Datasets and, if possible, merge their HeadValue()s and commit the
result back to <right-dataset-name>. All the existing options to
`noms commit` are supported, to allow users to add metadata to the
Commit object that winds up in the Database.

Users can optionally provide a third Dataset name to commit to instead.

Fixes attic-labs#2535
  • Loading branch information
cmasone-attic committed Oct 27, 2016
1 parent e954427 commit 15fc056
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 337 deletions.
195 changes: 170 additions & 25 deletions cmd/noms/noms_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,100 @@ package main

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"regexp"

"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/merge"
"github.com/attic-labs/noms/go/spec"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/go/util/status"
"github.com/attic-labs/noms/go/util/verbose"
flag "github.com/juju/gnuflag"
)

var allowDupe bool
var (
allowDupe bool
doMerge bool
mergePolicy string

var nomsCommit = &util.Command{
Run: runCommit,
UsageLine: "commit [options] [absolute-path] <dataset>",
Short: "Commits a specified value as head of the dataset",
Long: "If absolute-path is not provided, then it is read from stdin. See Spelling Objects at https://github.com/attic-labs/noms/blob/master/doc/spelling.md for details on the dataset and absolute-path arguments.",
Flags: setupCommitFlags,
Nargs: 1, // if absolute-path not present we read it from stdin
}
nomsCommit = &util.Command{
Run: runCommit,
UsageLine: "commit [options] [absolute-path] <dataset> | commit [options] --merge [--policy=n|l|r|a] <database> <left-dataset-name> <right-dataset-name> [<output-dataset>]",
Short: "Commits a specified value as head of the dataset",
Long: "If absolute-path is not provided, then it is read from stdin. See Spelling Objects at https://github.com/attic-labs/noms/blob/master/doc/spelling.md for details on the dataset and absolute-path arguments.\nIf --merge is provided, you must provide a working database and at least the names of two datasets whose head values are to be merged. If you provide a third dataset name, its head will be set to this new merge commit. If not, the head of <right-dataset-name> will be set instead.",
Flags: setupCommitFlags,
Nargs: 1, // if absolute-path not present we read it from stdin
}
datasetRe = regexp.MustCompile("^" + datas.DatasetRe.String() + "$")
)

func setupCommitFlags() *flag.FlagSet {
commitFlagSet := flag.NewFlagSet("commit", flag.ExitOnError)
commitFlagSet.BoolVar(&allowDupe, "allow-dupe", false, "creates a new commit, even if it would be identical (modulo metadata and parents) to the existing HEAD.")
commitFlagSet.BoolVar(&doMerge, "merge", false, "")
commitFlagSet.StringVar(&mergePolicy, "policy", "n", "conflict resolution policy for merging. Defaults to 'n', which means no resolution strategy will be applied. Supported values are 'l' (left), 'r' (right) and 'a' (ask). 'ask' will bring up a simple command-line prompt allowing you to resolve conflicts by choosing between 'l' or 'r' on a case-by-case basis.")
spec.RegisterCommitMetaFlags(commitFlagSet)
verbose.RegisterVerboseFlags(commitFlagSet)
return commitFlagSet
}

func checkIfTrue(b bool, format string, args ...interface{}) {
if b {
d.CheckErrorNoUsage(fmt.Errorf(format, args...))
}
}

func runCommit(args []string) int {
cfg := config.NewResolver()
db, ds, err := cfg.GetDataset(args[len(args)-1])
d.CheckError(err)
defer db.Close()

var path string
if len(args) == 2 {
path = args[0]
var db datas.Database
var ds datas.Dataset
var err error
var value types.Value
var parents types.Set

if doMerge {
if len(args) < 3 || len(args) > 4 {
d.CheckError(fmt.Errorf("If --merge is specified, you must pass 3 or 4 arguments"))
}
db, err = cfg.GetDatabase(args[0])
d.CheckError(err)
defer db.Close()

leftDS, rightDS, outDS := resolveDatasets(db, args[1:]...)
ds = outDS

left, right, ancestor := getMergeCandidates(db, leftDS, rightDS)
policy := decidePolicy(mergePolicy)
pc := newMergeProgressChan()
value, err = policy(left, right, ancestor, db, pc)
d.CheckErrorNoUsage(err)
close(pc)
parents = types.NewSet(leftDS.HeadRef(), rightDS.HeadRef())
} else {
readPath, _, err := bufio.NewReader(os.Stdin).ReadLine()
db, ds, err = cfg.GetDataset(args[len(args)-1])
d.CheckError(err)
path = string(readPath)
}
absPath, err := spec.NewAbsolutePath(path)
d.CheckError(err)
defer db.Close()

value := absPath.Resolve(db)
if value == nil {
d.CheckErrorNoUsage(errors.New(fmt.Sprintf("Error resolving value: %s", path)))
var path string
if len(args) == 2 {
path = args[0]
} else {
readPath, _, err := bufio.NewReader(os.Stdin).ReadLine()
d.CheckError(err)
path = string(readPath)
}
absPath, err := spec.NewAbsolutePath(path)
d.CheckError(err)

value = absPath.Resolve(db)
checkIfTrue(value == nil, "Error resolving value: %s", path)
}

oldCommitRef, oldCommitExists := ds.MaybeHeadRef()
Expand All @@ -72,7 +114,7 @@ func runCommit(args []string) int {
meta, err := spec.CreateCommitMetaStruct(db, "", "", nil, nil)
d.CheckErrorNoUsage(err)

ds, err = db.Commit(ds, value, datas.CommitOptions{Meta: meta})
ds, err = db.Commit(ds, value, datas.CommitOptions{Meta: meta, Parents: parents})
d.CheckErrorNoUsage(err)

if oldCommitExists {
Expand All @@ -82,3 +124,106 @@ func runCommit(args []string) int {
}
return 0
}

func resolveDatasets(db datas.Database, names ...string) (leftDS, rightDS, outDS datas.Dataset) {
makeDS := func(dsName string) datas.Dataset {
if !datasetRe.MatchString(dsName) {
d.CheckError(fmt.Errorf("Invalid dataset %s, must match %s\n", dsName, datas.DatasetRe.String()))
}
return db.GetDataset(dsName)
}
leftDS = makeDS(names[0])
rightDS = makeDS(names[1])
outDS = makeDS(names[len(names)-1])
return
}

func getMergeCandidates(db datas.Database, leftDS, rightDS datas.Dataset) (left, right, ancestor types.Value) {
leftRef, ok := leftDS.MaybeHeadRef()
checkIfTrue(!ok, "Dataset %s has no data\n", leftDS.ID())
rightRef, ok := rightDS.MaybeHeadRef()
checkIfTrue(!ok, "Dataset %s has no data\n", rightDS.ID())
ancestorCommit, ok := getCommonAncestor(leftRef, rightRef, db)
checkIfTrue(!ok, "Datasets %s and %s have no common ancestor\n", leftDS.ID(), rightDS.ID())

return leftDS.HeadValue(), rightDS.HeadValue(), ancestorCommit.Get(datas.ValueField)
}

func getCommonAncestor(r1, r2 types.Ref, vr types.ValueReader) (a types.Struct, found bool) {
aRef, found := datas.FindCommonAncestor(r1, r2, vr)
if !found {
return
}
v := vr.ReadValue(aRef.TargetHash())
if v == nil {
panic(aRef.TargetHash().String() + " not found")
}
if !datas.IsCommitType(v.Type()) {
panic("Not a commit: " + types.EncodedValueMaxLines(v, 10) + " ...\n")
}
return v.(types.Struct), true
}

func newMergeProgressChan() chan struct{} {
pc := make(chan struct{}, 128)
go func() {
count := 0
for range pc {
if verbose.Verbose() {
count++
status.Printf("Applied %d changes...", count)
}
}
}()
return pc
}

func decidePolicy(policy string) merge.Policy {
var resolve merge.ResolveFunc
switch policy {
case "n", "N":
resolve = merge.None
case "l", "L":
resolve = merge.Ours
case "r", "R":
resolve = merge.Theirs
case "a", "A":
resolve = func(aType, bType types.DiffChangeType, a, b types.Value, path types.Path) (change types.DiffChangeType, merged types.Value, ok bool) {
return cliResolve(os.Stdin, os.Stdout, aType, bType, a, b, path)
}
default:
d.CheckError(fmt.Errorf("Unsupported merge policy: %s. Choices are n, l, r and a.", policy))
}
return merge.NewThreeWay(resolve)
}

func cliResolve(in io.Reader, out io.Writer, aType, bType types.DiffChangeType, a, b types.Value, path types.Path) (change types.DiffChangeType, merged types.Value, ok bool) {
stringer := func(v types.Value) (s string, success bool) {
switch v := v.(type) {
case types.Bool, types.Number, types.String:
return fmt.Sprintf("%v", v), true
}
return "", false
}
left, lOk := stringer(a)
right, rOk := stringer(b)
if !lOk || !rOk {
return change, merged, false
}

// TODO: Handle removes as well.
fmt.Fprintf(out, "\nConflict at: %s\n", path.String())
fmt.Fprintf(out, "Left: %s\nRight: %s\n\n", left, right)
var choice rune
for {
fmt.Fprintln(out, "Enter 'l' to accept the left value, 'r' to accept the right value")
_, err := fmt.Fscanf(in, "%c\n", &choice)
d.PanicIfError(err)
switch choice {
case 'l', 'L':
return aType, a, true
case 'r', 'R':
return bType, b, true
}
}
}

0 comments on commit 15fc056

Please sign in to comment.