Ver código fonte

Merge pull request #1 from vmarkovtsev/master

Initial files
Vadim Markovtsev 8 anos atrás
pai
commit
a28e9064c7
7 arquivos alterados com 1513 adições e 0 exclusões
  1. 15 0
      README.md
  2. 294 0
      analyser.go
  3. 99 0
      cmd/hercules/main.go
  4. 141 0
      file.go
  5. 243 0
      file_test.go
  6. 17 0
      labours.py
  7. 704 0
      rbtree.go

+ 15 - 0
README.md

@@ -0,0 +1,15 @@
+Hercules
+--------
+
+This tool calculates the weekly lines burnout in a Git repository.
+
+###Usage
+
+```
+hercules https://github.com/src-d/go-git | python3 labours.py
+hercules /path/to/cloned/go-git | python3 labours.py
+hercules https://github.com/torvalds/linux /tmp/linux_cache | python3 labours.py
+```
+
+###License
+MIT.

+ 294 - 0
analyser.go

@@ -0,0 +1,294 @@
+package hercules
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"time"
+	"unicode/utf8"
+
+	"github.com/sergi/go-diff/diffmatchpatch"
+	"gopkg.in/src-d/go-git.v4"
+)
+
+type Analyser struct {
+	Repository  *git.Repository
+	Granularity int
+	OnProgress  func(int, int)
+}
+
+func checkClose(c io.Closer) {
+	if err := c.Close(); err != nil {
+		panic(err)
+	}
+}
+
+func loc(file *git.Blob) (int, error) {
+	reader, err := file.Reader()
+	if err != nil {
+		panic(err)
+	}
+	defer checkClose(reader)
+	scanner := bufio.NewScanner(reader)
+	counter := 0
+	for scanner.Scan() {
+		if !utf8.Valid(scanner.Bytes()) {
+			return -1, errors.New("binary")
+		}
+		counter++
+	}
+	return counter, nil
+}
+
+func str(file *git.Blob) string {
+	reader, err := file.Reader()
+	if err != nil {
+		panic(err)
+	}
+	defer checkClose(reader)
+	buf := new(bytes.Buffer)
+	buf.ReadFrom(reader)
+	return buf.String()
+}
+
+func (analyser *Analyser) handleInsertion(
+	change *git.Change, day int, status map[int]int64, files map[string]*File) {
+	blob, err := analyser.Repository.Blob(change.To.TreeEntry.Hash)
+	if err != nil {
+		panic(err)
+	}
+	lines, err := loc(blob)
+	if err != nil {
+		return
+	}
+	name := change.To.Name
+	file, exists := files[name]
+	if exists {
+		panic(fmt.Sprintf("file %s already exists", name))
+	}
+	file = NewFile(day, lines, status)
+	files[name] = file
+}
+
+func (analyser *Analyser) handleDeletion(
+	change *git.Change, day int, status map[int]int64, files map[string]*File) {
+	blob, err := analyser.Repository.Blob(change.From.TreeEntry.Hash)
+	if err != nil {
+		panic(err)
+	}
+	lines, err := loc(blob)
+	if err != nil {
+		return
+	}
+	name := change.From.Name
+	file := files[name]
+	file.Update(day, 0, 0, lines)
+	delete(files, name)
+}
+
+func (analyser *Analyser) handleModification(
+	change *git.Change, day int, status map[int]int64, files map[string]*File) {
+	blob_from, err := analyser.Repository.Blob(change.From.TreeEntry.Hash)
+	if err != nil {
+		panic(err)
+	}
+	blob_to, err := analyser.Repository.Blob(change.To.TreeEntry.Hash)
+	if err != nil {
+		panic(err)
+	}
+	// we are not validating UTF-8 here because for example
+	// git/git 4f7770c87ce3c302e1639a7737a6d2531fe4b160 fetch-pack.c is invalid UTF-8
+	str_from := str(blob_from)
+	str_to := str(blob_to)
+	file, exists := files[change.From.Name]
+	if !exists {
+		// fmt.Fprintf(os.Stderr, "warning: file %s does not exist\n", change.From.Name)
+		analyser.handleInsertion(change, day, status, files)
+		return
+	}
+	// possible rename
+	if change.To.Name != change.From.Name {
+		analyser.handleRename(change.From.Name, change.To.Name, files)
+	}
+	dmp := diffmatchpatch.New()
+	src, dst, _ := dmp.DiffLinesToRunes(str_from, str_to)
+	if file.Len() != len(src) {
+		panic(fmt.Sprintf("%s: internal integrity error src %d != %d",
+			change.To.Name, len(src), file.Len()))
+	}
+	diffs := dmp.DiffMainRunes(src, dst, false)
+	// we do not call RunesToDiffLines so the number of lines equals
+	// to the rune count
+	position := 0
+	for _, edit := range diffs {
+		length := utf8.RuneCountInString(edit.Text)
+		func() {
+			defer func() {
+				r := recover()
+				if r != nil {
+					fmt.Fprintf(os.Stderr, "%s: internal diff error\n", change.To.Name)
+					fmt.Fprint(os.Stderr, "====BEFORE====\n")
+					fmt.Fprint(os.Stderr, str_from)
+					fmt.Fprint(os.Stderr, "====AFTER====\n")
+					fmt.Fprint(os.Stderr, str_to)
+					fmt.Fprint(os.Stderr, "====END====\n")
+					panic(r)
+				}
+			}()
+			switch edit.Type {
+			case diffmatchpatch.DiffEqual:
+				position += length
+			case diffmatchpatch.DiffInsert:
+				file.Update(day, position, length, 0)
+				position += length
+			case diffmatchpatch.DiffDelete:
+				file.Update(day, position, 0, length)
+			default:
+				panic(fmt.Sprintf("diff operation is not supported: %d", edit.Type))
+			}
+		}()
+	}
+	if file.Len() != len(dst) {
+		panic(fmt.Sprintf("%s: internal integrity error dst %d != %d",
+			change.To.Name, len(dst), file.Len()))
+	}
+}
+
+func (analyser *Analyser) handleRename(from, to string, files map[string]*File) {
+	file, exists := files[from]
+	if !exists {
+		panic(fmt.Sprintf("file %s does not exist", from))
+	}
+	files[to] = file
+	delete(files, from)
+}
+
+func (analyser *Analyser) commits() []*git.Commit {
+	result := []*git.Commit{}
+	repository := analyser.Repository
+	head, err := repository.Head()
+	if err != nil {
+		panic(err)
+	}
+	commit, err := repository.Commit(head.Hash())
+	if err != nil {
+		panic(err)
+	}
+	result = append(result, commit)
+	for ; err != io.EOF; commit, err = commit.Parents().Next() {
+		if err != nil {
+			panic(err)
+		}
+		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 result
+}
+
+func (analyser *Analyser) groupStatus(status map[int]int64, day int) []int64 {
+	granularity := analyser.Granularity
+	result := make([]int64, day/granularity)
+	var group int64
+	for i := 0; i < day; i++ {
+		group += status[i]
+		if i%granularity == (granularity - 1) {
+			result[i/granularity] = group
+			group = 0
+		}
+	}
+	return result
+}
+
+func (analyser *Analyser) Analyse() [][]int64 {
+	granularity := analyser.Granularity
+	if granularity == 0 {
+		granularity = 1
+	}
+	onProgress := analyser.OnProgress
+	if onProgress == nil {
+		onProgress = func(int, int) {}
+	}
+
+	// current daily alive number of lines; key is the number of days from the
+	// beginning of the history
+	status := map[int]int64{}
+	// weekly snapshots of status
+	statuses := [][]int64{}
+	// mapping <file path> -> hercules.File
+	files := map[string]*File{}
+	// list of commits belonging to the default branch, from oldest to newest
+	commits := analyser.commits()
+
+	var day0 time.Time // will be initialized in the first iteration
+	var prev_tree *git.Tree = nil
+	prev_day := 0
+
+	for index, commit := range commits {
+		onProgress(index, len(commits))
+		tree, err := commit.Tree()
+		if err != nil {
+			panic(err)
+		}
+		if index == 0 {
+			// first iteration - initialize the file objects from the tree
+			day0 = commit.Author.When
+			func() {
+				file_iter := tree.Files()
+				defer file_iter.Close()
+				for {
+					file, err := file_iter.Next()
+					if err != nil {
+						if err == io.EOF {
+							break
+						}
+						panic(err)
+					}
+					lines, err := loc(&file.Blob)
+					if err == nil {
+						files[file.Name] = NewFile(0, lines, status)
+					}
+				}
+			}()
+		} else {
+			day := int(commit.Author.When.Sub(day0).Hours() / 24)
+			delta := (day / granularity) - (prev_day / granularity)
+			if delta > 0 {
+				prev_day = day
+				statuses = append(statuses, analyser.groupStatus(status, day))
+			}
+			tree_diff, err := git.DiffTree(prev_tree, tree)
+			if err != nil {
+				panic(err)
+			}
+			for _, change := range tree_diff {
+				switch change.Action {
+				case git.Insert:
+					analyser.handleInsertion(change, day, status, files)
+				case git.Delete:
+					analyser.handleDeletion(change, day, status, files)
+				case git.Modify:
+					func() {
+						defer func() {
+							r := recover()
+							if r != nil {
+								fmt.Fprintf(os.Stderr, "%s: modification error\n", commit.Hash.String())
+								panic(r)
+							}
+						}()
+						analyser.handleModification(change, day, status, files)
+					}()
+				default:
+					panic(fmt.Sprintf("unsupported action: %d", change.Action))
+				}
+			}
+		}
+		prev_tree = tree
+	}
+	return statuses
+}

+ 99 - 0
cmd/hercules/main.go

@@ -0,0 +1,99 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"runtime/pprof"
+	"strconv"
+	"strings"
+
+	"gopkg.in/src-d/go-git.v4"
+	"gopkg.in/src-d/hercules.v1"
+)
+
+func main() {
+	var profile bool
+	var granularity int
+	flag.BoolVar(&profile, "profile", false, "Collect the profile to hercules.pprof.")
+	flag.IntVar(&granularity, "granularity", 30, "Report granularity in days.")
+	flag.Parse()
+	if (granularity <= 0) {
+		fmt.Fprint(os.Stderr, "Warning: adjusted the granularity to 1 day\n")
+		granularity = 1
+	}
+	if profile {
+		prof, _ := os.Create("profile")
+		pprof.StartCPUProfile(prof)
+		defer pprof.StopCPUProfile()
+	}
+	if len(flag.Args()) == 0 || len(flag.Args()) > 3 {
+		fmt.Fprint(os.Stderr,
+			         "Usage: hercules <path to repo or URL> [<disk cache path>]\n")
+		os.Exit(1)
+	}
+  uri := flag.Arg(0)
+	var repository *git.Repository
+	var err error
+	if strings.Contains(uri, "://") {
+		if len(flag.Args()) == 2 {
+			repository, err = git.NewFilesystemRepository(flag.Arg(1))
+			if err != nil {
+				panic(err)
+			}
+		} else {
+			repository = git.NewMemoryRepository()
+		}
+		fmt.Fprint(os.Stderr, "cloning...\r")
+		err = repository.Clone(&git.CloneOptions{
+		  URL: uri,
+	  })
+		fmt.Fprint(os.Stderr, "          \r")
+	} else {
+		if uri[len(uri) - 1] == os.PathSeparator {
+			uri = uri[:len(uri) - 1]
+		}
+		if !strings.HasSuffix(uri, ".git") {
+			uri += string(os.PathSeparator) + ".git"
+		}
+		repository, err = git.NewFilesystemRepository(uri)
+	}
+	if err != nil {
+		panic(err)
+	}
+	// core logic
+	analyser := hercules.Analyser{
+		Repository: repository,
+		OnProgress: func(commit, length int) {
+		  fmt.Fprintf(os.Stderr, "%d / %d\r", commit, length)
+	  },
+		Granularity: granularity,
+	}
+	statuses := analyser.Analyse()
+	fmt.Fprint(os.Stderr, "        \r")
+	if len(statuses) == 0 {
+		return
+	}
+	// determine the maximum length of each value
+	var maxnum int64
+	for _, status := range statuses {
+		for _, val := range status {
+			if val > maxnum {
+				maxnum = val
+			}
+		}
+	}
+	width := len(strconv.FormatInt(maxnum, 10))
+	last := len(statuses[len(statuses) - 1])
+	// print the resulting triangle matrix
+	for _, status := range statuses {
+		for i := 0; i < last; i++ {
+			var val int64
+			if i < len(status) {
+				val = status[i]
+			}
+			fmt.Printf("%[1]*[2]d ", width, val)
+		}
+		println()
+	}
+}

+ 141 - 0
file.go

@@ -0,0 +1,141 @@
+package hercules
+
+import "fmt"
+
+type File struct {
+	tree   *RBTree
+	status map[int]int64
+}
+
+const TreeEnd int = -1
+
+func min(a int, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+func max(a int, b int) int {
+	if a < b {
+		return b
+	}
+	return a
+}
+
+func NewFile(time int, length int, status map[int]int64) *File {
+	file := new(File)
+	file.status = status
+	file.tree = new(RBTree)
+	if length > 0 {
+		status[time] += int64(length)
+		file.tree.Insert(Item{key: 0, value: time})
+	}
+	file.tree.Insert(Item{key: length, value: TreeEnd})
+	return file
+}
+
+func (file *File) Len() int {
+	return file.tree.Max().Item().key
+}
+
+func (file *File) Update(time int, pos int, ins_length int, del_length int) {
+	if time < 0 {
+		panic("time may not be negative")
+	}
+	if pos < 0 {
+		panic("attempt to insert/delete at a negative position")
+	}
+	if ins_length < 0 || del_length < 0 {
+		panic("ins_length and del_length must be nonnegative")
+	}
+	if ins_length|del_length == 0 {
+		return
+	}
+	tree := file.tree
+	if pos > tree.Max().Item().key {
+		panic(fmt.Sprintf("attempt to insert after the end of the file: %d < %d",
+			tree.Max().Item().key, pos))
+	}
+	status := file.status
+	iter := tree.FindLE(pos)
+	origin := *iter.Item()
+	status[time] += int64(ins_length)
+	if del_length == 0 {
+		// simple case with insertions only
+		if origin.key < pos {
+			iter = iter.Next()
+		}
+		for ; !iter.Limit(); iter = iter.Next() {
+			iter.Item().key += ins_length
+		}
+		tree.Insert(Item{key: pos, value: time})
+		if origin.key < pos {
+			tree.Insert(Item{key: pos + ins_length, value: origin.value})
+		}
+		return
+	}
+
+	// delete nodes
+	for true {
+		node := iter.Item()
+		next_iter := iter.Next()
+		if next_iter.Limit() {
+			if pos+del_length > node.key {
+				panic("attempt to delete after the end of the file")
+			}
+			break
+		}
+		delta := min(next_iter.Item().key, pos+del_length) - max(node.key, pos)
+		if delta <= 0 {
+			break
+		}
+		status[node.value] -= int64(delta)
+		if node.key >= pos {
+			origin = *node
+			tree.DeleteWithIterator(iter)
+		}
+		iter = next_iter
+	}
+
+	// prepare for the keys update
+	var previous *Item
+	if ins_length > 0 {
+		if origin.value != time {
+			// insert our new interval
+			_, iter = tree.Insert(Item{key: pos, value: time})
+		}
+	} else {
+		// rollback 1 position back, see "for true" deletion cycle ^
+		iter = iter.Prev()
+		previous = iter.Item()
+	}
+
+	// update the keys of all subsequent nodes
+	delta := ins_length - del_length
+	if delta != 0 {
+		for iter = iter.Next(); !iter.Limit(); iter = iter.Next() {
+			// we do not need to re-balance the tree
+			iter.Item().key += delta
+		}
+		// have to adjust origin in case ins_length == 0
+		if origin.key > pos {
+			origin.key += delta
+		}
+	}
+	if ins_length > 0 {
+		tree.Insert(Item{pos + ins_length, origin.value})
+	} else if (pos > origin.key && previous.value != origin.value) || pos == origin.key {
+		// continue the original interval
+		tree.Insert(Item{pos, origin.value})
+	}
+}
+
+func (file *File) Dump() string {
+	buffer := ""
+	for iter := file.tree.Min(); !iter.Limit(); iter = iter.Next() {
+		node := iter.Item()
+		buffer += fmt.Sprintf("%d %d\n", node.key, node.value)
+	}
+	return buffer
+}

+ 243 - 0
file_test.go

@@ -0,0 +1,243 @@
+package hercules
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func fixture() (*File, map[int]int64) {
+	status := map[int]int64{}
+	file := NewFile(0, 100, status)
+	return file, status
+}
+
+func TestInitialize(t *testing.T) {
+	file, status := fixture()
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 100 -1
+	assert.Equal(t, "0 0\n100 -1\n", dump)
+	assert.Equal(t, int64(100), status[0])
+}
+
+func testPanic(t *testing.T, method func(*File), msg string) {
+	defer func() {
+		r := recover()
+		assert.NotNil(t, r, "not panic()-ed")
+		assert.IsType(t, "", r)
+		assert.Contains(t, r.(string), msg)
+	}()
+	file, _ := fixture()
+	method(file)
+}
+
+func TestBullshit(t *testing.T) {
+	testPanic(t, func(file *File) { file.Update(1, -10, 10, 0) }, "insert")
+	testPanic(t, func(file *File) { file.Update(1, 110, 10, 0) }, "insert")
+	testPanic(t, func(file *File) { file.Update(1, -10, 0, 10) }, "delete")
+	testPanic(t, func(file *File) { file.Update(1, 100, 0, 10) }, "delete")
+	testPanic(t, func(file *File) { file.Update(1, 0, -10, 0) }, "length")
+	testPanic(t, func(file *File) { file.Update(1, 0, 0, -10) }, "length")
+	testPanic(t, func(file *File) { file.Update(1, 0, -10, -10) }, "length")
+	testPanic(t, func(file *File) { file.Update(-1, 0, 10, 10) }, "time")
+	file, status := fixture()
+	file.Update(1, 10, 0, 0)
+	assert.Equal(t, int64(100), status[0])
+	assert.Equal(t, int64(0), status[1])
+}
+
+func TestLen(t *testing.T) {
+	file, _ := fixture()
+	assert.Equal(t, 100, file.Len())
+}
+
+func TestInsert(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 10, 10, 0)
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 10 1
+	// 20 0
+	// 110 -1
+	assert.Equal(t, "0 0\n10 1\n20 0\n110 -1\n", dump)
+	assert.Equal(t, int64(100), status[0])
+	assert.Equal(t, int64(10), status[1])
+}
+
+func TestZeroInitialize(t *testing.T) {
+	status := map[int]int64{}
+	file := NewFile(0, 0, status)
+	assert.NotContains(t, status, 0)
+	dump := file.Dump()
+	// Output:
+	// 0 -1
+	assert.Equal(t, "0 -1\n", dump)
+	file.Update(1, 0, 10, 0)
+	dump = file.Dump()
+	// Output:
+	// 0 1
+	// 10 -1
+	assert.Equal(t, "0 1\n10 -1\n", dump)
+	assert.Equal(t, int64(10), status[1])
+}
+
+func TestDelete(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 10, 0, 10)
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 90 -1
+	assert.Equal(t, "0 0\n90 -1\n", dump)
+	assert.Equal(t, int64(90), status[0])
+	assert.Equal(t, int64(0), status[1])
+}
+
+func TestFused(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 10, 6, 7)
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 10 1
+	// 16 0
+	// 99 -1
+	assert.Equal(t, "0 0\n10 1\n16 0\n99 -1\n", dump)
+	assert.Equal(t, int64(93), status[0])
+	assert.Equal(t, int64(6), status[1])
+}
+
+func TestInsertSameStart(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 10, 10, 0)
+	file.Update(2, 10, 10, 0)
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 10 2
+	// 20 1
+	// 30 0
+	// 120 -1
+	assert.Equal(t, "0 0\n10 2\n20 1\n30 0\n120 -1\n", dump)
+	assert.Equal(t, int64(100), status[0])
+	assert.Equal(t, int64(10), status[1])
+	assert.Equal(t, int64(10), status[2])
+}
+
+func TestInsertEnd(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 100, 10, 0)
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 100 1
+	// 110 -1
+	assert.Equal(t, "0 0\n100 1\n110 -1\n", dump)
+	assert.Equal(t, int64(100), status[0])
+	assert.Equal(t, int64(10), status[1])
+}
+
+func TestDeleteSameStart0(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 0, 0, 10)
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 90 -1
+	assert.Equal(t, "0 0\n90 -1\n", dump)
+	assert.Equal(t, int64(90), status[0])
+	assert.Equal(t, int64(0), status[1])
+}
+
+func TestDeleteSameStartMiddle(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 10, 10, 0)
+	file.Update(2, 10, 0, 5)
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 10 1
+	// 15 0
+	// 105 -1
+	assert.Equal(t, "0 0\n10 1\n15 0\n105 -1\n", dump)
+	assert.Equal(t, int64(100), status[0])
+	assert.Equal(t, int64(5), status[1])
+}
+
+func TestDeleteIntersection(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 10, 10, 0)
+	file.Update(2, 15, 0, 10)
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 10 1
+	// 15 0
+	// 100 -1
+	assert.Equal(t, "0 0\n10 1\n15 0\n100 -1\n", dump)
+	assert.Equal(t, int64(95), status[0])
+	assert.Equal(t, int64(5), status[1])
+}
+
+func TestDeleteAll(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 0, 0, 100)
+	// Output:
+	// 0 -1
+	dump := file.Dump()
+	assert.Equal(t, "0 -1\n", dump)
+	assert.Equal(t, int64(0), status[0])
+	assert.Equal(t, int64(0), status[1])
+}
+
+func TestFusedIntersection(t *testing.T) {
+	file, status := fixture()
+	file.Update(1, 10, 10, 0)
+	file.Update(2, 15, 3, 10)
+	dump := file.Dump()
+	// Output:
+	// 0 0
+	// 10 1
+	// 15 2
+	// 18 0
+	// 103 -1
+	assert.Equal(t, "0 0\n10 1\n15 2\n18 0\n103 -1\n", dump)
+	assert.Equal(t, int64(95), status[0])
+	assert.Equal(t, int64(5), status[1])
+	assert.Equal(t, int64(3), status[2])
+}
+
+func TestTorture(t *testing.T) {
+	file, status := fixture()
+	// 0 0 | 100 -1                             [0]: 100
+	file.Update(1, 20, 30, 0)
+	// 0 0 | 20 1 | 50 0 | 130 -1               [0]: 100, [1]: 30
+	file.Update(2, 20, 0, 5)
+	// 0 0 | 20 1 | 45 0 | 125 -1               [0]: 100, [1]: 25
+	file.Update(3, 20, 0, 5)
+	// 0 0 | 20 1 | 40 0 | 120 -1               [0]: 100, [1]: 20
+	file.Update(4, 20, 10, 0)
+	// 0 0 | 20 4 | 30 1 | 50 0 | 130 -1        [0]: 100, [1]: 20, [4]: 10
+	file.Update(5, 45, 0, 10)
+	// 0 0 | 20 4 | 30 1 | 45 0 | 120 -1        [0]: 95, [1]: 15, [4]: 10
+	file.Update(6, 45, 5, 0)
+	// 0 0 | 20 4 | 30 1 | 45 6 | 50 0 | 125 -1 [0]: 95, [1]: 15, [4]: 10, [6]: 5
+	file.Update(7, 10, 0, 50)
+	// 0 0 | 75 -1                              [0]: 75
+	file.Update(8, 0, 10, 10)
+	// 0 8 | 10 0 | 75 -1                       [0]: 65, [8]: 10
+	dump := file.Dump()
+	assert.Equal(t, "0 8\n10 0\n75 -1\n", dump)
+	assert.Equal(t, int64(65), status[0])
+	assert.Equal(t, int64(0), status[1])
+	assert.Equal(t, int64(0), status[2])
+	assert.Equal(t, int64(0), status[3])
+	assert.Equal(t, int64(0), status[4])
+	assert.Equal(t, int64(0), status[5])
+	assert.Equal(t, int64(0), status[6])
+	assert.Equal(t, int64(0), status[7])
+	assert.Equal(t, int64(10), status[8])
+}

+ 17 - 0
labours.py

@@ -0,0 +1,17 @@
+import sys
+
+import matplotlib.pyplot as pyplot
+import numpy
+import seaborn  # to get nice colors, he-he
+
+
+def main():
+    matrix = []
+    for line in sys.stdin.read().split("\n")[:-1]:
+        matrix.append(numpy.fromstring(line, dtype=int, sep=" "))
+    matrix = numpy.array(matrix).T
+    pyplot.stackplot(numpy.arange(matrix.shape[1]), matrix)
+    pyplot.show()
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 704 - 0
rbtree.go

@@ -0,0 +1,704 @@
+//
+// Created by Yaz Saito on 06/10/12.
+//
+
+// A red-black tree with an API similar to C++ STL's.
+//
+// The implementation is inspired (read: stolen) from:
+// http://en.literateprograms.org/Red-black_tree_(C)#chunk use:private function prototypes.
+//
+package hercules
+
+//
+// Public definitions
+//
+
+// Item is the object stored in each tree node.
+type Item struct {
+	key   int
+	value int
+}
+
+type RBTree struct {
+	// Root of the tree
+	root *node
+
+	// The minimum and maximum nodes under the root.
+	minNode, maxNode *node
+
+	// Number of nodes under root, including the root
+	count int
+}
+
+// Return the number of elements in the tree.
+func (root *RBTree) Len() int {
+	return root.count
+}
+
+// A convenience function for finding an element equal to key. Return
+// nil if not found.
+func (root *RBTree) Get(key int) *int {
+	n, exact := root.findGE(key)
+	if exact {
+		return &n.item.value
+	}
+	return nil
+}
+
+// Create an iterator that points to the minimum item in the tree
+// If the tree is empty, return Limit()
+func (root *RBTree) Min() Iterator {
+	return Iterator{root, root.minNode}
+}
+
+// Create an iterator that points at the maximum item in the tree
+//
+// If the tree is empty, return NegativeLimit()
+func (root *RBTree) Max() Iterator {
+	if root.maxNode == nil {
+		return Iterator{root, negativeLimitNode}
+	}
+	return Iterator{root, root.maxNode}
+}
+
+// Create an iterator that points beyond the maximum item in the tree
+func (root *RBTree) Limit() Iterator {
+	return Iterator{root, nil}
+}
+
+// Create an iterator that points before the minimum item in the tree
+func (root *RBTree) NegativeLimit() Iterator {
+	return Iterator{root, negativeLimitNode}
+}
+
+// Find the smallest element N such that N >= key, and return the
+// iterator pointing to the element. If no such element is found,
+// return root.Limit().
+func (root *RBTree) FindGE(key int) Iterator {
+	n, _ := root.findGE(key)
+	return Iterator{root, n}
+}
+
+// Find the largest element N such that N <= key, and return the
+// iterator pointing to the element. If no such element is found,
+// return iter.NegativeLimit().
+func (root *RBTree) FindLE(key int) Iterator {
+	n, exact := root.findGE(key)
+	if exact {
+		return Iterator{root, n}
+	}
+	if n != nil {
+		return Iterator{root, n.doPrev()}
+	}
+	if root.maxNode == nil {
+		return Iterator{root, negativeLimitNode}
+	}
+	return Iterator{root, root.maxNode}
+}
+
+// Insert an item. If the item is already in the tree, do nothing and
+// return false. Else return true.
+func (root *RBTree) Insert(item Item) (bool, Iterator) {
+	// TODO: delay creating n until it is found to be inserted
+	n := root.doInsert(item)
+	if n == nil {
+		return false, Iterator{}
+	}
+	ins_n := n
+
+	n.color = red
+
+	for true {
+		// Case 1: N is at the root
+		if n.parent == nil {
+			n.color = black
+			break
+		}
+
+		// Case 2: The parent is black, so the tree already
+		// satisfies the RB properties
+		if n.parent.color == black {
+			break
+		}
+
+		// Case 3: parent and uncle are both red.
+		// Then paint both black and make grandparent red.
+		grandparent := n.parent.parent
+		var uncle *node
+		if n.parent.isLeftChild() {
+			uncle = grandparent.right
+		} else {
+			uncle = grandparent.left
+		}
+		if uncle != nil && uncle.color == red {
+			n.parent.color = black
+			uncle.color = black
+			grandparent.color = red
+			n = grandparent
+			continue
+		}
+
+		// Case 4: parent is red, uncle is black (1)
+		if n.isRightChild() && n.parent.isLeftChild() {
+			root.rotateLeft(n.parent)
+			n = n.left
+			continue
+		}
+		if n.isLeftChild() && n.parent.isRightChild() {
+			root.rotateRight(n.parent)
+			n = n.right
+			continue
+		}
+
+		// Case 5: parent is read, uncle is black (2)
+		n.parent.color = black
+		grandparent.color = red
+		if n.isLeftChild() {
+			root.rotateRight(grandparent)
+		} else {
+			root.rotateLeft(grandparent)
+		}
+		break
+	}
+	return true, Iterator{root, ins_n}
+}
+
+// Delete an item with the given key. Return true iff the item was
+// found.
+func (root *RBTree) DeleteWithKey(key int) bool {
+	iter := root.FindGE(key)
+	if iter.node != nil {
+		root.DeleteWithIterator(iter)
+		return true
+	}
+	return false
+}
+
+// Delete the current item.
+//
+// REQUIRES: !iter.Limit() && !iter.NegativeLimit()
+func (root *RBTree) DeleteWithIterator(iter Iterator) {
+	doAssert(!iter.Limit() && !iter.NegativeLimit())
+	root.doDelete(iter.node)
+}
+
+// Iterator allows scanning tree elements in sort order.
+//
+// Iterator invalidation rule is the same as C++ std::map<>'s. That
+// is, if you delete the element that an iterator points to, the
+// iterator becomes invalid. For other operation types, the iterator
+// remains valid.
+type Iterator struct {
+	root *RBTree
+	node *node
+}
+
+func (iter Iterator) Equal(iter_ Iterator) bool {
+	return iter.node == iter_.node
+}
+
+// Check if the iterator points beyond the max element in the tree
+func (iter Iterator) Limit() bool {
+	return iter.node == nil
+}
+
+// Check if the iterator points to the minimum element in the tree
+func (iter Iterator) Min() bool {
+	return iter.node == iter.root.minNode
+}
+
+// Check if the iterator points to the maximum element in the tree
+func (iter Iterator) Max() bool {
+	return iter.node == iter.root.maxNode
+}
+
+// Check if the iterator points before the minimum element in the tree
+func (iter Iterator) NegativeLimit() bool {
+	return iter.node == negativeLimitNode
+}
+
+// Return the current element. Allows mutating the node
+// (key to be changed with care!).
+//
+// REQUIRES: !iter.Limit() && !iter.NegativeLimit()
+func (iter Iterator) Item() *Item {
+	return &iter.node.item
+}
+
+// Create a new iterator that points to the successor of the current element.
+//
+// REQUIRES: !iter.Limit()
+func (iter Iterator) Next() Iterator {
+	doAssert(!iter.Limit())
+	if iter.NegativeLimit() {
+		return Iterator{iter.root, iter.root.minNode}
+	}
+	return Iterator{iter.root, iter.node.doNext()}
+}
+
+// Create a new iterator that points to the predecessor of the current
+// node.
+//
+// REQUIRES: !iter.NegativeLimit()
+func (iter Iterator) Prev() Iterator {
+	doAssert(!iter.NegativeLimit())
+	if !iter.Limit() {
+		return Iterator{iter.root, iter.node.doPrev()}
+	}
+	if iter.root.maxNode == nil {
+		return Iterator{iter.root, negativeLimitNode}
+	}
+	return Iterator{iter.root, iter.root.maxNode}
+}
+
+func doAssert(b bool) {
+	if !b {
+		panic("rbtree internal assertion failed")
+	}
+}
+
+const red = iota
+const black = 1 + iota
+
+type node struct {
+	item                Item
+	parent, left, right *node
+	color               int // black or red
+}
+
+var negativeLimitNode *node
+
+//
+// Internal node attribute accessors
+//
+func getColor(n *node) int {
+	if n == nil {
+		return black
+	}
+	return n.color
+}
+
+func (n *node) isLeftChild() bool {
+	return n == n.parent.left
+}
+
+func (n *node) isRightChild() bool {
+	return n == n.parent.right
+}
+
+func (n *node) sibling() *node {
+	doAssert(n.parent != nil)
+	if n.isLeftChild() {
+		return n.parent.right
+	}
+	return n.parent.left
+}
+
+// Return the minimum node that's larger than N. Return nil if no such
+// node is found.
+func (n *node) doNext() *node {
+	if n.right != nil {
+		m := n.right
+		for m.left != nil {
+			m = m.left
+		}
+		return m
+	}
+
+	for n != nil {
+		p := n.parent
+		if p == nil {
+			return nil
+		}
+		if n.isLeftChild() {
+			return p
+		}
+		n = p
+	}
+	return nil
+}
+
+// Return the maximum node that's smaller than N. Return nil if no
+// such node is found.
+func (n *node) doPrev() *node {
+	if n.left != nil {
+		return maxPredecessor(n)
+	}
+
+	for n != nil {
+		p := n.parent
+		if p == nil {
+			break
+		}
+		if n.isRightChild() {
+			return p
+		}
+		n = p
+	}
+	return negativeLimitNode
+}
+
+// Return the predecessor of "n".
+func maxPredecessor(n *node) *node {
+	doAssert(n.left != nil)
+	m := n.left
+	for m.right != nil {
+		m = m.right
+	}
+	return m
+}
+
+//
+// Tree methods
+//
+
+//
+// Private methods
+//
+
+func (root *RBTree) recomputeMinNode() {
+	root.minNode = root.root
+	if root.minNode != nil {
+		for root.minNode.left != nil {
+			root.minNode = root.minNode.left
+		}
+	}
+}
+
+func (root *RBTree) recomputeMaxNode() {
+	root.maxNode = root.root
+	if root.maxNode != nil {
+		for root.maxNode.right != nil {
+			root.maxNode = root.maxNode.right
+		}
+	}
+}
+
+func (root *RBTree) maybeSetMinNode(n *node) {
+	if root.minNode == nil {
+		root.minNode = n
+		root.maxNode = n
+	} else if n.item.key < root.minNode.item.key {
+		root.minNode = n
+	}
+}
+
+func (root *RBTree) maybeSetMaxNode(n *node) {
+	if root.maxNode == nil {
+		root.minNode = n
+		root.maxNode = n
+	} else if n.item.key > root.maxNode.item.key {
+		root.maxNode = n
+	}
+}
+
+// Try inserting "item" into the tree. Return nil if the item is
+// already in the tree. Otherwise return a new (leaf) node.
+func (root *RBTree) doInsert(item Item) *node {
+	if root.root == nil {
+		n := &node{item: item}
+		root.root = n
+		root.minNode = n
+		root.maxNode = n
+		root.count++
+		return n
+	}
+	parent := root.root
+	for true {
+		comp := item.key - parent.item.key
+		if comp == 0 {
+			return nil
+		} else if comp < 0 {
+			if parent.left == nil {
+				n := &node{item: item, parent: parent}
+				parent.left = n
+				root.count++
+				root.maybeSetMinNode(n)
+				return n
+			} else {
+				parent = parent.left
+			}
+		} else {
+			if parent.right == nil {
+				n := &node{item: item, parent: parent}
+				parent.right = n
+				root.count++
+				root.maybeSetMaxNode(n)
+				return n
+			} else {
+				parent = parent.right
+			}
+		}
+	}
+	panic("should not reach here")
+}
+
+// Find a node whose item >= key. The 2nd return value is true iff the
+// node.item==key. Returns (nil, false) if all nodes in the tree are <
+// key.
+func (root *RBTree) findGE(key int) (*node, bool) {
+	n := root.root
+	for true {
+		if n == nil {
+			return nil, false
+		}
+		comp := key - n.item.key
+		if comp == 0 {
+			return n, true
+		} else if comp < 0 {
+			if n.left != nil {
+				n = n.left
+			} else {
+				return n, false
+			}
+		} else {
+			if n.right != nil {
+				n = n.right
+			} else {
+				succ := n.doNext()
+				if succ == nil {
+					return nil, false
+				} else {
+					return succ, (key == succ.item.key)
+				}
+			}
+		}
+	}
+	panic("should not reach here")
+}
+
+// Delete N from the tree.
+func (root *RBTree) doDelete(n *node) {
+	if n.left != nil && n.right != nil {
+		pred := maxPredecessor(n)
+		root.swapNodes(n, pred)
+	}
+
+	doAssert(n.left == nil || n.right == nil)
+	child := n.right
+	if child == nil {
+		child = n.left
+	}
+	if n.color == black {
+		n.color = getColor(child)
+		root.deleteCase1(n)
+	}
+	root.replaceNode(n, child)
+	if n.parent == nil && child != nil {
+		child.color = black
+	}
+	root.count--
+	if root.count == 0 {
+		root.minNode = nil
+		root.maxNode = nil
+	} else {
+		if root.minNode == n {
+			root.recomputeMinNode()
+		}
+		if root.maxNode == n {
+			root.recomputeMaxNode()
+		}
+	}
+}
+
+// Move n to the pred's place, and vice versa
+//
+func (root *RBTree) swapNodes(n, pred *node) {
+	doAssert(pred != n)
+	isLeft := pred.isLeftChild()
+	tmp := *pred
+	root.replaceNode(n, pred)
+	pred.color = n.color
+
+	if tmp.parent == n {
+		// swap the positions of n and pred
+		if isLeft {
+			pred.left = n
+			pred.right = n.right
+			if pred.right != nil {
+				pred.right.parent = pred
+			}
+		} else {
+			pred.left = n.left
+			if pred.left != nil {
+				pred.left.parent = pred
+			}
+			pred.right = n
+		}
+		n.item = tmp.item
+		n.parent = pred
+
+		n.left = tmp.left
+		if n.left != nil {
+			n.left.parent = n
+		}
+		n.right = tmp.right
+		if n.right != nil {
+			n.right.parent = n
+		}
+	} else {
+		pred.left = n.left
+		if pred.left != nil {
+			pred.left.parent = pred
+		}
+		pred.right = n.right
+		if pred.right != nil {
+			pred.right.parent = pred
+		}
+		if isLeft {
+			tmp.parent.left = n
+		} else {
+			tmp.parent.right = n
+		}
+		n.item = tmp.item
+		n.parent = tmp.parent
+		n.left = tmp.left
+		if n.left != nil {
+			n.left.parent = n
+		}
+		n.right = tmp.right
+		if n.right != nil {
+			n.right.parent = n
+		}
+	}
+	n.color = tmp.color
+}
+
+func (root *RBTree) deleteCase1(n *node) {
+	for true {
+		if n.parent != nil {
+			if getColor(n.sibling()) == red {
+				n.parent.color = red
+				n.sibling().color = black
+				if n == n.parent.left {
+					root.rotateLeft(n.parent)
+				} else {
+					root.rotateRight(n.parent)
+				}
+			}
+			if getColor(n.parent) == black &&
+				getColor(n.sibling()) == black &&
+				getColor(n.sibling().left) == black &&
+				getColor(n.sibling().right) == black {
+				n.sibling().color = red
+				n = n.parent
+				continue
+			} else {
+				// case 4
+				if getColor(n.parent) == red &&
+					getColor(n.sibling()) == black &&
+					getColor(n.sibling().left) == black &&
+					getColor(n.sibling().right) == black {
+					n.sibling().color = red
+					n.parent.color = black
+				} else {
+					root.deleteCase5(n)
+				}
+			}
+		}
+		break
+	}
+}
+
+func (root *RBTree) deleteCase5(n *node) {
+	if n == n.parent.left &&
+		getColor(n.sibling()) == black &&
+		getColor(n.sibling().left) == red &&
+		getColor(n.sibling().right) == black {
+		n.sibling().color = red
+		n.sibling().left.color = black
+		root.rotateRight(n.sibling())
+	} else if n == n.parent.right &&
+		getColor(n.sibling()) == black &&
+		getColor(n.sibling().right) == red &&
+		getColor(n.sibling().left) == black {
+		n.sibling().color = red
+		n.sibling().right.color = black
+		root.rotateLeft(n.sibling())
+	}
+
+	// case 6
+	n.sibling().color = getColor(n.parent)
+	n.parent.color = black
+	if n == n.parent.left {
+		doAssert(getColor(n.sibling().right) == red)
+		n.sibling().right.color = black
+		root.rotateLeft(n.parent)
+	} else {
+		doAssert(getColor(n.sibling().left) == red)
+		n.sibling().left.color = black
+		root.rotateRight(n.parent)
+	}
+}
+
+func (root *RBTree) replaceNode(oldn, newn *node) {
+	if oldn.parent == nil {
+		root.root = newn
+	} else {
+		if oldn == oldn.parent.left {
+			oldn.parent.left = newn
+		} else {
+			oldn.parent.right = newn
+		}
+	}
+	if newn != nil {
+		newn.parent = oldn.parent
+	}
+}
+
+/*
+    X		     Y
+  A   Y	    =>     X   C
+     B C 	  A B
+*/
+func (root *RBTree) rotateLeft(x *node) {
+	y := x.right
+	x.right = y.left
+	if y.left != nil {
+		y.left.parent = x
+	}
+	y.parent = x.parent
+	if x.parent == nil {
+		root.root = y
+	} else {
+		if x.isLeftChild() {
+			x.parent.left = y
+		} else {
+			x.parent.right = y
+		}
+	}
+	y.left = x
+	x.parent = y
+}
+
+/*
+     Y           X
+   X   C  =>   A   Y
+  A B             B C
+*/
+func (root *RBTree) rotateRight(y *node) {
+	x := y.left
+
+	// Move "B"
+	y.left = x.right
+	if x.right != nil {
+		x.right.parent = y
+	}
+
+	x.parent = y.parent
+	if y.parent == nil {
+		root.root = x
+	} else {
+		if y.isLeftChild() {
+			y.parent.left = x
+		} else {
+			y.parent.right = x
+		}
+	}
+	x.right = y
+	y.parent = x
+}
+
+func init() {
+	negativeLimitNode = &node{}
+}