Skip to content

Commit

Permalink
Add FindCommonAncestor for Commits
Browse files Browse the repository at this point in the history
Once we integrate noms-merge into the `noms commit` command, this
function will allow us to stop requiring users to pass in the common
ancestor to be used when merging. The code can just find it and merge
away.

Toward attic-labs#2535
  • Loading branch information
cmasone-attic committed Sep 15, 2016
1 parent 2660d47 commit 1705139
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 19 deletions.
68 changes: 68 additions & 0 deletions go/datas/commit.go
Expand Up @@ -5,6 +5,9 @@
package datas

import (
"fmt"
"sort"

"github.com/attic-labs/noms/go/d"
"github.com/attic-labs/noms/go/types"
)
Expand Down Expand Up @@ -66,6 +69,71 @@ func CommitDescendsFrom(commit types.Struct, ancestor types.Ref, vr types.ValueR
return true
}

// FindCommonAncestor returns the most recent common ancestor of c1 and c2, if
// one exists, setting ok to true. If there is no common ancestor, ok is set to false.
func FindCommonAncestor(c1, c2 types.Struct, vr types.ValueReader) (a types.Struct, ok bool) {
c1Q, c2Q := &types.RefByHeight{types.NewRef(c1)}, &types.RefByHeight{types.NewRef(c2)}

makeSet := func(refs types.RefSlice) types.Set {
vals := make(types.ValueSlice, refs.Len())
for i, r := range refs {
vals[i] = r
}
return types.NewSet(vals...)
}

for !c1Q.Empty() && !c2Q.Empty() {
c1Ht, c2Ht := c1Q.MaxHeight(), c2Q.MaxHeight()
if c1Ht == c2Ht {
c1p, c2p := c1Q.PopRefsOfHeight(c1Ht), c2Q.PopRefsOfHeight(c2Ht)
c1s, c2s := makeSet(c1p), makeSet(c2p)
intersection := types.NewIntersectionIterator(c1s.Iterator(), c2s.Iterator())
if common := intersection.Next(); common != nil {
return common.(types.Ref).TargetValue(vr).(types.Struct), true
}

refParentsToQueue(c1p, c1Q, vr)
refParentsToQueue(c2p, c2Q, vr)
} else if c1Ht > c2Ht {
refParentsToQueue(c1Q.PopRefsOfHeight(c1Ht), c1Q, vr)
} else {
refParentsToQueue(c2Q.PopRefsOfHeight(c2Ht), c2Q, vr)
}
}
return
}

func printQ(q *types.RefByHeight, vr types.ValueReader) {
for i := 0; i < q.Len(); i++ {
r := q.PeekAt(i)
fmt.Printf("%v (height %d), ", r.TargetValue(vr).(types.Struct).Get(ValueField), r.Height())
}
fmt.Println("")
}

func printSet(refs types.RefSlice, vr types.ValueReader) {
for _, r := range refs {
fmt.Printf("%v (height %d), ", r.TargetValue(vr).(types.Struct).Get(ValueField), r.Height())
}
fmt.Println("")
}

func parentsAsRefs(c types.Struct, q *types.RefByHeight) {
p := c.Get(ParentsField).(types.Set)
p.IterAll(func(v types.Value) {
q.PushBack(v.(types.Ref))
})
sort.Sort(q)
}

func refParentsToQueue(refs types.RefSlice, q *types.RefByHeight, vr types.ValueReader) {
for _, r := range refs {
c := r.TargetValue(vr).(types.Struct)
parentsAsRefs(c, q) // room to improve...
}
sort.Sort(q)
}

// getAncestors returns set of direct ancestors with height >= minHeight
func getAncestors(commits types.Set, minHeight uint64, vr types.ValueReader) types.Set {
ancestors := types.NewSet()
Expand Down
76 changes: 76 additions & 0 deletions go/datas/commit_test.go
Expand Up @@ -118,6 +118,82 @@ func toValuesString(refSet types.Set, vr types.ValueReader) string {
return strings.Join(values, ",")
}

func TestFindCommonAncestor(t *testing.T) {
assert := assert.New(t)
db := NewDatabase(chunks.NewTestStore())
defer db.Close()

// Add a commit and return it
addCommit := func(ds string, val string, parents ...types.Struct) types.Struct {
commit := NewCommit(types.String(val), toRefSet(parents...), types.EmptyStruct)
var err error
db, err = db.Commit(ds, commit)
assert.NoError(err)
return commit
}

// Assert that c is the common ancestor of a and b
assertCommonAncestor := func(expected, a, b types.Struct) {
if found, ok := FindCommonAncestor(a, b, db); assert.True(ok) {
assert.True(
expected.Equals(found),
"%s should be common ancestor of %s, %s. Got %s",
expected.Get(ValueField),
a.Get(ValueField),
b.Get(ValueField),
found.Get(ValueField),
)
}
}

// Build commit DAG
//
// ds-a: a1<-a2<-a3<-a4<-a5<-a6
// ^ ^ ^ |
// | \ \----\ /-/
// | \ \V
// ds-b: \ b3<-b4<-b5
// \
// \
// ds-c: c2<-c3
// /
// /
// V
// ds-d: d1<-d2
//
a, b, c, d := "ds-a", "ds-b", "ds-c", "ds-d"
a1 := addCommit(a, "a1")
d1 := addCommit(d, "d1")
a2 := addCommit(a, "a2", a1)
c2 := addCommit(c, "c2", a1)
d2 := addCommit(d, "d2", d1)
a3 := addCommit(a, "a3", a2)
b3 := addCommit(b, "b3", a2)
c3 := addCommit(c, "c3", c2, d2)
a4 := addCommit(a, "a4", a3)
b4 := addCommit(b, "b4", b3)
a5 := addCommit(a, "a5", a4)
b5 := addCommit(b, "b5", b4, a3)
a6 := addCommit(a, "a6", a5, b5)

assertCommonAncestor(a1, a1, a1) // All self
assertCommonAncestor(a1, a1, a2) // One side self
assertCommonAncestor(a2, a3, b3) // Common parent
assertCommonAncestor(a2, a4, b4) // Common grandparent
assertCommonAncestor(a1, a6, c3) // Traversing multiple parents on both sides

// No common ancestor
if found, ok := FindCommonAncestor(d2, a6, db); !assert.False(ok) {
assert.Fail(
"Unexpected common ancestor!",
"Should be no common ancestor of %s, %s. Got %s",
d2.Get(ValueField),
a6.Get(ValueField),
found.Get(ValueField),
)
}
}

func TestCommitDescendsFrom(t *testing.T) {
assert := assert.New(t)
db := NewDatabase(chunks.NewTestStore())
Expand Down
26 changes: 7 additions & 19 deletions go/datas/pull.go
Expand Up @@ -166,35 +166,27 @@ type traverseResult struct {
// If one queue is 'taller' than the other, it's clear that we can process all refs from the taller queue with height greater than the height of the 'shorter' queue. We should also be able to process refs from the taller queue that are of the same height as the shorter queue, as long as we also check to see if they're common to both queues. It is not safe, however, to pull unique items off the shorter queue at this point. It's possible that, in processing some of the Refs from the taller queue, that these Refs will be discovered to be common after all.
// TODO: Bug 2203
func planWork(srcQ, sinkQ *types.RefByHeight) (srcRefs, sinkRefs, comRefs types.RefSlice) {
srcHt, sinkHt := tallestHeight(srcQ), tallestHeight(sinkQ)
srcHt, sinkHt := srcQ.MaxHeight(), sinkQ.MaxHeight()
if srcHt > sinkHt {
srcRefs = popRefsOfHeight(srcQ, srcHt)
srcRefs = srcQ.PopRefsOfHeight(srcHt)
return
}
if sinkHt > srcHt {
sinkRefs = popRefsOfHeight(sinkQ, sinkHt)
sinkRefs = sinkQ.PopRefsOfHeight(sinkHt)
return
}
d.PanicIfFalse(srcHt == sinkHt)
srcRefs, comRefs = findCommon(srcQ, sinkQ, srcHt)
sinkRefs = popRefsOfHeight(sinkQ, sinkHt)
return
}

func popRefsOfHeight(q *types.RefByHeight, height uint64) (refs types.RefSlice) {
for tallestHeight(q) == height {
r := q.PopBack()
refs = append(refs, r)
}
sinkRefs = sinkQ.PopRefsOfHeight(sinkHt)
return
}

func findCommon(taller, shorter *types.RefByHeight, height uint64) (tallRefs, comRefs types.RefSlice) {
d.PanicIfFalse(tallestHeight(taller) == height)
d.PanicIfFalse(tallestHeight(shorter) == height)
d.PanicIfFalse(taller.MaxHeight() == height)
d.PanicIfFalse(shorter.MaxHeight() == height)
comIndices := []int{}
// Walk through shorter and taller in tandem from the back (where the tallest Refs are). Refs from taller that go into a work queue are popped off directly, but doing so to shorter would mess up shortIdx. So, instead just keep track of the indices of common refs and drop them from shorter at the end.
for shortIdx := shorter.Len() - 1; !taller.Empty() && tallestHeight(taller) == height; {
for shortIdx := shorter.Len() - 1; !taller.Empty() && taller.MaxHeight() == height; {
tallPeek := taller.PeekEnd()
shortPeek := shorter.PeekAt(shortIdx)
if types.HeightOrder(tallPeek, shortPeek) {
Expand All @@ -211,10 +203,6 @@ func findCommon(taller, shorter *types.RefByHeight, height uint64) (tallRefs, co
return
}

func tallestHeight(h *types.RefByHeight) uint64 {
return h.PeekEnd().Height()
}

func sendWork(ch chan<- types.Ref, refs types.RefSlice) {
for _, r := range refs {
ch <- r
Expand Down
14 changes: 14 additions & 0 deletions go/types/ref_heap.go
Expand Up @@ -69,6 +69,20 @@ func (h *RefByHeight) Unique() {
*h = result
}

// PopRefsOfHeight pops off and returns all refs r in h for which r.Height() == height.
func (h *RefByHeight) PopRefsOfHeight(height uint64) (refs RefSlice) {
for h.MaxHeight() == height {
r := h.PopBack()
refs = append(refs, r)
}
return
}

// MaxHeight returns the height of the 'tallest' Ref in h.
func (h RefByHeight) MaxHeight() uint64 {
return h.PeekEnd().Height()
}

func (h RefByHeight) Empty() bool {
return h.Len() == 0
}
Expand Down

0 comments on commit 1705139

Please sign in to comment.