Explorar o código

Merge pull request #85 from vmarkovtsev/master

Fix getting the results from a tree history
Vadim Markovtsev %!s(int64=6) %!d(string=hai) anos
pai
achega
b50415b72c

+ 1 - 1
Makefile

@@ -39,5 +39,5 @@ ${GOPATH}/pkg/$(PKG)/gopkg.in/bblfsh/client-go.v2: ${GOPATH}/src/gopkg.in/bblfsh
 	cd ${GOPATH}/src/gopkg.in/bblfsh/client-go.v2 && \
 	make dependencies
 
-${GOPATH}/bin/hercules${EXE}: *.go */*.go */*/*.go ${GOPATH}/pkg/$(PKG)/gopkg.in/bblfsh/client-go.v2 internal/pb/pb.pb.go internal/pb/pb_pb2.py cmd/hercules/plugin_template_source.go
+${GOPATH}/bin/hercules${EXE}: *.go */*.go */*/*.go */*/*/*.go ${GOPATH}/pkg/$(PKG)/gopkg.in/bblfsh/client-go.v2 internal/pb/pb.pb.go internal/pb/pb_pb2.py cmd/hercules/plugin_template_source.go
 	go get -tags "$(TAGS)" -ldflags "-X gopkg.in/src-d/hercules.v4.BinaryGitHash=$(shell git rev-parse HEAD)" gopkg.in/src-d/hercules.v4/cmd/hercules

+ 2 - 2
OCTOPUS.md

@@ -29,9 +29,9 @@ which pipeline instance to apply.
   
 ### Major changes
 
-* `Pipeline`
+* `Pipeline` - done
   * `Commits()` - done
   * `Run()` - done
 * `Burndown` - done
-* `Couples`
+* `Couples` - done
 * `FileDiff` - done

+ 1 - 1
cmd/hercules/root.go

@@ -170,7 +170,7 @@ targets can be added using the --plugin system.`,
 					bar.SetMaxWidth(80)
 					bar.Start()
 				}
-				if commit == length {
+				if commit == length - 1 {
 					bar.Finish()
 					fmt.Fprint(os.Stderr, "\r"+strings.Repeat(" ", 80)+"\rfinalizing...")
 				} else {

+ 5 - 0
core.go

@@ -94,6 +94,11 @@ func ForkCopyPipelineItem(origin PipelineItem, n int) []PipelineItem {
 	return core.ForkCopyPipelineItem(origin ,n)
 }
 
+// IsMergeCommit indicates whether the commit is a merge or not.
+func IsMergeCommit(deps map[string]interface{}) bool {
+	return core.IsMergeCommit(deps)
+}
+
 // PipelineItemRegistry contains all the known PipelineItem-s.
 type PipelineItemRegistry = core.PipelineItemRegistry
 

+ 23 - 38
internal/burndown/file.go

@@ -8,11 +8,8 @@ import (
 	"gopkg.in/src-d/hercules.v4/internal/rbtree"
 )
 
-// Status is the something we would like to keep track of in File.Update().
-type Status struct {
-	data   interface{}
-	update func(interface{}, int, int, int)
-}
+// Updater is the function which is called back on File.Update().
+type Updater = func(currentTime, previousTime, delta int)
 
 // File encapsulates a balanced binary tree to store line intervals and
 // a cumulative mapping of values to the corresponding length counters. Users
@@ -30,13 +27,7 @@ type File struct {
 	Hash     plumbing.Hash
 
 	tree     *rbtree.RBTree
-	statuses []Status
-}
-
-// NewStatus initializes a new instance of Status struct. It is needed to set the only two
-// private fields which are not supposed to be replaced during the whole lifetime.
-func NewStatus(data interface{}, update func(interface{}, int, int, int)) Status {
-	return Status{data: data, update: update}
+	updaters []Updater
 }
 
 // TreeEnd denotes the value of the last leaf in the tree.
@@ -47,13 +38,16 @@ const TreeMaxBinPower = 14
 // TreeMergeMark is the special day which disables the status updates and is used in File.Merge().
 const TreeMergeMark = (1 << TreeMaxBinPower) - 1
 
-func (file *File) updateTime(currentTime int, previousTime int, delta int) {
+func (file *File) updateTime(currentTime, previousTime, delta int) {
 	if currentTime & TreeMergeMark == TreeMergeMark {
 		// merge mode
 		return
 	}
-	for _, status := range file.statuses {
-		status.update(status.data, currentTime, previousTime, delta)
+	if previousTime & TreeMergeMark == TreeMergeMark {
+		previousTime = currentTime
+	}
+	for _, update := range file.updaters {
+		update(currentTime, previousTime, delta)
 	}
 }
 
@@ -64,11 +58,11 @@ func (file *File) updateTime(currentTime int, previousTime int, delta int) {
 // length is the starting length of the tree (the key of the second and the
 // last node);
 //
-// statuses are the attached interval length mappings.
-func NewFile(hash plumbing.Hash, time int, length int, statuses ...Status) *File {
-	file := &File{Hash: hash, tree: new(rbtree.RBTree), statuses: statuses}
+// updaters are the attached interval length mappings.
+func NewFile(hash plumbing.Hash, time int, length int, updaters ...Updater) *File {
+	file := &File{Hash: hash, tree: new(rbtree.RBTree), updaters: updaters}
+	file.updateTime(time, time, length)
 	if length > 0 {
-		file.updateTime(time, time, length)
 		file.tree.Insert(rbtree.Item{Key: 0, Value: time})
 	}
 	file.tree.Insert(rbtree.Item{Key: length, Value: TreeEnd})
@@ -82,9 +76,9 @@ func NewFile(hash plumbing.Hash, time int, length int, statuses ...Status) *File
 //
 // vals is a slice with the starting tree values. Must match the size of keys.
 //
-// statuses are the attached interval length mappings.
-func NewFileFromTree(hash plumbing.Hash, keys []int, vals []int, statuses ...Status) *File {
-	file := &File{Hash: hash, tree: new(rbtree.RBTree), statuses: statuses}
+// updaters are the attached interval length mappings.
+func NewFileFromTree(hash plumbing.Hash, keys []int, vals []int, updaters ...Updater) *File {
+	file := &File{Hash: hash, tree: new(rbtree.RBTree), updaters: updaters}
 	if len(keys) != len(vals) {
 		panic("keys and vals must be of equal length")
 	}
@@ -96,15 +90,15 @@ func NewFileFromTree(hash plumbing.Hash, keys []int, vals []int, statuses ...Sta
 }
 
 // Clone copies the file. It performs a deep copy of the tree;
-// depending on `clearStatuses` the original statuses are removed or not.
-// Any new `statuses` are appended.
-func (file *File) Clone(clearStatuses bool, statuses ...Status) *File {
-	clone := &File{Hash: file.Hash, tree: file.tree.Clone(), statuses: file.statuses}
+// depending on `clearStatuses` the original updaters are removed or not.
+// Any new `updaters` are appended.
+func (file *File) Clone(clearStatuses bool, updaters ...Updater) *File {
+	clone := &File{Hash: file.Hash, tree: file.tree.Clone(), updaters: file.updaters}
 	if clearStatuses {
-		clone.statuses = []Status{}
+		clone.updaters = []Updater{}
 	}
-	for _, status := range statuses {
-		clone.statuses = append(clone.statuses, status)
+	for _, updater := range updaters {
+		clone.updaters = append(clone.updaters, updater)
 	}
 	return clone
 }
@@ -299,15 +293,6 @@ func (file *File) Merge(day int, others... *File) bool {
 	return true
 }
 
-// Status returns the bound status object by the specified index.
-func (file *File) Status(index int) interface{} {
-	if index < 0 || index >= len(file.statuses) {
-		panic(fmt.Sprintf("status index %d is out of bounds [0, %d)",
-			index, len(file.statuses)))
-	}
-	return file.statuses[index].data
-}
-
 // Dump formats the underlying line interval tree into a string.
 // Useful for error messages, panic()-s and debugging.
 func (file *File) Dump() string {

+ 21 - 16
internal/burndown/file_test.go

@@ -9,14 +9,15 @@ import (
 	"gopkg.in/src-d/go-git.v4/plumbing"
 )
 
-func updateStatusFile(
-	status interface{}, _ int, previousTime int, delta int) {
-	status.(map[int]int64)[previousTime] += int64(delta)
+func updateStatusFile(status map[int]int64, _, previousTime, delta int) {
+	status[previousTime] += int64(delta)
 }
 
 func fixtureFile() (*File, map[int]int64) {
 	status := map[int]int64{}
-	file := NewFile(plumbing.ZeroHash, 0, 100, NewStatus(status, updateStatusFile))
+	file := NewFile(plumbing.ZeroHash, 0, 100, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
 	return file, status
 }
 
@@ -111,7 +112,9 @@ func TestCloneFileClearStatus(t *testing.T) {
 	file.Update(4, 20, 10, 0)
 	// 0 0 | 20 4 | 30 1 | 50 0 | 130 -1        [0]: 100, [1]: 20, [4]: 10
 	newStatus := map[int]int64{}
-	clone := file.Clone(true, NewStatus(newStatus, updateStatusFile))
+	clone := file.Clone(true, func(a, b, c int) {
+		updateStatusFile(newStatus, a, b, c)
+	})
 	clone.Update(5, 45, 0, 10)
 	// 0 0 | 20 4 | 30 1 | 45 0 | 120 -1        [0]: -5, [1]: -5
 	clone.Update(6, 45, 5, 0)
@@ -168,8 +171,10 @@ func TestInsertFile(t *testing.T) {
 
 func TestZeroInitializeFile(t *testing.T) {
 	status := map[int]int64{}
-	file := NewFile(plumbing.ZeroHash, 0, 0, NewStatus(status, updateStatusFile))
-	assert.NotContains(t, status, 0)
+	file := NewFile(plumbing.ZeroHash, 0, 0, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
+	assert.Contains(t, status, 0)
 	dump := file.Dump()
 	// Output:
 	// 0 -1
@@ -419,7 +424,9 @@ func TestBug3File(t *testing.T) {
 
 func TestBug4File(t *testing.T) {
 	status := map[int]int64{}
-	file := NewFile(plumbing.ZeroHash, 0, 10, NewStatus(status, updateStatusFile))
+	file := NewFile(plumbing.ZeroHash, 0, 10, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
 	file.Update(125, 0, 20, 9)
 	file.Update(125, 0, 20, 20)
 	file.Update(166, 12, 1, 1)
@@ -449,14 +456,18 @@ func TestBug5File(t *testing.T) {
 	status := map[int]int64{}
 	keys := []int{0, 2, 4, 7, 10}
 	vals := []int{24, 28, 24, 28, -1}
-	file := NewFileFromTree(plumbing.ZeroHash, keys, vals, NewStatus(status, updateStatusFile))
+	file := NewFileFromTree(plumbing.ZeroHash, keys, vals, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
 	file.Update(28, 0, 1, 3)
 	dump := file.Dump()
 	assert.Equal(t, "0 28\n2 24\n5 28\n8 -1\n", dump)
 
 	keys = []int{0, 1, 16, 18}
 	vals = []int{305, 0, 157, -1}
-	file = NewFileFromTree(plumbing.ZeroHash, keys, vals, NewStatus(status, updateStatusFile))
+	file = NewFileFromTree(plumbing.ZeroHash, keys, vals, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
 	file.Update(310, 0, 0, 2)
 	dump = file.Dump()
 	assert.Equal(t, "0 0\n14 157\n16 -1\n", dump)
@@ -484,12 +495,6 @@ func TestUpdatePanic(t *testing.T) {
 	assert.Contains(t, paniced, "invalid tree state")
 }
 
-func TestFileStatus(t *testing.T) {
-	f, _ := fixtureFile()
-	assert.Panics(t, func() { f.Status(1) })
-	assert.NotNil(t, f.Status(0))
-}
-
 func TestFileValidate(t *testing.T) {
 	keys := [...]int{0}
 	vals := [...]int{-1}

+ 192 - 90
internal/core/forks.go

@@ -8,7 +8,7 @@ import (
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/hercules.v4/internal/toposort"
-)
+		)
 
 // OneShotMergeProcessor provides the convenience method to consume merges only once.
 type OneShotMergeProcessor struct {
@@ -34,6 +34,11 @@ func (proc *OneShotMergeProcessor) ShouldConsumeCommit(deps map[string]interface
 	return false
 }
 
+// IsMergeCommit indicates whether the commit is a merge or not.
+func IsMergeCommit(deps map[string]interface{}) bool {
+	return deps[DependencyCommit].(*object.Commit).NumParents() > 1
+}
+
 // NoopMerger provides an empty Merge() method suitable for PipelineItem.
 type NoopMerger struct {
 }
@@ -82,6 +87,8 @@ type runAction struct {
 	Items []int
 }
 
+type orderer = func(reverse, direction bool) []string
+
 func cloneItems(origin []PipelineItem, n int) [][]PipelineItem {
 	clones := make([][]PipelineItem, n)
 	for j := 0; j < n; j++ {
@@ -124,12 +131,11 @@ func prepareRunPlan(commits []*object.Commit) []runAction {
 	hashes, dag := buildDag(commits)
 	leaveRootComponent(hashes, dag)
 	numParents := bindNumParents(hashes, dag)
-	mergedDag, mergedSeq := mergeDag(numParents, hashes, dag)
+	mergedDag, mergedSeq := mergeDag(hashes, dag)
 	orderNodes := bindOrderNodes(mergedDag)
 	collapseFastForwards(orderNodes, hashes, mergedDag, dag, mergedSeq)
-
 	/*fmt.Printf("digraph Hercules {\n")
-	for i, c := range order {
+	for i, c := range orderNodes(false, false) {
 		commit := hashes[c]
 		fmt.Printf("  \"%s\"[label=\"[%d] %s\"]\n", commit.Hash.String(), i, commit.Hash.String()[:6])
 		for _, child := range mergedDag[commit.Hash] {
@@ -137,9 +143,19 @@ func prepareRunPlan(commits []*object.Commit) []runAction {
 		}
 	}
 	fmt.Printf("}\n")*/
-
 	plan := generatePlan(orderNodes, numParents, hashes, mergedDag, dag, mergedSeq)
 	plan = optimizePlan(plan)
+	/*for _, p := range plan {
+		firstItem := p.Items[0]
+		switch p.Action {
+		case runActionCommit:
+			fmt.Fprintln(os.Stderr, "C", firstItem, p.Commit.Hash.String())
+		case runActionFork:
+			fmt.Fprintln(os.Stderr, "F", p.Items)
+		case runActionMerge:
+			fmt.Fprintln(os.Stderr, "M", p.Items)
+		}
+	}*/
 	return plan
 }
 
@@ -251,8 +267,8 @@ func leaveRootComponent(
 }
 
 // bindOrderNodes returns curried "orderNodes" function.
-func bindOrderNodes(mergedDag map[plumbing.Hash][]*object.Commit) func(reverse bool) []string {
-	return func(reverse bool) []string {
+func bindOrderNodes(mergedDag map[plumbing.Hash][]*object.Commit) orderer {
+	return func(reverse, direction bool) []string {
 		graph := toposort.NewGraph()
 		keys := make([]plumbing.Hash, 0, len(mergedDag))
 		for key := range mergedDag {
@@ -268,7 +284,11 @@ func bindOrderNodes(mergedDag map[plumbing.Hash][]*object.Commit) func(reverse b
 				return children[i].Hash.String() < children[j].Hash.String()
 			})
 			for _, c := range children {
-				graph.AddEdge(key.String(), c.Hash.String())
+				if !direction {
+					graph.AddEdge(key.String(), c.Hash.String())
+				} else {
+					graph.AddEdge(c.Hash.String(), key.String())
+				}
 			}
 		}
 		order, ok := graph.Toposort()
@@ -276,7 +296,7 @@ func bindOrderNodes(mergedDag map[plumbing.Hash][]*object.Commit) func(reverse b
 			// should never happen
 			panic("Could not topologically sort the DAG of commits")
 		}
-		if reverse {
+		if reverse != direction {
 			// one day this must appear in the standard library...
 			for i, j := 0, len(order)-1; i < len(order)/2; i, j = i+1, j-1 {
 				order[i], order[j] = order[j], order[i]
@@ -288,111 +308,180 @@ func bindOrderNodes(mergedDag map[plumbing.Hash][]*object.Commit) func(reverse b
 
 // mergeDag turns sequences of consecutive commits into single nodes.
 func mergeDag(
-	numParents func(c *object.Commit) int,
 	hashes map[string]*object.Commit,
 	dag map[plumbing.Hash][]*object.Commit) (
 	mergedDag, mergedSeq map[plumbing.Hash][]*object.Commit) {
 
-	parentOf := func(c *object.Commit) plumbing.Hash {
-		var parent plumbing.Hash
-		for _, p := range c.ParentHashes {
-			if _, exists := hashes[p.String()]; exists {
-				if parent != plumbing.ZeroHash {
-					// more than one parent
-					return plumbing.ZeroHash
-				}
-				parent = p
-			}
+	parents := map[plumbing.Hash][]plumbing.Hash{}
+	for key, vals := range dag {
+		for _, val := range vals {
+			parents[val.Hash] = append(parents[val.Hash], key)
 		}
-		return parent
 	}
 	mergedDag = map[plumbing.Hash][]*object.Commit{}
 	mergedSeq = map[plumbing.Hash][]*object.Commit{}
 	visited := map[plumbing.Hash]bool{}
-	for ch := range dag {
-		c := hashes[ch.String()]
-		if visited[c.Hash] {
+	for head := range dag {
+		if visited[head] {
 			continue
 		}
+		c := head
 		for true {
-			parent := parentOf(c)
-			if parent == plumbing.ZeroHash || len(dag[parent]) != 1 {
+			next := parents[c]
+			if len(next) != 1 || len(dag[next[0]]) != 1 {
 				break
 			}
-			c = hashes[parent.String()]
+			c = next[0]
 		}
-		head := c
+		head = c
 		var seq []*object.Commit
-		children := dag[c.Hash]
 		for true {
-			visited[c.Hash] = true
-			seq = append(seq, c)
-			if len(children) != 1 {
+			visited[c] = true
+			seq = append(seq, hashes[c.String()])
+			if len(dag[c]) != 1 {
 				break
 			}
-			c = children[0]
-			children = dag[c.Hash]
-			if numParents(c) != 1 {
+			c = dag[c][0].Hash
+			if len(parents[c]) != 1 {
 				break
 			}
 		}
-		mergedSeq[head.Hash] = seq
-		mergedDag[head.Hash] = dag[seq[len(seq)-1].Hash]
+		mergedSeq[head] = seq
+		mergedDag[head] = dag[seq[len(seq)-1].Hash]
 	}
 	return
 }
 
 // collapseFastForwards removes the fast forward merges.
 func collapseFastForwards(
-	orderNodes func(reverse bool) []string,
-	hashes map[string]*object.Commit,
+	orderNodes orderer, hashes map[string]*object.Commit,
 	mergedDag, dag, mergedSeq map[plumbing.Hash][]*object.Commit)  {
 
-	for _, strkey := range orderNodes(true) {
+	parents := map[plumbing.Hash][]plumbing.Hash{}
+	for key, vals := range mergedDag {
+		for _, val := range vals {
+			parents[val.Hash] = append(parents[val.Hash], key)
+		}
+	}
+	processed := map[plumbing.Hash]bool{}
+	for _, strkey := range orderNodes(false, true) {
 		key := hashes[strkey].Hash
+		processed[key] = true
+		repeat:
 		vals, exists := mergedDag[key]
 		if !exists {
 			continue
 		}
-		if len(vals) == 2 {
-			grand1 := mergedDag[vals[0].Hash]
-			grand2 := mergedDag[vals[1].Hash]
-			if len(grand2) == 1 && vals[0].Hash == grand2[0].Hash {
-				mergedDag[key] = mergedDag[vals[0].Hash]
-				dag[key] = vals[1:]
-				delete(mergedDag, vals[0].Hash)
-				delete(mergedDag, vals[1].Hash)
-				mergedSeq[key] = append(mergedSeq[key], mergedSeq[vals[1].Hash]...)
-				mergedSeq[key] = append(mergedSeq[key], mergedSeq[vals[0].Hash]...)
-				delete(mergedSeq, vals[0].Hash)
-				delete(mergedSeq, vals[1].Hash)
+		if len(vals) < 2 {
+			continue
+		}
+		toRemove := map[plumbing.Hash]bool{}
+		for _, child := range vals {
+			var queue []plumbing.Hash
+			visited := map[plumbing.Hash]bool{child.Hash: true}
+			childParents := parents[child.Hash]
+			childNumOtherParents := 0
+			for _, parent := range childParents {
+				if parent != key {
+					visited[parent] = true
+					childNumOtherParents++
+					queue = append(queue, parent)
+				}
+			}
+			var immediateParent plumbing.Hash
+			if childNumOtherParents == 1 {
+				immediateParent = queue[0]
+			}
+			for len(queue) > 0 {
+				head := queue[len(queue)-1]
+				queue = queue[:len(queue)-1]
+				if processed[head] {
+					if head == key {
+						toRemove[child.Hash] = true
+						if childNumOtherParents == 1 && len(mergedDag[immediateParent]) == 1 {
+							mergedSeq[immediateParent] = append(
+								mergedSeq[immediateParent], mergedSeq[child.Hash]...)
+							delete(mergedSeq, child.Hash)
+							mergedDag[immediateParent] = mergedDag[child.Hash]
+							delete(mergedDag, child.Hash)
+							parents[child.Hash] = parents[immediateParent]
+							for _, vals := range parents {
+								for i, v := range vals {
+									if v == child.Hash {
+										vals[i] = immediateParent
+										break
+									}
+								}
+							}
+						}
+					}
+					break
+				}
+				for _, parent := range parents[head] {
+					if !visited[parent] {
+						visited[head] = true
+						queue = append(queue, parent)
+					}
+				}
+			}
+		}
+		if len(toRemove) == 0 {
+			continue
+		}
+		var newVals []*object.Commit
+		for _, child := range vals {
+			if !toRemove[child.Hash] {
+				newVals = append(newVals, child)
+			}
+		}
+		merged := false
+		if len(newVals) == 1 {
+			onlyChild := newVals[0].Hash
+			if len(parents[onlyChild]) == 1 {
+				merged = true
+				mergedSeq[key] = append(mergedSeq[key], mergedSeq[onlyChild]...)
+				delete(mergedSeq, onlyChild)
+				mergedDag[key] = mergedDag[onlyChild]
+				delete(mergedDag, onlyChild)
+				parents[onlyChild] = parents[key]
+				for _, vals := range parents {
+					for i, v := range vals {
+						if v == onlyChild {
+							vals[i] = key
+							break
+						}
+					}
+				}
 			}
-			// symmetric
-			if len(grand1) == 1 && vals[1].Hash == grand1[0].Hash {
-				mergedDag[key] = mergedDag[vals[1].Hash]
-				dag[key] = vals[:1]
-				delete(mergedDag, vals[0].Hash)
-				delete(mergedDag, vals[1].Hash)
-				mergedSeq[key] = append(mergedSeq[key], mergedSeq[vals[0].Hash]...)
-				mergedSeq[key] = append(mergedSeq[key], mergedSeq[vals[1].Hash]...)
-				delete(mergedSeq, vals[0].Hash)
-				delete(mergedSeq, vals[1].Hash)
+		}
+		if !merged {
+			mergedDag[key] = newVals
+		}
+		newVals = []*object.Commit{}
+		node := mergedSeq[key][len(mergedSeq[key])-1].Hash
+		for _, child := range dag[node] {
+			if !toRemove[child.Hash] {
+				newVals = append(newVals, child)
 			}
 		}
+		dag[node] = newVals
+		if merged {
+			goto repeat
+		}
 	}
 }
 
 // generatePlan creates the list of actions from the commit DAG.
 func generatePlan(
-	orderNodes func(reverse bool) []string,
-	numParents func(c *object.Commit) int,
+	orderNodes orderer, numParents func(c *object.Commit) int,
 	hashes map[string]*object.Commit,
 	mergedDag, dag, mergedSeq map[plumbing.Hash][]*object.Commit) []runAction {
 
 	var plan []runAction
 	branches := map[plumbing.Hash]int{}
+	branchers := map[plumbing.Hash]map[plumbing.Hash]int{}
 	counter := 1
-	for seqIndex, name := range orderNodes(false) {
+	for seqIndex, name := range orderNodes(false, true) {
 		commit := hashes[name]
 		if seqIndex == 0 {
 			branches[commit.Hash] = 0
@@ -412,6 +501,7 @@ func generatePlan(
 				Commit: c,
 				Items: []int{branch},
 			})
+
 		}
 		appendMergeIfNeeded := func() {
 			if numParents(commit) < 2 {
@@ -421,22 +511,31 @@ func generatePlan(
 			var items []int
 			minBranch := 1 << 31
 			for _, parent := range commit.ParentHashes {
-				if _, exists := hashes[parent.String()]; exists {
-					parentBranch := branches[parent]
-					if len(dag[parent]) == 1 && minBranch > parentBranch {
-						minBranch = parentBranch
-					}
-					items = append(items, parentBranch)
-					if parentBranch != branch {
-						appendCommit(commit, parentBranch)
+				if _, exists := hashes[parent.String()]; !exists {
+					continue
+				}
+				parentBranch := -1
+				if parents, exists := branchers[commit.Hash]; exists {
+					if inheritedBranch, exists := parents[parent]; exists {
+						parentBranch = inheritedBranch
 					}
 				}
+				if parentBranch == -1 {
+					parentBranch = branches[parent]
+				}
+				if len(dag[parent]) == 1 && minBranch > parentBranch {
+					minBranch = parentBranch
+				}
+				items = append(items, parentBranch)
+				if parentBranch != branch {
+					appendCommit(commit, parentBranch)
+				}
 			}
 			if minBranch < 1 << 31 {
 				branch = minBranch
 				branches[commit.Hash] = minBranch
 			} else if !branchExists() {
-				panic("!branchExists()")
+				log.Panicf("!branchExists(%s)", commit.Hash.String())
 			}
 			plan = append(plan, runAction{
 				Action: runActionMerge,
@@ -444,6 +543,7 @@ func generatePlan(
 				Items: items,
 			})
 		}
+		var head plumbing.Hash
 		if subseq, exists := mergedSeq[commit.Hash]; exists {
 			for subseqIndex, offspring := range subseq {
 				if branchExists() {
@@ -453,22 +553,34 @@ func generatePlan(
 					appendMergeIfNeeded()
 				}
 			}
-			branches[subseq[len(subseq)-1].Hash] = branch
+			head = subseq[len(subseq)-1].Hash
+			branches[head] = branch
+		} else {
+			head = commit.Hash
 		}
 		if len(mergedDag[commit.Hash]) > 1 {
-			branches[mergedDag[commit.Hash][0].Hash] = branch
 			children := []int{branch}
 			for i, child := range mergedDag[commit.Hash] {
-				if i > 0 {
+				if i == 0 {
+					branches[child.Hash] = branch
+					continue
+				}
+				if _, exists := branches[child.Hash]; !exists {
 					branches[child.Hash] = counter
-					children = append(children, counter)
-					counter++
 				}
+				parents := branchers[child.Hash]
+				if parents == nil {
+					parents = map[plumbing.Hash]int{}
+					branchers[child.Hash] = parents
+				}
+				parents[head] = counter
+				children = append(children, counter)
+				counter++
 			}
 			plan = append(plan, runAction{
 				Action: runActionFork,
 				Commit: nil,
-				Items: children,
+				Items:  children,
 			})
 		}
 	}
@@ -577,14 +689,4 @@ func optimizePlan(plan []runAction) []runAction {
 		return optimizedPlan
 	}
 	return plan
-	// TODO(vmarkovtsev): there can be also duplicate redundant merges, e.g.
-	/*
-	0 4e34f03d829fbacb71cde0e010de87ea945dc69a [3]
-	0 4e34f03d829fbacb71cde0e010de87ea945dc69a [12]
-	2                                          [3 12]
-	0 06716c2b39422938b77ddafa4d5c39bb9e4476da [3]
-	0 06716c2b39422938b77ddafa4d5c39bb9e4476da [12]
-	2                                          [3 12]
-	0 1219c7bf9e0e1a93459a052ab8b351bfc379dc19 [12]
-	*/
 }

+ 66 - 0
internal/core/forks_test.go

@@ -0,0 +1,66 @@
+package core
+
+import (
+	"testing"
+	"gopkg.in/src-d/go-git.v4"
+	"github.com/stretchr/testify/assert"
+)
+
+type testForkPipelineItem struct {
+	NoopMerger
+	Mutable map[int]bool
+	Immutable string
+}
+
+func (item *testForkPipelineItem) Name() string {
+	return "Test"
+}
+
+func (item *testForkPipelineItem) Provides() []string {
+	arr := [...]string{"test"}
+	return arr[:]
+}
+
+func (item *testForkPipelineItem) Requires() []string {
+	return []string{}
+}
+
+func (item *testForkPipelineItem) Configure(facts map[string]interface{}) {
+}
+
+func (item *testForkPipelineItem) ListConfigurationOptions() []ConfigurationOption {
+	return nil
+}
+
+func (item *testForkPipelineItem) Flag() string {
+	return "mytest"
+}
+
+func (item *testForkPipelineItem) Features() []string {
+	return nil
+}
+
+func (item *testForkPipelineItem) Initialize(repository *git.Repository) {
+	item.Mutable = map[int]bool{}
+}
+
+func (item *testForkPipelineItem) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
+	return map[string]interface{}{"test": "foo"}, nil
+}
+
+func (item *testForkPipelineItem) Fork(n int) []PipelineItem {
+	return ForkCopyPipelineItem(item, n)
+}
+
+func TestForkCopyPipelineItem(t *testing.T) {
+	origin := &testForkPipelineItem{}
+	origin.Initialize(nil)
+	origin.Mutable[2] = true
+	origin.Immutable = "before"
+	clone := origin.Fork(1)[0].(*testForkPipelineItem)
+	origin.Immutable = "after"
+	origin.Mutable[1] = true
+	assert.True(t, clone.Mutable[1])
+	assert.True(t, clone.Mutable[2])
+	assert.Equal(t, "before", clone.Immutable)
+}

+ 17 - 25
internal/core/pipeline.go

@@ -362,31 +362,18 @@ func (pipeline *Pipeline) Len() int {
 	return len(pipeline.items)
 }
 
-// Commits returns the critical path in the repository's history. It starts
-// from HEAD and traces commits backwards till the root. When it encounters
-// a merge (more than one parent), it always chooses the first parent.
+// Commits returns the list of commits from the history similar to `git log` over the HEAD.
 func (pipeline *Pipeline) Commits() []*object.Commit {
-	result := []*object.Commit{}
-	repository := pipeline.repository
-	head, err := repository.Head()
+	cit, err := pipeline.repository.Log(&git.LogOptions{From: plumbing.ZeroHash})
 	if err != nil {
-		panic(err)
+		log.Fatalf("unable to collect the commit history: %v", err)
 	}
-	commit, err := repository.CommitObject(head.Hash())
-	if err != nil {
-		panic(err)
-	}
-	// the first parent matches the head
-	for ; err != io.EOF; commit, err = commit.Parents().Next() {
-		if err != nil {
-			panic(err)
-		}
+	defer cit.Close()
+	var result []*object.Commit
+	cit.ForEach(func(commit *object.Commit) error {
 		result = append(result, commit)
-	}
-	// reverse the order
-	for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
-		result[i], result[j] = result[j], result[i]
-	}
+		return nil
+	})
 	return result
 }
 
@@ -567,6 +554,7 @@ func (pipeline *Pipeline) Run(commits []*object.Commit) (map[LeafPipelineItem]in
 	plan := prepareRunPlan(commits)
 	progressSteps := len(plan) + 2
 	branches := map[int][]PipelineItem{0: pipeline.items}
+	var newestTime int64
 
 	for index, step := range plan {
 		onProgress(index + 1, progressSteps)
@@ -592,6 +580,10 @@ func (pipeline *Pipeline) Run(commits []*object.Commit) (map[LeafPipelineItem]in
 					state[key] = val
 				}
 			}
+			commitTime := step.Commit.Author.When.Unix()
+			if commitTime > newestTime {
+				newestTime = commitTime
+			}
 		case runActionFork:
 			for i, clone := range cloneItems(branches[firstItem], len(step.Items)-1) {
 				branches[step.Items[i+1]] = clone
@@ -608,15 +600,15 @@ func (pipeline *Pipeline) Run(commits []*object.Commit) (map[LeafPipelineItem]in
 	}
 	onProgress(len(plan) + 1, progressSteps)
 	result := map[LeafPipelineItem]interface{}{}
-	for _, item := range getMasterBranch(branches) {
+	for index, item := range getMasterBranch(branches) {
 		if casted, ok := item.(LeafPipelineItem); ok {
-			result[casted] = casted.Finalize()
+			result[pipeline.items[index].(LeafPipelineItem)] = casted.Finalize()
 		}
 	}
 	onProgress(progressSteps, progressSteps)
 	result[nil] = &CommonAnalysisResult{
-		BeginTime:     commits[0].Author.When.Unix(),
-		EndTime:       commits[len(commits)-1].Author.When.Unix(),
+		BeginTime:     plan[0].Commit.Author.When.Unix(),
+		EndTime:       newestTime,
 		CommitsNumber: len(commits),
 		RunTime:       time.Since(startRunTime),
 	}

+ 146 - 56
internal/core/pipeline_test.go

@@ -285,12 +285,14 @@ func TestPipelineOnProgress(t *testing.T) {
 func TestPipelineCommits(t *testing.T) {
 	pipeline := NewPipeline(test.Repository)
 	commits := pipeline.Commits()
-	assert.True(t, len(commits) >= 90)
-	assert.Equal(t, commits[0].Hash, plumbing.NewHash(
+	assert.True(t, len(commits) >= 100)
+	hashMap := map[plumbing.Hash]bool{}
+	for _, c := range commits {
+		hashMap[c.Hash] = true
+	}
+	assert.Equal(t, len(commits), len(hashMap))
+	assert.Contains(t, hashMap, plumbing.NewHash(
 		"cce947b98a050c6d356bc6ba95030254914027b1"))
-	assert.Equal(t, commits[89].Hash, plumbing.NewHash(
-		"6db8065cdb9bb0758f36a7e75fc72ab95f9e8145"))
-	assert.NotEqual(t, commits[len(commits)-1], commits[len(commits)-2])
 }
 
 func TestLoadCommitsFromFile(t *testing.T) {
@@ -469,6 +471,70 @@ func TestPrepareRunPlanSmall(t *testing.T) {
 	assert.Equal(t, "a28e9064c70618dc9d68e1401b889975e0680d11", plan[9].Commit.Hash.String())
 }
 
+func TestMergeDag(t *testing.T) {
+	cit, err := test.Repository.Log(&git.LogOptions{From: plumbing.ZeroHash})
+	if err != nil {
+		panic(err)
+	}
+	defer cit.Close()
+	var commits []*object.Commit
+	timeCutoff := time.Date(2017, 8, 12, 0, 0, 0, 0, time.FixedZone("CET", 7200))
+	cit.ForEach(func(commit *object.Commit) error {
+		reliableTime := time.Date(commit.Author.When.Year(), commit.Author.When.Month(),
+			commit.Author.When.Day(), commit.Author.When.Hour(), commit.Author.When.Minute(),
+			commit.Author.When.Second(), 0, time.FixedZone("CET", 7200))
+		if reliableTime.Before(timeCutoff) {
+			commits = append(commits, commit)
+		}
+		return nil
+	})
+	hashes, dag := buildDag(commits)
+	leaveRootComponent(hashes, dag)
+	mergedDag, _ := mergeDag(hashes, dag)
+	for key, vals := range mergedDag {
+		if key != plumbing.NewHash("a28e9064c70618dc9d68e1401b889975e0680d11") &&
+			key != plumbing.NewHash("db325a212d0bc99b470e000641d814745024bbd5") {
+			assert.Len(t, vals, len(dag[key]), key.String())
+		} else {
+			mvals := map[string]bool{}
+			for _, val := range vals {
+				mvals[val.Hash.String()] = true
+			}
+			if key == plumbing.NewHash("a28e9064c70618dc9d68e1401b889975e0680d11") {
+				assert.Contains(t, mvals, "db325a212d0bc99b470e000641d814745024bbd5")
+				assert.Contains(t, mvals, "be9b61e09b08b98e64ed461a4004c9e2412f78ee")
+			}
+			if key == plumbing.NewHash("db325a212d0bc99b470e000641d814745024bbd5") {
+				assert.Contains(t, mvals, "f30daba81ff2bf0b3ba02a1e1441e74f8a4f6fee")
+				assert.Contains(t, mvals, "8a03b5620b1caa72ec9cb847ea88332621e2950a")
+			}
+		}
+	}
+	assert.Len(t, mergedDag, 8)
+	assert.Contains(t, mergedDag, plumbing.NewHash("cce947b98a050c6d356bc6ba95030254914027b1"))
+	assert.Contains(t, mergedDag, plumbing.NewHash("a3ee37f91f0d705ec9c41ae88426f0ae44b2fbc3"))
+	assert.Contains(t, mergedDag, plumbing.NewHash("a28e9064c70618dc9d68e1401b889975e0680d11"))
+	assert.Contains(t, mergedDag, plumbing.NewHash("be9b61e09b08b98e64ed461a4004c9e2412f78ee"))
+	assert.Contains(t, mergedDag, plumbing.NewHash("db325a212d0bc99b470e000641d814745024bbd5"))
+	assert.Contains(t, mergedDag, plumbing.NewHash("f30daba81ff2bf0b3ba02a1e1441e74f8a4f6fee"))
+	assert.Contains(t, mergedDag, plumbing.NewHash("8a03b5620b1caa72ec9cb847ea88332621e2950a"))
+	assert.Contains(t, mergedDag, plumbing.NewHash("dd9dd084d5851d7dc4399fc7dbf3d8292831ebc5"))
+	queue := []plumbing.Hash{plumbing.NewHash("cce947b98a050c6d356bc6ba95030254914027b1")}
+	visited := map[plumbing.Hash]bool{}
+	for len(queue) > 0 {
+		head := queue[len(queue)-1]
+		queue = queue[:len(queue)-1]
+		if visited[head] {
+			continue
+		}
+		visited[head] = true
+		for _, child := range mergedDag[head] {
+			queue = append(queue, child.Hash)
+		}
+	}
+	assert.Len(t, visited, 8)
+}
+
 func TestPrepareRunPlanBig(t *testing.T) {
 	cases := [][7]int {
 		{2017, 8, 9, 0, 0, 0, 0},
@@ -480,61 +546,85 @@ func TestPrepareRunPlanBig(t *testing.T) {
 		{2017, 12, 9, 1, 1, 1, 1},
 		{2017, 12, 10, 1, 1, 1, 1},
 		{2017, 12, 11, 2, 2, 2, 2},
-		{2017, 12, 19, 4, 4, 4, 4},
-		{2017, 12, 27, 4, 4, 4, 4},
-		{2018, 1, 10, 4, 4, 4, 4},
-		{2018, 1, 16, 4, 4, 4, 4},
-		{2018, 1, 18, 7, 6, 7, 7},
-		{2018, 1, 23, 8, 6, 8, 8},
-		{2018, 3, 12, 9, 7, 9, 9},
-		{2018, 5, 13, 9, 7, 9, 9},
-		{2018, 5, 16, 13, 9, 13, 13},
+		{2017, 12, 19, 3, 3, 3, 3},
+		{2017, 12, 27, 3, 3, 3, 3},
+		{2018, 1, 10, 3, 3, 3, 3},
+		{2018, 1, 16, 3, 3, 3, 3},
+		{2018, 1, 18, 4, 5, 4, 4},
+		{2018, 1, 23, 5, 5, 5, 5},
+		{2018, 3, 12, 6, 6, 6, 6},
+		{2018, 5, 13, 6, 6, 6, 6},
+		{2018, 5, 16, 7, 7, 7, 7},
 	}
 	for _, testCase := range cases {
-		cit, err := test.Repository.Log(&git.LogOptions{From: plumbing.ZeroHash})
-		if err != nil {
-			panic(err)
-		}
-		defer cit.Close()
-		var commits []*object.Commit
-		timeCutoff := time.Date(
-			testCase[0], time.Month(testCase[1]), testCase[2], 0, 0, 0, 0, time.FixedZone("CET", 7200))
-		cit.ForEach(func(commit *object.Commit) error {
-			reliableTime := time.Date(commit.Author.When.Year(), commit.Author.When.Month(),
-				commit.Author.When.Day(), commit.Author.When.Hour(), commit.Author.When.Minute(),
-				commit.Author.When.Second(), 0, time.FixedZone("CET", 7200))
-			if reliableTime.Before(timeCutoff) {
-				commits = append(commits, commit)
+		func() {
+			cit, err := test.Repository.Log(&git.LogOptions{From: plumbing.ZeroHash})
+			if err != nil {
+				panic(err)
 			}
-			return nil
-		})
-		plan := prepareRunPlan(commits)
-		/*for _, p := range plan {
-			if p.Commit != nil {
-				fmt.Println(p.Action, p.Commit.Hash.String(), p.Items)
-			} else {
-				fmt.Println(p.Action, strings.Repeat(" ", 40), p.Items)
+			defer cit.Close()
+			var commits []*object.Commit
+			timeCutoff := time.Date(
+				testCase[0], time.Month(testCase[1]), testCase[2], 0, 0, 0, 0, time.FixedZone("CET", 7200))
+			cit.ForEach(func(commit *object.Commit) error {
+				reliableTime := time.Date(commit.Author.When.Year(), commit.Author.When.Month(),
+					commit.Author.When.Day(), commit.Author.When.Hour(), commit.Author.When.Minute(),
+					commit.Author.When.Second(), 0, time.FixedZone("CET", 7200))
+				if reliableTime.Before(timeCutoff) {
+					commits = append(commits, commit)
+				}
+				return nil
+			})
+			plan := prepareRunPlan(commits)
+			/*for _, p := range plan {
+				if p.Commit != nil {
+					fmt.Println(p.Action, p.Commit.Hash.String(), p.Items)
+				} else {
+					fmt.Println(p.Action, strings.Repeat(" ", 40), p.Items)
+				}
+			}*/
+			numCommits := 0
+			numForks := 0
+			numMerges := 0
+			numDeletes := 0
+			processed := map[plumbing.Hash]map[int]int{}
+			for _, p := range plan {
+				switch p.Action {
+				case runActionCommit:
+					branches := processed[p.Commit.Hash]
+					if branches == nil {
+						branches = map[int]int{}
+						processed[p.Commit.Hash] = branches
+					}
+					branches[p.Items[0]]++
+					for _, parent := range p.Commit.ParentHashes {
+						assert.Contains(t, processed, parent)
+					}
+					numCommits++
+				case runActionFork:
+					numForks++
+				case runActionMerge:
+					counts := map[int]int{}
+					for _, i := range p.Items {
+						counts[i]++
+					}
+					for x, v := range counts {
+						assert.Equal(t, 1, v, x)
+					}
+					numMerges++
+				case runActionDelete:
+					numDeletes++
+				}
 			}
-		}*/
-		numCommits := 0
-		numForks := 0
-		numMerges := 0
-		numDeletes := 0
-		for _, p := range plan {
-			switch p.Action {
-			case runActionCommit:
-				numCommits++
-			case runActionFork:
-				numForks++
-			case runActionMerge:
-				numMerges++
-			case runActionDelete:
-				numDeletes++
+			for c, branches := range processed {
+				for b, v := range branches {
+					assert.Equal(t, 1, v, fmt.Sprint(c.String(), b))
+				}
 			}
-		}
-		assert.Equal(t, numCommits, len(commits)+testCase[3], fmt.Sprintf("commits %v", testCase))
-		assert.Equal(t, numForks, testCase[4], fmt.Sprintf("forks %v", testCase))
-		assert.Equal(t, numMerges, testCase[5], fmt.Sprintf("merges %v", testCase))
-		assert.Equal(t, numMerges, testCase[6], fmt.Sprintf("deletes %v", testCase))
+			assert.Equal(t, numCommits, len(commits)+testCase[3], fmt.Sprintf("commits %v", testCase))
+			assert.Equal(t, numForks, testCase[4], fmt.Sprintf("forks %v", testCase))
+			assert.Equal(t, numMerges, testCase[5], fmt.Sprintf("merges %v", testCase))
+			assert.Equal(t, numDeletes, testCase[6], fmt.Sprintf("deletes %v", testCase))
+		}()
 	}
 }

+ 1 - 1
internal/plumbing/identity/identity.go

@@ -9,7 +9,7 @@ import (
 	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
 	"gopkg.in/src-d/hercules.v4/internal/core"
-)
+	)
 
 // Detector determines the author of a commit. Same person can commit under different
 // signatures, and we apply some heuristics to merge those together.

+ 14 - 1
internal/plumbing/tree_diff.go

@@ -2,11 +2,13 @@ package plumbing
 
 import (
 	"io"
+	"log"
 	"strings"
 
 	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
 	"gopkg.in/src-d/hercules.v4/internal/core"
+	"gopkg.in/src-d/go-git.v4/plumbing"
 )
 
 // TreeDiff generates the list of changes for a commit. A change can be either one or two blobs
@@ -17,6 +19,7 @@ type TreeDiff struct {
 	core.NoopMerger
 	SkipDirs     []string
 	previousTree *object.Tree
+	previousCommit plumbing.Hash
 }
 
 const (
@@ -89,6 +92,15 @@ func (treediff *TreeDiff) Initialize(repository *git.Repository) {
 // in Provides(). If there was an error, nil is returned.
 func (treediff *TreeDiff) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
 	commit := deps[core.DependencyCommit].(*object.Commit)
+	pass := false
+	for _, hash := range commit.ParentHashes {
+		if hash == treediff.previousCommit {
+			pass = true
+		}
+	}
+	if !pass && treediff.previousCommit != plumbing.ZeroHash {
+		log.Panicf("%s > %s", treediff.previousCommit.String(), commit.Hash.String())
+	}
 	tree, err := commit.Tree()
 	if err != nil {
 		return nil, err
@@ -123,10 +135,11 @@ func (treediff *TreeDiff) Consume(deps map[string]interface{}) (map[string]inter
 		}
 	}
 	treediff.previousTree = tree
+	treediff.previousCommit = commit.Hash
 
 	if len(treediff.SkipDirs) > 0 {
 		// filter without allocation
-		filteredDiff := diff[:0]
+		filteredDiff := make([]*object.Change, 0, len(diff))
 	OUTER:
 		for _, change := range diff {
 			for _, dir := range treediff.SkipDirs {

+ 189 - 182
leaves/burndown.go

@@ -21,7 +21,7 @@ import (
 	items "gopkg.in/src-d/hercules.v4/internal/plumbing"
 	"gopkg.in/src-d/hercules.v4/internal/plumbing/identity"
 	"gopkg.in/src-d/hercules.v4/yaml"
-)
+	)
 
 // BurndownAnalysis allows to gather the line burndown statistics for a Git repository.
 // It is a LeafPipelineItem.
@@ -49,21 +49,27 @@ type BurndownAnalysis struct {
 
 	// Repository points to the analysed Git repository struct from go-git.
 	repository *git.Repository
-	// globalStatus is the current daily alive number of lines; key is the number
-	// of days from the beginning of the history.
-	globalStatus map[int]int64
-	// globalHistory is the periodic snapshots of globalStatus.
-	globalHistory [][]int64
-	// fileHistories is the periodic snapshots of each file's status.
-	fileHistories map[string][][]int64
-	// peopleHistories is the periodic snapshots of each person's status.
-	peopleHistories [][][]int64
+	// globalHistory is the daily deltas of daily line counts.
+	// E.g. day 0: day 0 +50 lines
+	//      day 10: day 0 -10 lines; day 10 +20 lines
+	//      day 12: day 0 -5 lines; day 10 -3 lines; day 12 +10 lines
+	// map [0] [0] = 50
+	// map[10] [0] = -10
+	// map[10][10] = 20
+	// map[12] [0] = -5
+	// map[12][10] = -3
+	// map[12][12] = 10
+	globalHistory sparseHistory
+	// fileHistories is the daily deltas of each file's daily line counts.
+	fileHistories map[string]sparseHistory
+	// peopleHistories is the daily deltas of each person's daily line counts.
+	peopleHistories []sparseHistory
 	// files is the mapping <file path> -> *File.
 	files map[string]*burndown.File
+	// renames is a quick and dirty solution for the "future branch renames" problem.
+	renames map[string]string
 	// matrix is the mutual deletions and self insertions.
 	matrix []map[int]int64
-	// people is the people's individual time stats.
-	people []map[int]int64
 	// day is the most recent day index processed.
 	day int
 	// previousDay is the day from the previous sample period -
@@ -79,18 +85,18 @@ type BurndownResult struct {
 	// [number of samples][number of bands]
 	// The number of samples depends on Sampling: the less Sampling, the bigger the number.
 	// The number of bands depends on Granularity: the less Granularity, the bigger the number.
-	GlobalHistory [][]int64
+	GlobalHistory DenseHistory
 	// The key is the path inside the Git repository. The value's dimensions are the same as
 	// in GlobalHistory.
-	FileHistories map[string][][]int64
+	FileHistories map[string]DenseHistory
 	// [number of people][number of samples][number of bands]
-	PeopleHistories [][][]int64
+	PeopleHistories []DenseHistory
 	// [number of people][number of people + 2]
 	// The first element is the total number of lines added by the author.
 	// The second element is the number of removals by unidentified authors (outside reversedPeopleDict).
 	// The rest of the elements are equal the number of line removals by the corresponding
 	// authors in reversedPeopleDict: 2 -> 0, 3 -> 1, etc.
-	PeopleMatrix [][]int64
+	PeopleMatrix DenseHistory
 
 	// The following members are private.
 
@@ -123,6 +129,11 @@ const (
 	authorSelf = (1 << (32 - burndown.TreeMaxBinPower)) - 2
 )
 
+type sparseHistory = map[int]map[int]int64
+
+// DenseHistory is the matrix [number of samples][number of bands] -> number of lines.
+type DenseHistory = [][]int64
+
 // Name of this PipelineItem. Uniquely identifies the type, used for mapping keys, etc.
 func (analyser *BurndownAnalysis) Name() string {
 	return "Burndown"
@@ -225,27 +236,25 @@ func (analyser *BurndownAnalysis) Initialize(repository *git.Repository) {
 		analyser.Sampling = analyser.Granularity
 	}
 	analyser.repository = repository
-	analyser.globalStatus = map[int]int64{}
-	analyser.globalHistory = [][]int64{}
-	analyser.fileHistories = map[string][][]int64{}
-	analyser.peopleHistories = make([][][]int64, analyser.PeopleNumber)
+	analyser.globalHistory = sparseHistory{}
+	analyser.fileHistories = map[string]sparseHistory{}
+	analyser.peopleHistories = make([]sparseHistory, analyser.PeopleNumber)
 	analyser.files = map[string]*burndown.File{}
+	analyser.renames = map[string]string{}
 	analyser.matrix = make([]map[int]int64, analyser.PeopleNumber)
-	analyser.people = make([]map[int]int64, analyser.PeopleNumber)
 	analyser.day = 0
 	analyser.previousDay = 0
 }
 
-// Consume runs this PipelineItem on the next commit data.
+// Consume runs this PipelineItem on the next commit's data.
 // `deps` contain all the results from upstream PipelineItem-s as requested by Requires().
 // Additionally, DependencyCommit is always present there and represents the analysed *object.Commit.
 // This function returns the mapping with analysis results. The keys must be the same as
 // in Provides(). If there was an error, nil is returned.
 func (analyser *BurndownAnalysis) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
-	commit := deps[core.DependencyCommit].(*object.Commit)
 	author := deps[identity.DependencyAuthor].(int)
 	day := deps[items.DependencyDay].(int)
-	if len(commit.ParentHashes) <= 1 {
+	if !core.IsMergeCommit(deps) {
 		analyser.day = day
 		analyser.onNewDay()
 	} else {
@@ -310,19 +319,24 @@ func (analyser *BurndownAnalysis) Merge(branches []core.PipelineItem) {
 
 // Finalize returns the result of the analysis. Further Consume() calls are not expected.
 func (analyser *BurndownAnalysis) Finalize() interface{} {
-	gs, fss, pss := analyser.groupStatus()
-	analyser.updateHistories(1, gs, fss, pss)
-	for key, statuses := range analyser.fileHistories {
-		if len(statuses) == len(analyser.globalHistory) {
-			continue
-		}
-		padding := make([][]int64, len(analyser.globalHistory)-len(statuses))
-		for i := range padding {
-			padding[i] = make([]int64, len(analyser.globalStatus))
+	globalHistory, lastDay := analyser.groupSparseHistory(analyser.globalHistory, -1)
+	fileHistories := map[string]DenseHistory{}
+	for key, history := range analyser.fileHistories {
+		fileHistories[key], _ = analyser.groupSparseHistory(history, lastDay)
+	}
+	peopleHistories := make([]DenseHistory, analyser.PeopleNumber)
+	for i, history := range analyser.peopleHistories {
+		if len(history) > 0 {
+			// there can be people with only trivial merge commits and without own lines
+			peopleHistories[i], _ = analyser.groupSparseHistory(history, lastDay)
+		} else {
+			peopleHistories[i] = make(DenseHistory, len(globalHistory))
+			for j, gh := range globalHistory {
+				peopleHistories[i][j] = make([]int64, len(gh))
+			}
 		}
-		analyser.fileHistories[key] = append(padding, statuses...)
 	}
-	peopleMatrix := make([][]int64, analyser.PeopleNumber)
+	peopleMatrix := make(DenseHistory, analyser.PeopleNumber)
 	for i, row := range analyser.matrix {
 		mrow := make([]int64, analyser.PeopleNumber+2)
 		peopleMatrix[i] = mrow
@@ -336,9 +350,9 @@ func (analyser *BurndownAnalysis) Finalize() interface{} {
 		}
 	}
 	return BurndownResult{
-		GlobalHistory:      analyser.globalHistory,
-		FileHistories:      analyser.fileHistories,
-		PeopleHistories:    analyser.peopleHistories,
+		GlobalHistory:      globalHistory,
+		FileHistories:      fileHistories,
+		PeopleHistories:    peopleHistories,
 		PeopleMatrix:       peopleMatrix,
 		reversedPeopleDict: analyser.reversedPeopleDict,
 		sampling:           analyser.Sampling,
@@ -365,8 +379,8 @@ func (analyser *BurndownAnalysis) Deserialize(pbmessage []byte) (interface{}, er
 		return nil, err
 	}
 	result := BurndownResult{}
-	convertCSR := func(mat *pb.BurndownSparseMatrix) [][]int64 {
-		res := make([][]int64, mat.NumberOfRows)
+	convertCSR := func(mat *pb.BurndownSparseMatrix) DenseHistory {
+		res := make(DenseHistory, mat.NumberOfRows)
 		for i := 0; i < int(mat.NumberOfRows); i++ {
 			res[i] = make([]int64, mat.NumberOfColumns)
 			for j := 0; j < len(mat.Rows[i].Columns); j++ {
@@ -376,18 +390,18 @@ func (analyser *BurndownAnalysis) Deserialize(pbmessage []byte) (interface{}, er
 		return res
 	}
 	result.GlobalHistory = convertCSR(msg.Project)
-	result.FileHistories = map[string][][]int64{}
+	result.FileHistories = map[string]DenseHistory{}
 	for _, mat := range msg.Files {
 		result.FileHistories[mat.Name] = convertCSR(mat)
 	}
 	result.reversedPeopleDict = make([]string, len(msg.People))
-	result.PeopleHistories = make([][][]int64, len(msg.People))
+	result.PeopleHistories = make([]DenseHistory, len(msg.People))
 	for i, mat := range msg.People {
 		result.PeopleHistories[i] = convertCSR(mat)
 		result.reversedPeopleDict[i] = mat.Name
 	}
 	if msg.PeopleInteraction != nil {
-		result.PeopleMatrix = make([][]int64, msg.PeopleInteraction.NumberOfRows)
+		result.PeopleMatrix = make(DenseHistory, msg.PeopleInteraction.NumberOfRows)
 	}
 	for i := 0; i < len(result.PeopleMatrix); i++ {
 		result.PeopleMatrix[i] = make([]int64, msg.PeopleInteraction.NumberOfColumns)
@@ -432,12 +446,12 @@ func (analyser *BurndownAnalysis) MergeResults(
 		}()
 	}
 	if len(bar1.FileHistories) > 0 || len(bar2.FileHistories) > 0 {
-		merged.FileHistories = map[string][][]int64{}
+		merged.FileHistories = map[string]DenseHistory{}
 		historyMutex := sync.Mutex{}
 		for key, fh1 := range bar1.FileHistories {
 			if fh2, exists := bar2.FileHistories[key]; exists {
 				wg.Add(1)
-				go func(fh1, fh2 [][]int64, key string) {
+				go func(fh1, fh2 DenseHistory, key string) {
 					defer wg.Done()
 					historyMutex.Lock()
 					defer historyMutex.Unlock()
@@ -459,7 +473,7 @@ func (analyser *BurndownAnalysis) MergeResults(
 		}
 	}
 	if len(merged.reversedPeopleDict) > 0 {
-		merged.PeopleHistories = make([][][]int64, len(merged.reversedPeopleDict))
+		merged.PeopleHistories = make([]DenseHistory, len(merged.reversedPeopleDict))
 		for i, key := range merged.reversedPeopleDict {
 			ptrs := people[key]
 			if ptrs[1] < 0 {
@@ -474,7 +488,7 @@ func (analyser *BurndownAnalysis) MergeResults(
 				wg.Add(1)
 				go func(i int) {
 					defer wg.Done()
-					var m1, m2 [][]int64
+					var m1, m2 DenseHistory
 					if len(bar1.PeopleHistories) > 0 {
 						m1 = bar1.PeopleHistories[ptrs[1]]
 					}
@@ -506,7 +520,7 @@ func (analyser *BurndownAnalysis) MergeResults(
 						merged.PeopleMatrix, make([]int64, len(merged.reversedPeopleDict)+2))
 				}
 			} else {
-				merged.PeopleMatrix = make([][]int64, len(merged.reversedPeopleDict))
+				merged.PeopleMatrix = make(DenseHistory, len(merged.reversedPeopleDict))
 				for i := range merged.PeopleMatrix {
 					merged.PeopleMatrix[i] = make([]int64, len(merged.reversedPeopleDict)+2)
 				}
@@ -535,8 +549,8 @@ func (analyser *BurndownAnalysis) MergeResults(
 // mergeMatrices takes two [number of samples][number of bands] matrices,
 // resamples them to days so that they become square, sums and resamples back to the
 // least of (sampling1, sampling2) and (granularity1, granularity2).
-func mergeMatrices(m1, m2 [][]int64, granularity1, sampling1, granularity2, sampling2 int,
-	c1, c2 *core.CommonAnalysisResult) [][]int64 {
+func mergeMatrices(m1, m2 DenseHistory, granularity1, sampling1, granularity2, sampling2 int,
+	c1, c2 *core.CommonAnalysisResult) DenseHistory {
 	commonMerged := *c1
 	commonMerged.Merge(c2)
 
@@ -566,8 +580,8 @@ func mergeMatrices(m1, m2 [][]int64, granularity1, sampling1, granularity2, samp
 			int(c2.BeginTime-commonMerged.BeginTime)/(3600*24))
 	}
 
-	// convert daily to [][]in(t64
-	result := make([][]int64, (size+sampling-1)/sampling)
+	// convert daily to [][]int64
+	result := make(DenseHistory, (size+sampling-1)/sampling)
 	for i := range result {
 		result[i] = make([]int64, (size+granularity-1)/granularity)
 		sampledIndex := i * sampling
@@ -591,7 +605,7 @@ func mergeMatrices(m1, m2 [][]int64, granularity1, sampling1, granularity2, samp
 // Rows: *at least* len(matrix) * sampling + offset
 // Columns: *at least* len(matrix[...]) * granularity + offset
 // `matrix` can be sparse, so that the last columns which are equal to 0 are truncated.
-func addBurndownMatrix(matrix [][]int64, granularity, sampling int, daily [][]float32, offset int) {
+func addBurndownMatrix(matrix DenseHistory, granularity, sampling int, daily [][]float32, offset int) {
 	// Determine the maximum number of bands; the actual one may be larger but we do not care
 	maxCols := 0
 	for _, row := range matrix {
@@ -809,7 +823,7 @@ func (analyser *BurndownAnalysis) serializeBinary(result *BurndownResult, writer
 	return nil
 }
 
-func sortedKeys(m map[string][][]int64) []string {
+func sortedKeys(m map[string]DenseHistory) []string {
 	keys := make([]string, 0, len(m))
 	for k := range m {
 		keys = append(keys, k)
@@ -847,54 +861,70 @@ func (analyser *BurndownAnalysis) unpackPersonWithDay(value int) (int, int) {
 }
 
 func (analyser *BurndownAnalysis) onNewDay() {
-	day := analyser.day
-	sampling := analyser.Sampling
-	delta := (day / sampling) - (analyser.previousDay / sampling)
-	if delta > 0 {
-		analyser.previousDay = day
-		gs, fss, pss := analyser.groupStatus()
-		analyser.updateHistories(delta, gs, fss, pss)
+	if analyser.day > analyser.previousDay {
+		analyser.previousDay = analyser.day
 	}
 }
 
-func (analyser *BurndownAnalysis) updateStatus(
-	status interface{}, _ int, previousValue int, delta int) {
+func (analyser *BurndownAnalysis) updateGlobal(currentTime, previousTime, delta int) {
+	_, currentDay := analyser.unpackPersonWithDay(currentTime)
+	_, previousDay := analyser.unpackPersonWithDay(previousTime)
+	currentHistory := analyser.globalHistory[currentDay]
+	if currentHistory == nil {
+		currentHistory = map[int]int64{}
+		analyser.globalHistory[currentDay] = currentHistory
+	}
+	currentHistory[previousDay] += int64(delta)
+}
+
+// updateFile is bound to the specific `history` in the closure.
+func (analyser *BurndownAnalysis) updateFile(
+	history sparseHistory, currentTime, previousTime, delta int) {
+		
+	_, currentDay := analyser.unpackPersonWithDay(currentTime)
+	_, previousDay := analyser.unpackPersonWithDay(previousTime)
 
-	_, previousTime := analyser.unpackPersonWithDay(previousValue)
-	status.(map[int]int64)[previousTime] += int64(delta)
+	currentHistory := history[currentDay]
+	if currentHistory == nil {
+		currentHistory = map[int]int64{}
+		history[currentDay] = currentHistory
+	}
+	currentHistory[previousDay] += int64(delta)
 }
 
-func (analyser *BurndownAnalysis) updatePeople(
-	peopleUncasted interface{}, _ int, previousValue int, delta int) {
-	previousAuthor, previousTime := analyser.unpackPersonWithDay(previousValue)
+func (analyser *BurndownAnalysis) updateAuthor(currentTime, previousTime, delta int) {
+	previousAuthor, previousDay := analyser.unpackPersonWithDay(previousTime)
 	if previousAuthor == identity.AuthorMissing {
 		return
 	}
-	people := peopleUncasted.([]map[int]int64)
-	stats := people[previousAuthor]
-	if stats == nil {
-		stats = map[int]int64{}
-		people[previousAuthor] = stats
+	_, currentDay := analyser.unpackPersonWithDay(currentTime)
+	history := analyser.peopleHistories[previousAuthor]
+	if history == nil {
+		history = sparseHistory{}
+		analyser.peopleHistories[previousAuthor] = history
+	}
+	currentHistory := history[currentDay]
+	if currentHistory == nil {
+		currentHistory = map[int]int64{}
+		history[currentDay] = currentHistory
 	}
-	stats[previousTime] += int64(delta)
+	currentHistory[previousDay] += int64(delta)
 }
 
-func (analyser *BurndownAnalysis) updateMatrix(
-	matrixUncasted interface{}, currentTime int, previousTime int, delta int) {
-
-	matrix := matrixUncasted.([]map[int]int64)
+func (analyser *BurndownAnalysis) updateMatrix(currentTime, previousTime, delta int) {
 	newAuthor, _ := analyser.unpackPersonWithDay(currentTime)
 	oldAuthor, _ := analyser.unpackPersonWithDay(previousTime)
+
 	if oldAuthor == identity.AuthorMissing {
 		return
 	}
 	if newAuthor == oldAuthor && delta > 0 {
 		newAuthor = authorSelf
 	}
-	row := matrix[oldAuthor]
+	row := analyser.matrix[oldAuthor]
 	if row == nil {
 		row = map[int]int64{}
-		matrix[oldAuthor] = row
+		analyser.matrix[oldAuthor] = row
 	}
 	cell, exists := row[newAuthor]
 	if !exists {
@@ -905,19 +935,26 @@ func (analyser *BurndownAnalysis) updateMatrix(
 }
 
 func (analyser *BurndownAnalysis) newFile(
-	hash plumbing.Hash, author int, day int, size int, global map[int]int64,
-	people []map[int]int64, matrix []map[int]int64) *burndown.File {
-	statuses := make([]burndown.Status, 1)
-	statuses[0] = burndown.NewStatus(global, analyser.updateStatus)
+	hash plumbing.Hash, name string, author int, day int, size int) (*burndown.File, error) {
+	updaters := make([]burndown.Updater, 1)
+	updaters[0] = analyser.updateGlobal
 	if analyser.TrackFiles {
-		statuses = append(statuses, burndown.NewStatus(map[int]int64{}, analyser.updateStatus))
+		history := analyser.fileHistories[name]
+		if history == nil {
+			// can be not nil if the file was created in a future branch
+			history = sparseHistory{}
+		}
+		analyser.fileHistories[name] = history
+		updaters = append(updaters, func(currentTime, previousTime, delta int) {
+			analyser.updateFile(history, currentTime, previousTime, delta)
+		})
 	}
 	if analyser.PeopleNumber > 0 {
-		statuses = append(statuses, burndown.NewStatus(people, analyser.updatePeople))
-		statuses = append(statuses, burndown.NewStatus(matrix, analyser.updateMatrix))
+		updaters = append(updaters, analyser.updateAuthor)
+		updaters = append(updaters, analyser.updateMatrix)
 		day = analyser.packPersonWithDay(author, day)
 	}
-	return burndown.NewFile(hash, day, size, statuses...)
+	return burndown.NewFile(hash, day, size, updaters...), nil
 }
 
 func (analyser *BurndownAnalysis) handleInsertion(
@@ -935,11 +972,9 @@ func (analyser *BurndownAnalysis) handleInsertion(
 	if exists {
 		return fmt.Errorf("file %s already exists", name)
 	}
-	file = analyser.newFile(
-		blob.Hash, author, analyser.day, lines,
-		analyser.globalStatus, analyser.people, analyser.matrix)
+	file, err = analyser.newFile(blob.Hash, name, author, analyser.day, lines)
 	analyser.files[name] = file
-	return nil
+	return err
 }
 
 func (analyser *BurndownAnalysis) handleDeletion(
@@ -958,6 +993,8 @@ func (analyser *BurndownAnalysis) handleDeletion(
 	file.Update(analyser.packPersonWithDay(author, analyser.day), 0, 0, lines)
 	file.Hash = plumbing.ZeroHash
 	delete(analyser.files, name)
+	delete(analyser.fileHistories, name)
+	analyser.renames[name] = ""
 	return nil
 }
 
@@ -1067,113 +1104,83 @@ func (analyser *BurndownAnalysis) handleModification(
 }
 
 func (analyser *BurndownAnalysis) handleRename(from, to string) error {
+	if from == to {
+		return nil
+	}
 	file, exists := analyser.files[from]
 	if !exists {
-		return fmt.Errorf("file %s does not exist", from)
+		return fmt.Errorf("file %s > %s does not exist (files)", from, to)
 	}
 	analyser.files[to] = file
 	delete(analyser.files, from)
-	return nil
-}
 
-func (analyser *BurndownAnalysis) groupStatus() ([]int64, map[string][]int64, [][]int64) {
-	granularity := analyser.Granularity
-	if granularity == 0 {
-		granularity = 1
-	}
-	day := analyser.day
-	day++
-	adjust := 0
-	if day%granularity != 0 {
-		adjust = 1
-	}
-	global := make([]int64, day/granularity+adjust)
-	var group int64
-	for i := 0; i < day; i++ {
-		group += analyser.globalStatus[i]
-		if (i % granularity) == (granularity - 1) {
-			global[i/granularity] = group
-			group = 0
-		}
-	}
-	if day%granularity != 0 {
-		global[len(global)-1] = group
-	}
-	locals := make(map[string][]int64)
 	if analyser.TrackFiles {
-		for key, file := range analyser.files {
-			status := make([]int64, day/granularity+adjust)
-			var group int64
-			for i := 0; i < day; i++ {
-				group += file.Status(1).(map[int]int64)[i]
-				if (i % granularity) == (granularity - 1) {
-					status[i/granularity] = group
-					group = 0
+		history := analyser.fileHistories[from]
+		if history == nil {
+			// a future branch could have already renamed it and we are retarded
+			futureRename, exists := analyser.renames[from]
+			if futureRename == "" && exists {
+				// the file will be deleted in the future, whatever
+				history = sparseHistory{}
+			} else {
+				history = analyser.fileHistories[futureRename]
+				if history == nil {
+					return fmt.Errorf("file %s > %s does not exist (histories)", from, to)
 				}
 			}
-			if day%granularity != 0 {
-				status[len(status)-1] = group
-			}
-			locals[key] = status
-		}
-	}
-	peoples := make([][]int64, len(analyser.people))
-	for key, person := range analyser.people {
-		status := make([]int64, day/granularity+adjust)
-		var group int64
-		for i := 0; i < day; i++ {
-			group += person[i]
-			if (i % granularity) == (granularity - 1) {
-				status[i/granularity] = group
-				group = 0
-			}
-		}
-		if day%granularity != 0 {
-			status[len(status)-1] = group
 		}
-		peoples[key] = status
+		analyser.fileHistories[to] = history
+		delete(analyser.fileHistories, from)
 	}
-	return global, locals, peoples
+	analyser.renames[from] = to
+	return nil
 }
 
-func (analyser *BurndownAnalysis) updateHistories(
-	delta int, globalStatus []int64, fileStatuses map[string][]int64, peopleStatuses [][]int64) {
-	for i := 0; i < delta; i++ {
-		analyser.globalHistory = append(analyser.globalHistory, globalStatus)
-	}
-	toDelete := make([]string, 0)
-	for key, fh := range analyser.fileHistories {
-		ls, exists := fileStatuses[key]
-		if !exists {
-			toDelete = append(toDelete, key)
-		} else {
-			for i := 0; i < delta; i++ {
-				fh = append(fh, ls)
-			}
-			analyser.fileHistories[key] = fh
-		}
-	}
-	for _, key := range toDelete {
-		delete(analyser.fileHistories, key)
-	}
-	for key, ls := range fileStatuses {
-		fh, exists := analyser.fileHistories[key]
-		if exists {
-			continue
+func (analyser *BurndownAnalysis) groupSparseHistory(
+	history sparseHistory, lastDay int) (DenseHistory, int) {
+
+	if len(history) == 0 {
+		panic("empty history")
+	}
+	var days []int
+	for day := range history {
+		days = append(days, day)
+	}
+	sort.Ints(days)
+	if lastDay >= 0 {
+		if days[len(days)-1] < lastDay {
+			days = append(days, lastDay)
+		} else if days[len(days)-1] > lastDay {
+			panic("days corruption")
 		}
-		for i := 0; i < delta; i++ {
-			fh = append(fh, ls)
+	} else {
+		lastDay = days[len(days)-1]
+	}
+	// [y][x]
+	// y - sampling
+	// x - granularity
+	samples := lastDay / analyser.Sampling + 1
+	bands := lastDay / analyser.Granularity + 1
+	result := make(DenseHistory, samples)
+	for i := 0; i < bands; i++ {
+		result[i] = make([]int64, bands)
+	}
+	prevsi := 0
+	for _, day := range days {
+		si := day / analyser.Sampling
+		if si > prevsi {
+			state := result[prevsi]
+			for i := prevsi + 1; i <= si; i++ {
+				copy(result[i], state)
+			}
+			prevsi = si
 		}
-		analyser.fileHistories[key] = fh
-	}
-
-	for key, ph := range analyser.peopleHistories {
-		ls := peopleStatuses[key]
-		for i := 0; i < delta; i++ {
-			ph = append(ph, ls)
+		sample := result[si]
+		for bday, value := range history[day] {
+			sample[bday / analyser.Granularity] += value
 		}
-		analyser.peopleHistories[key] = ph
 	}
+	return result, lastDay
 }
 
 func init() {

+ 35 - 37
leaves/burndown_test.go

@@ -18,12 +18,12 @@ import (
 	items "gopkg.in/src-d/hercules.v4/internal/plumbing"
 	"gopkg.in/src-d/hercules.v4/internal/plumbing/identity"
 	"gopkg.in/src-d/hercules.v4/internal/test"
-)
+	)
 
 func TestBurndownMeta(t *testing.T) {
 	burndown := BurndownAnalysis{}
 	assert.Equal(t, burndown.Name(), "Burndown")
-	assert.Equal(t, len(burndown.Provides()), 0)
+	assert.Len(t, burndown.Provides(), 0)
 	required := [...]string{
 		items.DependencyFileDiff, items.DependencyTreeChanges, items.DependencyBlobCache,
 		items.DependencyDay, identity.DependencyAuthor}
@@ -184,16 +184,15 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 	assert.Nil(t, result)
 	assert.Nil(t, err)
 	assert.Equal(t, burndown.previousDay, 0)
-	assert.Equal(t, len(burndown.files), 3)
+	assert.Len(t, burndown.files, 3)
 	assert.Equal(t, burndown.files["cmd/hercules/main.go"].Len(), 207)
 	assert.Equal(t, burndown.files["analyser.go"].Len(), 926)
 	assert.Equal(t, burndown.files[".travis.yml"].Len(), 12)
-	assert.Equal(t, len(burndown.people), 2)
-	assert.Equal(t, burndown.people[0][0], int64(12+207+926))
-	assert.Equal(t, len(burndown.globalStatus), 1)
-	assert.Equal(t, burndown.globalStatus[0], int64(12+207+926))
-	assert.Equal(t, len(burndown.globalHistory), 0)
-	assert.Equal(t, len(burndown.fileHistories), 0)
+	assert.Len(t, burndown.peopleHistories, 2)
+	assert.Equal(t, burndown.peopleHistories[0][0][0], int64(12+207+926))
+	assert.Len(t, burndown.globalHistory, 1)
+	assert.Equal(t, burndown.globalHistory[0][0], int64(12+207+926))
+	assert.Len(t, burndown.fileHistories, 3)
 	burndown2 := BurndownAnalysis{
 		Granularity: 30,
 		Sampling:    0,
@@ -201,9 +200,8 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 	burndown2.Initialize(test.Repository)
 	_, err = burndown2.Consume(deps)
 	assert.Nil(t, err)
-	assert.Equal(t, len(burndown2.people), 0)
-	assert.Equal(t, len(burndown2.peopleHistories), 0)
-	assert.Equal(t, len(burndown2.fileHistories), 0)
+	assert.Len(t, burndown2.peopleHistories, 0)
+	assert.Len(t, burndown2.fileHistories, 0)
 
 	// stage 2
 	// 2b1ed978194a94edeabbca6de7ff3b5771d4d665
@@ -281,16 +279,15 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 	assert.Nil(t, result)
 	assert.Nil(t, err)
 	assert.Equal(t, burndown.previousDay, 30)
-	assert.Equal(t, len(burndown.files), 2)
+	assert.Len(t, burndown.files, 2)
 	assert.Equal(t, burndown.files["cmd/hercules/main.go"].Len(), 290)
 	assert.Equal(t, burndown.files["burndown.go"].Len(), 543)
-	assert.Equal(t, len(burndown.people), 2)
-	assert.Equal(t, len(burndown.globalStatus), 2)
-	assert.Equal(t, burndown.globalStatus[0], int64(464))
-	assert.Equal(t, burndown.globalStatus[1], int64(0))
-	assert.Equal(t, len(burndown.globalHistory), 1)
-	assert.Equal(t, len(burndown.globalHistory[0]), 2)
-	assert.Equal(t, len(burndown.fileHistories), 3)
+	assert.Len(t, burndown.peopleHistories, 2)
+	assert.Len(t, burndown.globalHistory, 2)
+	assert.Equal(t, burndown.globalHistory[0][0], int64(1145))
+	assert.Equal(t, burndown.globalHistory[30][0], int64(-681))
+	assert.Equal(t, burndown.globalHistory[30][30], int64(369))
+	assert.Len(t, burndown.fileHistories, 2)
 	out := burndown.Finalize().(BurndownResult)
 	/*
 		GlobalHistory   [][]int64
@@ -298,23 +295,23 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 		PeopleHistories [][][]int64
 		PeopleMatrix    [][]int64
 	*/
-	assert.Equal(t, len(out.GlobalHistory), 2)
+	assert.Len(t, out.GlobalHistory, 2)
 	for i := 0; i < 2; i++ {
-		assert.Equal(t, len(out.GlobalHistory[i]), 2)
+		assert.Len(t, out.GlobalHistory[i], 2)
 	}
-	assert.Equal(t, len(out.GlobalHistory), 2)
+	assert.Len(t, out.GlobalHistory, 2)
 	assert.Equal(t, out.GlobalHistory[0][0], int64(1145))
 	assert.Equal(t, out.GlobalHistory[0][1], int64(0))
 	assert.Equal(t, out.GlobalHistory[1][0], int64(464))
 	assert.Equal(t, out.GlobalHistory[1][1], int64(369))
-	assert.Equal(t, len(out.FileHistories), 2)
-	assert.Equal(t, len(out.FileHistories["cmd/hercules/main.go"]), 2)
-	assert.Equal(t, len(out.FileHistories["burndown.go"]), 2)
-	assert.Equal(t, len(out.FileHistories["cmd/hercules/main.go"][0]), 2)
-	assert.Equal(t, len(out.FileHistories["burndown.go"][0]), 2)
-	assert.Equal(t, len(out.PeopleMatrix), 2)
-	assert.Equal(t, len(out.PeopleMatrix[0]), 4)
-	assert.Equal(t, len(out.PeopleMatrix[1]), 4)
+	assert.Len(t, out.FileHistories, 2)
+	assert.Len(t, out.FileHistories["cmd/hercules/main.go"], 2)
+	assert.Len(t, out.FileHistories["burndown.go"], 2)
+	assert.Len(t, out.FileHistories["cmd/hercules/main.go"][0], 2)
+	assert.Len(t, out.FileHistories["burndown.go"][0], 2)
+	assert.Len(t, out.PeopleMatrix, 2)
+	assert.Len(t, out.PeopleMatrix[0], 4)
+	assert.Len(t, out.PeopleMatrix[1], 4)
 	assert.Equal(t, out.PeopleMatrix[0][0], int64(1145))
 	assert.Equal(t, out.PeopleMatrix[0][1], int64(0))
 	assert.Equal(t, out.PeopleMatrix[0][2], int64(0))
@@ -323,11 +320,11 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 	assert.Equal(t, out.PeopleMatrix[1][1], int64(0))
 	assert.Equal(t, out.PeopleMatrix[1][2], int64(0))
 	assert.Equal(t, out.PeopleMatrix[1][3], int64(0))
-	assert.Equal(t, len(out.PeopleHistories), 2)
+	assert.Len(t, out.PeopleHistories, 2)
 	for i := 0; i < 2; i++ {
-		assert.Equal(t, len(out.PeopleHistories[i]), 2)
-		assert.Equal(t, len(out.PeopleHistories[i][0]), 2)
-		assert.Equal(t, len(out.PeopleHistories[i][1]), 2)
+		assert.Len(t, out.PeopleHistories[i], 2)
+		assert.Len(t, out.PeopleHistories[i][0], 2)
+		assert.Len(t, out.PeopleHistories[i][1], 2)
 	}
 }
 
@@ -488,7 +485,7 @@ func TestBurndownSerialize(t *testing.T) {
      464  369
   files:
     "burndown.go": |-
-      0     0
+      926   0
       293 250
     "cmd/hercules/main.go": |-
       207   0
@@ -526,7 +523,8 @@ func TestBurndownSerialize(t *testing.T) {
 	assert.Equal(t, msg.Files[0].Name, "burndown.go")
 	assert.Equal(t, msg.Files[1].Name, "cmd/hercules/main.go")
 	assert.Len(t, msg.Files[0].Rows, 2)
-	assert.Len(t, msg.Files[0].Rows[0].Columns, 0)
+	assert.Len(t, msg.Files[0].Rows[0].Columns, 1)
+	assert.Equal(t, msg.Files[0].Rows[0].Columns[0], uint32(926))
 	assert.Len(t, msg.Files[0].Rows[1].Columns, 2)
 	assert.Equal(t, msg.Files[0].Rows[1].Columns[0], uint32(293))
 	assert.Equal(t, msg.Files[0].Rows[1].Columns[1], uint32(250))

+ 2 - 2
leaves/comment_sentiment.go

@@ -309,7 +309,7 @@ func (sent *CommentSentimentAnalysis) mergeComments(nodes []*uast.Node) []string
 		lineNums = append(lineNums, line)
 	}
 	sort.Ints(lineNums)
-	buffer := []string{}
+	var buffer []string
 	for i, line := range lineNums {
 		lineNodes := lines[line]
 		maxEnd := line
@@ -326,7 +326,7 @@ func (sent *CommentSentimentAnalysis) mergeComments(nodes []*uast.Node) []string
 			continue
 		}
 		mergedComments = append(mergedComments, strings.Join(buffer, "\n"))
-		buffer = buffer[:0]
+		buffer = make([]string, 0, len(buffer))
 	}
 	// We remove unneeded chars and filter too short comments
 	filteredComments := make([]string, 0, len(mergedComments))

+ 155 - 46
leaves/couples.go

@@ -3,7 +3,7 @@ package leaves
 import (
 	"fmt"
 	"io"
-	"sort"
+		"sort"
 
 	"github.com/gogo/protobuf/proto"
 	"gopkg.in/src-d/go-git.v4"
@@ -31,6 +31,10 @@ type CouplesAnalysis struct {
 	peopleCommits []int
 	// files store every file occurred in the same commit with every other file.
 	files map[string]map[string]int
+	// renames point from new file name to old file name.
+	renames *[]rename
+	// lastCommit is the last commit which was consumed.
+	lastCommit *object.Commit
 	// reversedPeopleDict references IdentityDetector.ReversedPeopleDict
 	reversedPeopleDict []string
 }
@@ -47,6 +51,11 @@ type CouplesResult struct {
 	reversedPeopleDict []string
 }
 
+type rename struct {
+	FromName string
+	ToName string
+}
+
 // Name of this PipelineItem. Uniquely identifies the type, used for mapping keys, etc.
 func (couples *CouplesAnalysis) Name() string {
 	return "Couples"
@@ -94,6 +103,7 @@ func (couples *CouplesAnalysis) Initialize(repository *git.Repository) {
 	}
 	couples.peopleCommits = make([]int, couples.PeopleNumber+1)
 	couples.files = map[string]map[string]int{}
+	couples.renames = &[]rename{}
 	couples.OneShotMergeProcessor.Initialize()
 }
 
@@ -103,23 +113,18 @@ func (couples *CouplesAnalysis) Initialize(repository *git.Repository) {
 // This function returns the mapping with analysis results. The keys must be the same as
 // in Provides(). If there was an error, nil is returned.
 func (couples *CouplesAnalysis) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
-	if !couples.ShouldConsumeCommit(deps) {
-		return nil, nil
-	}
+	firstMerge := couples.ShouldConsumeCommit(deps)
+	mergeMode := core.IsMergeCommit(deps)
+	couples.lastCommit = deps[core.DependencyCommit].(*object.Commit)
 	author := deps[identity.DependencyAuthor].(int)
 	if author == identity.AuthorMissing {
 		author = couples.PeopleNumber
 	}
-	couples.peopleCommits[author]++
-	treeDiff := deps[items.DependencyTreeChanges].(object.Changes)
-	context := make([]string, 0)
-	deleteFile := func(name string) {
-		// we do not remove the file from people - the context does not expire
-		delete(couples.files, name)
-		for _, otherFiles := range couples.files {
-			delete(otherFiles, name)
-		}
+	if firstMerge {
+		couples.peopleCommits[author]++
 	}
+	treeDiff := deps[items.DependencyTreeChanges].(object.Changes)
+	context := make([]string, 0, len(treeDiff))
 	for _, change := range treeDiff {
 		action, err := change.Action()
 		if err != nil {
@@ -129,32 +134,28 @@ func (couples *CouplesAnalysis) Consume(deps map[string]interface{}) (map[string
 		fromName := change.From.Name
 		switch action {
 		case merkletrie.Insert:
-			context = append(context, toName)
-			couples.people[author][toName]++
+			if !mergeMode {
+				context = append(context, toName)
+				couples.people[author][toName]++
+			} else if couples.people[author][toName] == 0 {
+				couples.people[author][toName] = 1
+			}
 		case merkletrie.Delete:
-			deleteFile(fromName)
-			couples.people[author][fromName]++
+			if !mergeMode {
+				couples.people[author][fromName]++
+			} else if couples.people[author][fromName] == 0 {
+				couples.people[author][fromName] = 1
+			}
 		case merkletrie.Modify:
 			if fromName != toName {
 				// renamed
-				couples.files[toName] = couples.files[fromName]
-				for _, otherFiles := range couples.files {
-					val, exists := otherFiles[fromName]
-					if exists {
-						otherFiles[toName] = val
-					}
-				}
-				deleteFile(fromName)
-				for _, authorFiles := range couples.people {
-					val, exists := authorFiles[fromName]
-					if exists {
-						authorFiles[toName] = val
-						delete(authorFiles, fromName)
-					}
-				}
+				*couples.renames = append(
+					*couples.renames, rename{ToName: toName, FromName: fromName})
+			}
+			if !mergeMode {
+				context = append(context, toName)
+				couples.people[author][toName]++
 			}
-			context = append(context, toName)
-			couples.people[author][toName]++
 		}
 	}
 	for _, file := range context {
@@ -172,9 +173,10 @@ func (couples *CouplesAnalysis) Consume(deps map[string]interface{}) (map[string
 
 // Finalize returns the result of the analysis. Further Consume() calls are not expected.
 func (couples *CouplesAnalysis) Finalize() interface{} {
-	filesSequence := make([]string, len(couples.files))
+	files, people := couples.propagateRenames(couples.currentFiles())
+	filesSequence := make([]string, len(files))
 	i := 0
-	for file := range couples.files {
+	for file := range files {
 		filesSequence[i] = file
 		i++
 	}
@@ -188,12 +190,11 @@ func (couples *CouplesAnalysis) Finalize() interface{} {
 	peopleFiles := make([][]int, couples.PeopleNumber+1)
 	for i := range peopleMatrix {
 		peopleMatrix[i] = map[int]int64{}
-		for file, commits := range couples.people[i] {
-			fi, exists := filesIndex[file]
-			if exists {
+		for file, commits := range people[i] {
+			if fi, exists := filesIndex[file]; exists {
 				peopleFiles[i] = append(peopleFiles[i], fi)
 			}
-			for j, otherFiles := range couples.people {
+			for j, otherFiles := range people {
 				otherCommits := otherFiles[file]
 				delta := otherCommits
 				if otherCommits > commits {
@@ -210,7 +211,7 @@ func (couples *CouplesAnalysis) Finalize() interface{} {
 	filesMatrix := make([]map[int]int64, len(filesIndex))
 	for i := range filesMatrix {
 		filesMatrix[i] = map[int]int64{}
-		for otherFile, cooccs := range couples.files[filesSequence[i]] {
+		for otherFile, cooccs := range files[filesSequence[i]] {
 			filesMatrix[i][filesIndex[otherFile]] = int64(cooccs)
 		}
 	}
@@ -225,7 +226,7 @@ func (couples *CouplesAnalysis) Finalize() interface{} {
 
 // Fork clones this pipeline item.
 func (couples *CouplesAnalysis) Fork(n int) []core.PipelineItem {
-	return core.ForkSamePipelineItem(couples, n)
+	return core.ForkCopyPipelineItem(couples, n)
 }
 
 // Serialize converts the analysis result as returned by Finalize() to text or bytes.
@@ -362,7 +363,7 @@ func (couples *CouplesAnalysis) serializeText(result *CouplesResult, writer io.W
 	fmt.Fprintln(writer, "    matrix:")
 	for _, files := range result.FilesMatrix {
 		fmt.Fprint(writer, "      - {")
-		indices := []int{}
+		var indices []int
 		for file := range files {
 			indices = append(indices, file)
 		}
@@ -378,14 +379,14 @@ func (couples *CouplesAnalysis) serializeText(result *CouplesResult, writer io.W
 
 	fmt.Fprintln(writer, "  people_coocc:")
 	fmt.Fprintln(writer, "    index:")
-	for _, person := range couples.reversedPeopleDict {
+	for _, person := range result.reversedPeopleDict {
 		fmt.Fprintf(writer, "      - %s\n", yaml.SafeString(person))
 	}
 
 	fmt.Fprintln(writer, "    matrix:")
 	for _, people := range result.PeopleMatrix {
 		fmt.Fprint(writer, "      - {")
-		indices := []int{}
+		var indices []int
 		for file := range people {
 			indices = append(indices, file)
 		}
@@ -400,7 +401,7 @@ func (couples *CouplesAnalysis) serializeText(result *CouplesResult, writer io.W
 	}
 
 	fmt.Fprintln(writer, "    author_files:") // sorted by number of files each author changed
-	peopleFiles := sortByNumberOfFiles(result.PeopleFiles, couples.reversedPeopleDict, result.Files)
+	peopleFiles := sortByNumberOfFiles(result.PeopleFiles, result.reversedPeopleDict, result.Files)
 	for _, authorFiles := range peopleFiles {
 		fmt.Fprintf(writer, "      - %s:\n", yaml.SafeString(authorFiles.Author))
 		sort.Strings(authorFiles.Files)
@@ -474,6 +475,114 @@ func (couples *CouplesAnalysis) serializeBinary(result *CouplesResult, writer io
 	return nil
 }
 
+// currentFiles return the list of files in the last consumed commit.
+func (couples *CouplesAnalysis) currentFiles() map[string]bool {
+	files := map[string]bool{}
+	if couples.lastCommit == nil {
+		for key := range couples.files {
+			files[key] = true
+		}
+	}
+	tree, _ := couples.lastCommit.Tree()
+	fileIter := tree.Files()
+	fileIter.ForEach(func(fobj *object.File) error {
+		files[fobj.Name] = true
+		return nil
+	})
+	return files
+}
+
+// propagateRenames applies `renames` over the files from `lastCommit`.
+func (couples *CouplesAnalysis) propagateRenames(files map[string]bool) (
+	map[string]map[string]int, []map[string]int) {
+
+	renames := *couples.renames
+	reducedFiles := map[string]map[string]int{}
+	for file := range files {
+		fmap := map[string]int{}
+		refmap := couples.files[file]
+		for other := range files {
+			refval := refmap[other]
+			if refval > 0 {
+				fmap[other] = refval
+			}
+		}
+		if len(fmap) > 0 {
+			reducedFiles[file] = fmap
+		}
+	}
+	// propagate renames
+	aliases := map[string]map[string]bool{}
+	pointers := map[string]string{}
+	for i := range renames {
+		rename := renames[len(renames)-i-1]
+		toName := rename.ToName
+		if newTo, exists := pointers[toName]; exists {
+			toName = newTo
+		}
+		if _, exists := reducedFiles[toName]; exists {
+			if rename.FromName != toName {
+				var set map[string]bool
+				if set, exists = aliases[toName]; !exists {
+					set = map[string]bool{}
+					aliases[toName] = set
+				}
+				set[rename.FromName] = true
+				pointers[rename.FromName] = toName
+			}
+			continue
+		}
+	}
+	adjustments := map[string]map[string]int{}
+	for final, set := range aliases {
+		adjustment := map[string]int{}
+		for alias := range set {
+			for k, v := range couples.files[alias] {
+				adjustment[k] += v
+			}
+		}
+		adjustments[final] = adjustment
+	}
+	for _, adjustment := range adjustments {
+		for final, set := range aliases {
+			for alias := range set {
+				adjustment[final] += adjustment[alias]
+				delete(adjustment, alias)
+			}
+		}
+	}
+	for final, adjustment := range adjustments {
+		for key, val := range adjustment {
+			if coocc, exists := reducedFiles[final][key]; exists {
+				reducedFiles[final][key] = coocc + val
+				reducedFiles[key][final] = coocc + val
+			}
+		}
+	}
+	people := make([]map[string]int, len(couples.people))
+	for i, counts := range couples.people {
+		reducedCounts := map[string]int{}
+		people[i] = reducedCounts
+		for file := range files {
+			count := counts[file]
+			for alias := range aliases[file] {
+				count += counts[alias]
+			}
+			if count > 0 {
+				reducedCounts[file] = count
+			}
+		}
+		for key, val := range counts {
+			if _, exists := files[key]; !exists {
+				if _, exists = pointers[key]; !exists {
+					reducedCounts[key] = val
+				}
+			}
+		}
+	}
+	return reducedFiles, people
+}
+
 func init() {
 	core.Registry.Register(&CouplesAnalysis{})
 }

+ 125 - 69
leaves/couples_test.go

@@ -16,7 +16,7 @@ import (
 	"gopkg.in/src-d/hercules.v4/internal/plumbing"
 	"gopkg.in/src-d/hercules.v4/internal/plumbing/identity"
 	"gopkg.in/src-d/hercules.v4/internal/test"
-)
+	)
 
 func fixtureCouples() *CouplesAnalysis {
 	c := CouplesAnalysis{PeopleNumber: 3}
@@ -91,61 +91,78 @@ func TestCouplesConsumeFinalize(t *testing.T) {
 	deps := map[string]interface{}{}
 	deps[identity.DependencyAuthor] = 0
 	deps[core.DependencyCommit], _ = test.Repository.CommitObject(gitplumbing.NewHash(
-		"cce947b98a050c6d356bc6ba95030254914027b1"))
-	deps[plumbing.DependencyTreeChanges] = generateChanges("+two", "+four", "+six")
+		"a3ee37f91f0d705ec9c41ae88426f0ae44b2fbc3"))
+	deps[plumbing.DependencyTreeChanges] = generateChanges("+LICENSE2", "+file2.go", "+rbtree2.go")
 	c.Consume(deps)
-	deps[plumbing.DependencyTreeChanges] = generateChanges("+one", "-two", "=three", ">four>five")
+	deps[plumbing.DependencyTreeChanges] = generateChanges("+README.md", "-LICENSE2", "=analyser.go", ">file2.go>file_test.go")
 	c.Consume(deps)
 	deps[identity.DependencyAuthor] = 1
-	deps[plumbing.DependencyTreeChanges] = generateChanges("=one", "=three", "-six")
+	deps[plumbing.DependencyTreeChanges] = generateChanges("=README.md", "=analyser.go", "-rbtree2.go")
 	c.Consume(deps)
 	deps[identity.DependencyAuthor] = 2
-	deps[plumbing.DependencyTreeChanges] = generateChanges("=five")
+	deps[plumbing.DependencyTreeChanges] = generateChanges("=file_test.go")
 	c.Consume(deps)
-	assert.Equal(t, len(c.people[0]), 5)
-	assert.Equal(t, c.people[0]["one"], 1)
-	assert.Equal(t, c.people[0]["two"], 2)
-	assert.Equal(t, c.people[0]["three"], 1)
-	assert.Equal(t, c.people[0]["five"], 2)
-	assert.Equal(t, c.people[0]["six"], 1)
+	assert.Equal(t, len(c.people[0]), 6)
+	assert.Equal(t, c.people[0]["README.md"], 1)
+	assert.Equal(t, c.people[0]["LICENSE2"], 2)
+	assert.Equal(t, c.people[0]["analyser.go"], 1)
+	assert.Equal(t, c.people[0]["file2.go"], 1)
+	assert.Equal(t, c.people[0]["file_test.go"], 1)
+	assert.Equal(t, c.people[0]["rbtree2.go"], 1)
 	assert.Equal(t, len(c.people[1]), 3)
-	assert.Equal(t, c.people[1]["one"], 1)
-	assert.Equal(t, c.people[1]["three"], 1)
-	assert.Equal(t, c.people[1]["six"], 1)
+	assert.Equal(t, c.people[1]["README.md"], 1)
+	assert.Equal(t, c.people[1]["analyser.go"], 1)
+	assert.Equal(t, c.people[1]["rbtree2.go"], 1)
 	assert.Equal(t, len(c.people[2]), 1)
-	assert.Equal(t, c.people[2]["five"], 1)
-	assert.Equal(t, len(c.files["one"]), 3)
-	assert.Equal(t, c.files["one"]["one"], 2)
-	assert.Equal(t, c.files["one"]["three"], 2)
-	assert.Equal(t, c.files["one"]["five"], 1)
-	assert.NotContains(t, c.files, "two")
-	assert.NotContains(t, c.files, "four")
-	assert.NotContains(t, c.files, "six")
-	assert.Equal(t, len(c.files["three"]), 3)
-	assert.Equal(t, c.files["three"]["three"], 2)
-	assert.Equal(t, c.files["three"]["one"], 2)
-	assert.Equal(t, c.files["three"]["five"], 1)
-	assert.Equal(t, len(c.files["five"]), 3)
-	assert.Equal(t, c.files["five"]["five"], 3)
-	assert.Equal(t, c.files["five"]["one"], 1)
-	assert.Equal(t, c.files["five"]["three"], 1)
+	assert.Equal(t, c.people[2]["file_test.go"], 1)
+	assert.Equal(t, len(c.files["README.md"]), 3)
+	assert.Equal(t, c.files["README.md"], map[string]int{
+		"README.md": 2,
+		"analyser.go": 2,
+		"file_test.go": 1,
+	})
+	assert.Equal(t, c.files["LICENSE2"], map[string]int{
+		"LICENSE2": 1,
+		"file2.go": 1,
+		"rbtree2.go": 1,
+	})
+	assert.Equal(t, c.files["file2.go"], map[string]int{
+		"LICENSE2": 1,
+		"file2.go": 1,
+		"rbtree2.go": 1,
+	})
+	assert.Equal(t, c.files["rbtree2.go"], map[string]int{
+		"LICENSE2": 1,
+		"file2.go": 1,
+		"rbtree2.go": 1,
+	})
+	assert.Equal(t, c.files["analyser.go"], map[string]int{
+		"analyser.go": 2,
+		"README.md": 2,
+		"file_test.go": 1,
+	})
+	assert.Equal(t, c.files["file_test.go"], map[string]int{
+		"file_test.go": 2,
+		"README.md": 1,
+		"analyser.go": 1,
+	})
 	assert.Equal(t, c.peopleCommits[0], 2)
 	assert.Equal(t, c.peopleCommits[1], 1)
 	assert.Equal(t, c.peopleCommits[2], 1)
 	cr := c.Finalize().(CouplesResult)
 	assert.Equal(t, len(cr.Files), 3)
-	assert.Equal(t, cr.Files[0], "five")
-	assert.Equal(t, cr.Files[1], "one")
-	assert.Equal(t, cr.Files[2], "three")
+	assert.Equal(t, cr.Files[0], "README.md")
+	assert.Equal(t, cr.Files[1], "analyser.go")
+	assert.Equal(t, cr.Files[2], "file_test.go")
 	assert.Equal(t, len(cr.PeopleFiles[0]), 3)
 	assert.Equal(t, cr.PeopleFiles[0][0], 0)
 	assert.Equal(t, cr.PeopleFiles[0][1], 1)
 	assert.Equal(t, cr.PeopleFiles[0][2], 2)
 	assert.Equal(t, len(cr.PeopleFiles[1]), 2)
-	assert.Equal(t, cr.PeopleFiles[1][0], 1)
-	assert.Equal(t, cr.PeopleFiles[1][1], 2)
+	assert.Equal(t, cr.PeopleFiles[1][0], 0)
+	assert.Equal(t, cr.PeopleFiles[1][1], 1)
 	assert.Equal(t, len(cr.PeopleFiles[2]), 1)
-	assert.Equal(t, cr.PeopleFiles[2][0], 0)
+	assert.Equal(t, cr.PeopleFiles[2][0], 2)
 	assert.Equal(t, len(cr.PeopleMatrix[0]), 3)
 	assert.Equal(t, cr.PeopleMatrix[0][0], int64(7))
 	assert.Equal(t, cr.PeopleMatrix[0][1], int64(3))
@@ -158,17 +175,17 @@ func TestCouplesConsumeFinalize(t *testing.T) {
 	assert.Equal(t, cr.PeopleMatrix[2][2], int64(1))
 	assert.Equal(t, len(cr.FilesMatrix), 3)
 	assert.Equal(t, len(cr.FilesMatrix[0]), 3)
-	assert.Equal(t, cr.FilesMatrix[0][0], int64(3))
-	assert.Equal(t, cr.FilesMatrix[0][1], int64(1))
 	assert.Equal(t, cr.FilesMatrix[0][2], int64(1))
+	assert.Equal(t, cr.FilesMatrix[0][0], int64(2))
+	assert.Equal(t, cr.FilesMatrix[0][1], int64(2))
 	assert.Equal(t, len(cr.FilesMatrix[1]), 3)
-	assert.Equal(t, cr.FilesMatrix[1][0], int64(1))
+	assert.Equal(t, cr.FilesMatrix[1][2], int64(1))
+	assert.Equal(t, cr.FilesMatrix[1][0], int64(2))
 	assert.Equal(t, cr.FilesMatrix[1][1], int64(2))
-	assert.Equal(t, cr.FilesMatrix[1][2], int64(2))
 	assert.Equal(t, len(cr.FilesMatrix[2]), 3)
 	assert.Equal(t, cr.FilesMatrix[2][0], int64(1))
-	assert.Equal(t, cr.FilesMatrix[2][1], int64(2))
-	assert.Equal(t, cr.FilesMatrix[2][2], int64(2))
+	assert.Equal(t, cr.FilesMatrix[2][1], int64(1))
+	assert.Equal(t, cr.FilesMatrix[2][2], int64(3))
 }
 
 func TestCouplesFork(t *testing.T) {
@@ -176,36 +193,26 @@ func TestCouplesFork(t *testing.T) {
 	clones := couples1.Fork(1)
 	assert.Len(t, clones, 1)
 	couples2 := clones[0].(*CouplesAnalysis)
-	assert.True(t, couples1 == couples2)
+	assert.True(t, couples1 != couples2)
+	assert.Equal(t, *couples1, *couples2)
 	couples1.Merge([]core.PipelineItem{couples2})
 }
 
 func TestCouplesSerialize(t *testing.T) {
 	c := fixtureCouples()
-	c.PeopleNumber = 1
-	people := [...]string{"p1", "p2", "p3"}
-	facts := map[string]interface{}{}
-	c.Configure(facts)
-	assert.Equal(t, c.PeopleNumber, 1)
-	facts[identity.FactIdentityDetectorPeopleCount] = 3
-	facts[identity.FactIdentityDetectorReversedPeopleDict] = people[:]
-	c.Configure(facts)
-	assert.Equal(t, c.PeopleNumber, 3)
-	deps := map[string]interface{}{}
-	deps[identity.DependencyAuthor] = 0
-	deps[plumbing.DependencyTreeChanges] = generateChanges("+two", "+four", "+six")
-	deps[core.DependencyCommit], _ = test.Repository.CommitObject(gitplumbing.NewHash(
-		"cce947b98a050c6d356bc6ba95030254914027b1"))
-	c.Consume(deps)
-	deps[plumbing.DependencyTreeChanges] = generateChanges("+one", "-two", "=three", ">four>five")
-	c.Consume(deps)
-	deps[identity.DependencyAuthor] = 1
-	deps[plumbing.DependencyTreeChanges] = generateChanges("=one", "=three", "-six")
-	c.Consume(deps)
-	deps[identity.DependencyAuthor] = 2
-	deps[plumbing.DependencyTreeChanges] = generateChanges("=five")
-	c.Consume(deps)
-	result := c.Finalize().(CouplesResult)
+	result := CouplesResult {
+		PeopleMatrix: []map[int]int64{
+			{0:7, 1:3, 2:1}, {0:3, 1:3}, {0:1, 2:1}, {},
+		},
+		PeopleFiles: [][]int{
+			{0, 1, 2}, {1, 2}, {0}, {},
+		},
+		FilesMatrix: []map[int]int64{
+			{1:1, 2:1, 0:3}, {1:2, 2:2, 0:1}, {2:2, 0:1, 1:2},
+		},
+		Files: []string{"five", "one", "three"},
+		reversedPeopleDict: []string{"p1", "p2", "p3"},
+	}
 	buffer := &bytes.Buffer{}
 	c.Serialize(result, false, buffer)
 	assert.Equal(t, buffer.String(), `  files_coocc:
@@ -249,7 +256,7 @@ func TestCouplesSerialize(t *testing.T) {
 	assert.Equal(t, msg.PeopleFiles[1].Files, tmp2[:])
 	tmp3 := [...]int32{0}
 	assert.Equal(t, msg.PeopleFiles[2].Files, tmp3[:])
-	assert.Equal(t, msg.PeopleCouples.Index, people[:])
+	assert.Equal(t, msg.PeopleCouples.Index, result.reversedPeopleDict)
 	assert.Equal(t, msg.PeopleCouples.Matrix.NumberOfRows, int32(4))
 	assert.Equal(t, msg.PeopleCouples.Matrix.NumberOfColumns, int32(4))
 	data := [...]int64{7, 3, 1, 3, 3, 1, 1}
@@ -353,6 +360,55 @@ func TestCouplesMerge(t *testing.T) {
 	assert.Equal(t, merged.FilesMatrix[2], getCouplesMap(1, 200))
 }
 
+func TestCouplesCurrentFiles(t *testing.T) {
+	c := fixtureCouples()
+	c.lastCommit, _ = test.Repository.CommitObject(gitplumbing.NewHash(
+		"cce947b98a050c6d356bc6ba95030254914027b1"))
+	files := c.currentFiles()
+	assert.Equal(t, files, map[string]bool{".gitignore": true, "LICENSE": true})
+}
+
+func TestCouplesPropagateRenames(t *testing.T) {
+	c := fixtureCouples()
+	c.files["one"] = map[string]int{
+		"one": 1,
+		"two": 2,
+		"three": 3,
+	}
+	c.files["two"] = map[string]int{
+		"one": 2,
+		"two": 10,
+		"three": 1,
+		"four": 7,
+	}
+	c.files["three"] = map[string]int{
+		"one": 3,
+		"two": 1,
+		"three": 3,
+		"four": 2,
+	}
+	c.files["four"] = map[string]int{
+		"two": 7,
+		"three": 3,
+		"four": 1,
+	}
+	c.PeopleNumber = 1
+	c.people = make([]map[string]int, 1)
+	c.people[0] = map[string]int{}
+	c.people[0]["one"] = 1
+	c.people[0]["two"] = 2
+	c.people[0]["three"] = 3
+	c.people[0]["four"] = 4
+	*c.renames = []rename{{ToName: "four", FromName: "one"}}
+	files, people := c.propagateRenames(map[string]bool{"two": true, "three": true, "four": true})
+	assert.Len(t, files, 3)
+	assert.Len(t, people, 1)
+	assert.Equal(t, files["two"], map[string]int{"two": 10, "three": 1, "four": 9})
+	assert.Equal(t, files["three"], map[string]int{"two": 1, "three": 3, "four": 6})
+	assert.Equal(t, files["four"], map[string]int{"two": 9, "three": 6, "four": 2})
+	assert.Equal(t, people[0], map[string]int{"two": 2, "three": 3, "four": 5})
+}
+
 func getSlice(vals ...int) []int {
 	return vals
 }