Преглед на файлове

Finish the refactoring: universal serialization

Vadim Markovtsev преди 7 години
родител
ревизия
13f18a839d
променени са 11 файла, в които са добавени 610 реда и са изтрити 395 реда
  1. 4 3
      .travis.yml
  2. 86 1
      burndown.go
  3. 33 164
      cmd/hercules/main.go
  4. 141 0
      couples.go
  5. 1 1
      file.go
  6. 1 1
      file_test.go
  7. 202 96
      pb/pb.pb.go
  8. 34 18
      pb/pb.proto
  9. 12 8
      pipeline.go
  10. 7 101
      stdout/utils.go
  11. 89 2
      uast.go

+ 4 - 3
.travis.yml

@@ -15,20 +15,21 @@ go:
   - 1.8
   - 1.9
 
-go_import_path: gopkg.in/src-d/hercules.v2
+go_import_path: gopkg.in/src-d/hercules.v3
 
 before_install:
   - wget https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py --user && rm get-pip.py
   - export PATH=$PATH:~/.local/bin
   - pip3 install --user -r requirements.txt
   - pip3 install --user tensorflow
-  - docker run -d --privileged -p 9432:9432 --name bblfsh bblfsh/server
+  - docker run -d --privileged -p 9432:9432 --name bblfshd bblfsh/bblfshd
+  - docker exec -it bblfshd bblfshctl driver install --all
   - git clone https://github.com/bblfsh/libuast
   - cd libuast && cmake -DCMAKE_BUILD_TYPE=Release . && make && ln -s src libuast && cd ..
   - export CGO_CFLAGS="-I$(pwd)/libuast -I$(pwd)/libuast/libuast" && export CGO_LDFLAGS="-luast -L$(pwd)/libuast/lib -Wl,-rpath -Wl,$(pwd)/libuast/lib"
   
 script:
-  - go test -v -cpu=1,2 -coverprofile=coverage.txt -covermode=count gopkg.in/src-d/hercules.v2
+  - go test -v -cpu=1,2 -coverprofile=coverage.txt -covermode=count gopkg.in/src-d/hercules.v3
   - $GOPATH/bin/hercules -files -people -couples https://github.com/src-d/hercules | python3 labours.py -m all -o out --backend Agg --disable-projector
   - $GOPATH/bin/hercules -files -people -couples -pb https://github.com/src-d/hercules | python3 labours.py -f pb -m all -o out --backend Agg --disable-projector
 

+ 86 - 1
burndown.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"sort"
 	"unicode/utf8"
 
 	"github.com/sergi/go-diff/diffmatchpatch"
@@ -13,6 +14,9 @@ import (
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
 	"gopkg.in/src-d/go-git.v4/utils/merkletrie"
+	"gopkg.in/src-d/hercules.v3/stdout"
+	"gopkg.in/src-d/hercules.v3/pb"
+	"github.com/gogo/protobuf/proto"
 )
 
 // BurndownAnalyser allows to gather the line burndown statistics for a Git repository.
@@ -60,6 +64,8 @@ type BurndownAnalysis struct {
 	// previousDay is the day from the previous sample period -
 	// different from DaysSinceStart.previousDay.
 	previousDay int
+	// references IdentityDetector.ReversedPeopleDict
+	reversedPeopleDict []string
 }
 
 type BurndownResult struct {
@@ -134,6 +140,7 @@ func (analyser *BurndownAnalysis) Configure(facts map[string]interface{}) {
 	if people, _ := facts[ConfigBurndownTrackPeople].(bool); people {
 		if val, exists := facts[FactIdentityDetectorPeopleCount].(int); exists {
 			analyser.PeopleNumber = val
+			analyser.reversedPeopleDict = facts[FactIdentityDetectorReversedPeopleDict].([]string)
 		}
 	}
 	if val, exists := facts[ConfigBurndownDebug].(bool); exists {
@@ -238,7 +245,85 @@ func (analyser *BurndownAnalysis) Finalize() interface{} {
 		GlobalHistory:   analyser.globalHistory,
 		FileHistories:   analyser.fileHistories,
 		PeopleHistories: analyser.peopleHistories,
-		PeopleMatrix:    peopleMatrix}
+		PeopleMatrix:    peopleMatrix,
+	}
+}
+
+func (analyser *BurndownAnalysis) Serialize(result interface{}, binary bool, writer io.Writer) error {
+	burndownResult := result.(BurndownResult)
+	if binary {
+		return analyser.serializeBinary(&burndownResult, writer)
+	}
+	analyser.serializeText(&burndownResult, writer)
+	return nil
+}
+
+func (analyser *BurndownAnalysis) serializeText(result *BurndownResult, writer io.Writer) {
+	fmt.Fprintln(writer, "  granularity:", analyser.Granularity)
+	fmt.Fprintln(writer, "  sampling:", analyser.Sampling)
+	stdout.PrintMatrix(writer, result.GlobalHistory, 2, "project", true)
+	if len(result.FileHistories) > 0 {
+		fmt.Fprintln(writer, "  files:")
+		keys := sortedKeys(result.FileHistories)
+		for _, key := range keys {
+			stdout.PrintMatrix(writer, result.FileHistories[key], 4, key, true)
+		}
+	}
+
+	if len(result.PeopleHistories) > 0 {
+		fmt.Fprintln(writer, "  people_sequence:")
+		for key := range result.PeopleHistories {
+			fmt.Fprintln(writer, "    - " + stdout.SafeString(analyser.reversedPeopleDict[key]))
+		}
+		fmt.Fprintln(writer, "  people:")
+		for key, val := range result.PeopleHistories {
+			stdout.PrintMatrix(writer, val, 4, analyser.reversedPeopleDict[key], true)
+		}
+		fmt.Fprintln(writer, "  people_interaction: |-")
+		stdout.PrintMatrix(writer, result.PeopleMatrix, 4, "", false)
+	}
+}
+
+func (analyser *BurndownAnalysis) serializeBinary(result *BurndownResult, writer io.Writer) error {
+	message := pb.BurndownAnalysisResults{
+		Granularity: int32(analyser.Granularity),
+		Sampling: int32(analyser.Sampling),
+		Project: pb.ToBurndownSparseMatrix(result.GlobalHistory, "project"),
+	}
+	if len(result.FileHistories) > 0 {
+		message.Files = make([]*pb.BurndownSparseMatrix, len(result.FileHistories))
+		keys := sortedKeys(result.FileHistories)
+		i := 0
+		for _, key := range keys {
+			message.Files[i] = pb.ToBurndownSparseMatrix(
+				result.FileHistories[key], key)
+			i++
+		}
+	}
+
+	if len(result.PeopleHistories) > 0 {
+		message.People = make(
+		  []*pb.BurndownSparseMatrix, len(result.PeopleHistories))
+		for key, val := range result.PeopleHistories {
+			message.People[key] = pb.ToBurndownSparseMatrix(val, analyser.reversedPeopleDict[key])
+		}
+		message.PeopleInteraction = pb.DenseToCompressedSparseRowMatrix(result.PeopleMatrix)
+	}
+	serialized, err := proto.Marshal(&message)
+	if err != nil {
+		return err
+	}
+  writer.Write(serialized)
+	return nil
+}
+
+func sortedKeys(m map[string][][]int64) []string {
+	keys := make([]string, 0, len(m))
+	for k := range m {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	return keys
 }
 
 func checkClose(c io.Closer) {

+ 33 - 164
cmd/hercules/main.go

@@ -38,7 +38,6 @@ import (
 	"os"
 	"plugin"
 	"runtime/pprof"
-	"sort"
 	"strings"
 
 	"gopkg.in/src-d/go-billy.v3/osfs"
@@ -47,23 +46,13 @@ import (
 	"gopkg.in/src-d/go-git.v4/storage"
 	"gopkg.in/src-d/go-git.v4/storage/filesystem"
 	"gopkg.in/src-d/go-git.v4/storage/memory"
-	"gopkg.in/src-d/hercules.v2"
-	"gopkg.in/src-d/hercules.v2/stdout"
-	"gopkg.in/src-d/hercules.v2/pb"
+	"gopkg.in/src-d/hercules.v3"
+	"gopkg.in/src-d/hercules.v3/pb"
 	"github.com/vbauerster/mpb"
 	"github.com/vbauerster/mpb/decor"
 	"github.com/gogo/protobuf/proto"
 )
 
-func sortedKeys(m map[string][][]int64) []string {
-	keys := make([]string, 0, len(m))
-	for k := range m {
-		keys = append(keys, k)
-	}
-	sort.Strings(keys)
-	return keys
-}
-
 type OneLineWriter struct {
 	Writer io.Writer
 }
@@ -213,24 +202,6 @@ func main() {
 		}
 	}
 	facts["commits"] = commits
-
-	/*
-	var uastSaver *hercules.UASTChangesSaver
-	if withUasts {
-		pipeline.AddItem(&hercules.UASTExtractor{
-			Endpoint: bblfshEndpoint,
-			Context: func() context.Context {
-				ctx, _ := context.WithTimeout(context.Background(),
-					                            time.Duration(bblfshTimeout) * time.Second)
-				return ctx
-			},
-			PoolSize: uastPoolSize,
-		  Extensions: map[string]bool {"py": true, "java": true}})
-		pipeline.AddItem(&hercules.UASTChanges{})
-		uastSaver = &hercules.UASTChangesSaver{}
-		pipeline.AddItem(uastSaver)
-	}
-	*/
 	deployed := []hercules.PipelineItem{}
 	for name, valPtr := range deployChoices {
 		if *valPtr {
@@ -238,109 +209,44 @@ func main() {
 		}
 	}
 	pipeline.Initialize(facts)
-	result, err := pipeline.Run(commits)
+	results, err := pipeline.Run(commits)
 	if err != nil {
 		panic(err)
 	}
 	progress.Stop()
-	fmt.Fprint(os.Stderr, "writing...    \r")
-	_ = result
-	/*
-	burndownResults := result[burndowner].(hercules.BurndownResult)
-	if len(burndownResults.GlobalHistory) == 0 {
-		return
-	}
-	var couplesResult hercules.CouplesResult
-	if withCouples {
-		couplesResult = result[coupler].(hercules.CouplesResult)
-	}
-	*/
-	/*
-	if withUasts {
-		changedUasts := result[uastSaver].([][]hercules.UASTChange)
-		for i, changes := range changedUasts {
-			for j, change := range changes {
-				if change.Before == nil || change.After == nil {
-					continue
-				}
-				bs, _ := change.Before.Marshal()
-				ioutil.WriteFile(fmt.Sprintf(
-					"%d_%d_before_%s.pb", i, j, change.Change.From.TreeEntry.Hash.String()), bs, 0666)
-				blob, _ := repository.BlobObject(change.Change.From.TreeEntry.Hash)
-				s, _ := (&object.File{Blob: *blob}).Contents()
-				ioutil.WriteFile(fmt.Sprintf(
-					"%d_%d_before_%s.src", i, j, change.Change.From.TreeEntry.Hash.String()), []byte(s), 0666)
-				bs, _ = change.After.Marshal()
-				ioutil.WriteFile(fmt.Sprintf(
-					"%d_%d_after_%s.pb", i, j, change.Change.To.TreeEntry.Hash.String()), bs, 0666)
-				blob, _ = repository.BlobObject(change.Change.To.TreeEntry.Hash)
-				s, _ = (&object.File{Blob: *blob}).Contents()
-				ioutil.WriteFile(fmt.Sprintf(
-					"%d_%d_after_%s.src", i, j, change.Change.To.TreeEntry.Hash.String()), []byte(s), 0666)
-			}
-		}
-	}
-	*/
-	/*
-	reversedPeopleDict := facts[hercules.FactIdentityDetectorReversedPeopleDict].([]string)
+	fmt.Fprint(os.Stderr, "writing...\r")
 	begin := commits[0].Author.When.Unix()
 	end := commits[len(commits)-1].Author.When.Unix()
 	if !protobuf {
-		printResults(uri, begin, end, granularity, sampling,
-			withFiles, withPeople, withCouples,
-			burndownResults, couplesResult, reversedPeopleDict)
+		printResults(uri, begin, end, deployed, results)
 	} else {
-		serializeResults(uri, begin, end, granularity, sampling,
-			withFiles, withPeople, withCouples,
-			burndownResults, couplesResult, reversedPeopleDict)
-	}*/
+		protobufResults(uri, begin, end, deployed, results)
+	}
 }
 
 func printResults(
-	uri string, begin, end int64, granularity, sampling int,
-	withFiles, withPeople, withCouples bool,
-	burndownResults hercules.BurndownResult,
-	couplesResult hercules.CouplesResult,
-	reversePeopleDict []string) {
-
-	fmt.Println("burndown:")
-	fmt.Println("  version: 1")
-	fmt.Println("  begin:", begin)
-	fmt.Println("  end:", end)
-	fmt.Println("  granularity:", granularity)
-	fmt.Println("  sampling:", sampling)
-	fmt.Println("project:")
-	stdout.PrintMatrix(burndownResults.GlobalHistory, uri, true)
-	if withFiles {
-		fmt.Println("files:")
-		keys := sortedKeys(burndownResults.FileHistories)
-		for _, key := range keys {
-			stdout.PrintMatrix(burndownResults.FileHistories[key], key, true)
-		}
-	}
-	if withPeople {
-		fmt.Println("people_sequence:")
-		for key := range burndownResults.PeopleHistories {
-			fmt.Println("  - " + stdout.SafeString(reversePeopleDict[key]))
-		}
-		fmt.Println("people:")
-		for key, val := range burndownResults.PeopleHistories {
-			stdout.PrintMatrix(val, reversePeopleDict[key], true)
+	uri string, begin, end int64, deployed []hercules.PipelineItem,
+	results map[hercules.PipelineItem]interface{}) {
+	fmt.Println("hercules:")
+	fmt.Println("  version: 3")
+	fmt.Println("  cmdline:", strings.Join(os.Args, " "))
+	fmt.Println("  repository:", uri)
+	fmt.Println("  begin_unix_time:", begin)
+	fmt.Println("  end_unix_time:", end)
+
+	for _, item := range deployed {
+		result := results[item]
+		fmt.Printf("%s:\n", item.Name())
+		err := interface{}(item).(hercules.LeafPipelineItem).Serialize(result, false, os.Stdout)
+		if err != nil {
+			panic(err)
 		}
-		fmt.Println("people_interaction: |-")
-		stdout.PrintMatrix(burndownResults.PeopleMatrix, "", false)
-	}
-	if withCouples {
-		stdout.PrintCouples(&couplesResult, reversePeopleDict)
 	}
 }
 
-func serializeResults(
-	uri string, begin, end int64, granularity, sampling int,
-	withFiles, withPeople, withCouples bool,
-	burndownResults hercules.BurndownResult,
-	couplesResult hercules.CouplesResult,
-	reversePeopleDict []string) {
+func protobufResults(
+	uri string, begin, end int64, deployed []hercules.PipelineItem,
+	results map[hercules.PipelineItem]interface{}) {
 
   header := pb.Metadata{
 	  Version: 1,
@@ -348,58 +254,21 @@ func serializeResults(
 	  Repository: uri,
     BeginUnixTime: begin,
 	  EndUnixTime: end,
-	  Granularity: int32(granularity),
-	  Sampling: int32(sampling),
   }
 
 	message := pb.AnalysisResults{
 		Header: &header,
-		BurndownProject: pb.ToBurndownSparseMatrix(burndownResults.GlobalHistory, uri),
-	}
-
-	if withFiles {
-		message.BurndownFiles = make([]*pb.BurndownSparseMatrix, len(burndownResults.FileHistories))
-		keys := sortedKeys(burndownResults.FileHistories)
-		i := 0
-		for _, key := range keys {
-			message.BurndownFiles[i] = pb.ToBurndownSparseMatrix(
-				burndownResults.FileHistories[key], key)
-			i++
-		}
-	}
-
-	if withPeople {
-		message.BurndownDevelopers = make(
-		  []*pb.BurndownSparseMatrix, len(burndownResults.PeopleHistories))
-		for key, val := range burndownResults.PeopleHistories {
-			message.BurndownDevelopers[key] = pb.ToBurndownSparseMatrix(val, reversePeopleDict[key])
-		}
-		message.DevelopersInteraction = pb.DenseToCompressedSparseRowMatrix(
-			burndownResults.PeopleMatrix)
+		Contents: map[string][]byte{},
 	}
 
-	if withCouples {
-		message.FileCouples = &pb.Couples{
-			Index: couplesResult.Files,
-			Matrix: pb.MapToCompressedSparseRowMatrix(couplesResult.FilesMatrix),
-		}
-		message.DeveloperCouples = &pb.Couples{
-			Index: reversePeopleDict,
-			Matrix: pb.MapToCompressedSparseRowMatrix(couplesResult.PeopleMatrix),
-		}
-		message.TouchedFiles = &pb.DeveloperTouchedFiles{
-      Developers: make([]*pb.TouchedFiles, len(reversePeopleDict)),
-		}
-		for key := range reversePeopleDict {
-			files := couplesResult.PeopleFiles[key]
-			int32Files := make([]int32, len(files))
-			for i, f := range files {
-				int32Files[i] = int32(f)
-			}
-			message.TouchedFiles.Developers[key] = &pb.TouchedFiles{
-				Files: int32Files,
-			}
+	for _, item := range deployed {
+		result := results[item]
+		buffer := &bytes.Buffer{}
+		err := interface{}(item).(hercules.LeafPipelineItem).Serialize(result, true, buffer)
+		if err != nil {
+			panic(err)
 		}
+		message.Contents[item.Name()] = buffer.Bytes()
 	}
 
 	serialized, err := proto.Marshal(&message)

+ 141 - 0
couples.go

@@ -1,11 +1,16 @@
 package hercules
 
 import (
+	"fmt"
+	"io"
 	"sort"
 
 	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
 	"gopkg.in/src-d/go-git.v4/utils/merkletrie"
+	"gopkg.in/src-d/hercules.v3/stdout"
+	"gopkg.in/src-d/hercules.v3/pb"
+	"github.com/gogo/protobuf/proto"
 )
 
 type Couples struct {
@@ -18,6 +23,8 @@ type Couples struct {
 	people_commits []int
 	// files store every file occurred in the same commit with every other file.
 	files map[string]map[string]int
+	// references IdentityDetector.ReversedPeopleDict
+	reversedPeopleDict []string
 }
 
 type CouplesResult struct {
@@ -47,6 +54,7 @@ func (couples *Couples) ListConfigurationOptions() []ConfigurationOption {
 func (couples *Couples) Configure(facts map[string]interface{}) {
 	if val, exists := facts[FactIdentityDetectorPeopleCount].(int); exists {
 		couples.PeopleNumber = val
+		couples.reversedPeopleDict = facts[FactIdentityDetectorReversedPeopleDict].([]string)
 	}
 }
 
@@ -176,6 +184,139 @@ func (couples *Couples) Finalize() interface{} {
 		Files: filesSequence, FilesMatrix: filesMatrix}
 }
 
+func (couples *Couples) Serialize(result interface{}, binary bool, writer io.Writer) error {
+	couplesResult := result.(CouplesResult)
+	if binary {
+		return couples.serializeBinary(&couplesResult, writer)
+	}
+	couples.serializeText(&couplesResult, writer)
+	return nil
+}
+
+func (couples *Couples) serializeText(result *CouplesResult, writer io.Writer) {
+	fmt.Fprintln(writer, "  files_coocc:")
+	fmt.Fprintln(writer, "    index:")
+	for _, file := range result.Files {
+		fmt.Fprintf(writer, "      - %s\n", stdout.SafeString(file))
+	}
+
+	fmt.Fprintln(writer, "    matrix:")
+	for _, files := range result.FilesMatrix {
+		fmt.Fprint(writer, "      - {")
+		indices := []int{}
+		for file := range files {
+			indices = append(indices, file)
+		}
+		sort.Ints(indices)
+		for i, file := range indices {
+			fmt.Fprintf(writer, "%d: %d", file, files[file])
+			if i < len(indices)-1 {
+				fmt.Fprint(writer, ", ")
+			}
+		}
+		fmt.Fprintln(writer, "}")
+	}
+
+	fmt.Fprintln(writer, "  people_coocc:")
+	fmt.Fprintln(writer, "    index:")
+	for _, person := range couples.reversedPeopleDict {
+		fmt.Fprintf(writer, "      - %s\n", stdout.SafeString(person))
+	}
+
+	fmt.Fprintln(writer, "    matrix:")
+	for _, people := range result.PeopleMatrix {
+		fmt.Fprint(writer, "      - {")
+		indices := []int{}
+		for file := range people {
+			indices = append(indices, file)
+		}
+		sort.Ints(indices)
+		for i, person := range indices {
+			fmt.Fprintf(writer, "%d: %d", person, people[person])
+			if i < len(indices)-1 {
+				fmt.Fprint(writer, ", ")
+			}
+		}
+		fmt.Fprintln(writer, "}")
+	}
+
+	fmt.Fprintln(writer, "    author_files:") // sorted by number of files each author changed
+	peopleFiles := sortByNumberOfFiles(result.PeopleFiles, couples.reversedPeopleDict, result.Files)
+	for _, authorFiles := range peopleFiles {
+		fmt.Fprintf(writer, "      - %s:\n", stdout.SafeString(authorFiles.Author))
+		sort.Strings(authorFiles.Files)
+		for _, file := range authorFiles.Files {
+			fmt.Fprintf(writer, "        - %s\n", stdout.SafeString(file)) // sorted by path
+		}
+	}
+}
+
+func sortByNumberOfFiles(
+	peopleFiles [][]int, peopleDict []string, filesDict []string) authorFilesList {
+	var pfl authorFilesList
+	for peopleIdx, files := range peopleFiles {
+		if peopleIdx < len(peopleDict) {
+			fileNames := make([]string, len(files))
+			for i, fi := range files {
+				fileNames[i] = filesDict[fi]
+			}
+			pfl = append(pfl, authorFiles{peopleDict[peopleIdx], fileNames})
+		}
+	}
+	sort.Sort(pfl)
+	return pfl
+}
+
+type authorFiles struct {
+	Author string
+	Files  []string
+}
+
+type authorFilesList []authorFiles
+
+func (s authorFilesList) Len() int {
+	return len(s)
+}
+func (s authorFilesList) Swap(i, j int) {
+	s[i], s[j] = s[j], s[i]
+}
+func (s authorFilesList) Less(i, j int) bool {
+	return len(s[i].Files) < len(s[j].Files)
+}
+
+func (couples *Couples) serializeBinary(result *CouplesResult, writer io.Writer) error {
+	message := pb.CouplesResults{}
+
+	message.FileCouples = &pb.Couples{
+		Index: result.Files,
+		Matrix: pb.MapToCompressedSparseRowMatrix(result.FilesMatrix),
+	}
+	message.DeveloperCouples = &pb.Couples{
+		Index: couples.reversedPeopleDict,
+		Matrix: pb.MapToCompressedSparseRowMatrix(result.PeopleMatrix),
+	}
+	message.TouchedFiles = &pb.DeveloperTouchedFiles{
+    Developers: make([]*pb.TouchedFiles, len(couples.reversedPeopleDict)),
+	}
+	for key := range couples.reversedPeopleDict {
+		files := result.PeopleFiles[key]
+		int32Files := make([]int32, len(files))
+		for i, f := range files {
+			int32Files[i] = int32(f)
+		}
+		message.TouchedFiles.Developers[key] = &pb.TouchedFiles{
+			Files: int32Files,
+		}
+	}
+
+	serialized, err := proto.Marshal(&message)
+	if err != nil {
+		return err
+	}
+  writer.Write(serialized)
+	return nil
+}
+
 func init() {
 	Registry.Register(&Couples{})
 }

+ 1 - 1
file.go

@@ -2,7 +2,7 @@ package hercules
 
 import (
 	"fmt"
-	"gopkg.in/src-d/hercules.v2/rbtree"
+	"gopkg.in/src-d/hercules.v3/rbtree"
 )
 
 // A status is the something we would like to update during File.Update().

+ 1 - 1
file_test.go

@@ -4,7 +4,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"gopkg.in/src-d/hercules.v2/rbtree"
+	"gopkg.in/src-d/hercules.v3/rbtree"
 )
 
 func updateStatusFile(

+ 202 - 96
pb/pb.pb.go

@@ -11,10 +11,14 @@ It has these top-level messages:
 	Metadata
 	BurndownSparseMatrixRow
 	BurndownSparseMatrix
+	BurndownAnalysisResults
 	CompressedSparseRowMatrix
 	Couples
 	TouchedFiles
 	DeveloperTouchedFiles
+	CouplesResults
+	UASTChange
+	UASTChangesSaverResults
 	AnalysisResults
 */
 package pb
@@ -45,10 +49,6 @@ type Metadata struct {
 	BeginUnixTime int64 `protobuf:"varint,4,opt,name=begin_unix_time,json=beginUnixTime,proto3" json:"begin_unix_time,omitempty"`
 	// timestamp of the last analysed commit
 	EndUnixTime int64 `protobuf:"varint,5,opt,name=end_unix_time,json=endUnixTime,proto3" json:"end_unix_time,omitempty"`
-	// how many days are in each band [burndown_project, burndown_file, burndown_developer]
-	Granularity int32 `protobuf:"varint,6,opt,name=granularity,proto3" json:"granularity,omitempty"`
-	// how frequently we measure the state of each band [burndown_project, burndown_file, burndown_developer]
-	Sampling int32 `protobuf:"varint,7,opt,name=sampling,proto3" json:"sampling,omitempty"`
 }
 
 func (m *Metadata) Reset()                    { *m = Metadata{} }
@@ -91,20 +91,6 @@ func (m *Metadata) GetEndUnixTime() int64 {
 	return 0
 }
 
-func (m *Metadata) GetGranularity() int32 {
-	if m != nil {
-		return m.Granularity
-	}
-	return 0
-}
-
-func (m *Metadata) GetSampling() int32 {
-	if m != nil {
-		return m.Sampling
-	}
-	return 0
-}
-
 type BurndownSparseMatrixRow struct {
 	// the first `len(column)` elements are stored,
 	// the rest `number_of_columns - len(column)` values are zeros
@@ -164,6 +150,68 @@ func (m *BurndownSparseMatrix) GetRows() []*BurndownSparseMatrixRow {
 	return nil
 }
 
+type BurndownAnalysisResults struct {
+	// how many days are in each band [burndown_project, burndown_file, burndown_developer]
+	Granularity int32 `protobuf:"varint,1,opt,name=granularity,proto3" json:"granularity,omitempty"`
+	// how frequently we measure the state of each band [burndown_project, burndown_file, burndown_developer]
+	Sampling int32 `protobuf:"varint,2,opt,name=sampling,proto3" json:"sampling,omitempty"`
+	// always exists
+	Project *BurndownSparseMatrix `protobuf:"bytes,3,opt,name=project" json:"project,omitempty"`
+	// this is included if `-burndown-files` was specified
+	Files []*BurndownSparseMatrix `protobuf:"bytes,4,rep,name=files" json:"files,omitempty"`
+	// these two are included if `-burndown-people` was specified
+	People []*BurndownSparseMatrix `protobuf:"bytes,5,rep,name=people" json:"people,omitempty"`
+	// rows and cols order correspond to `burndown_developer`
+	PeopleInteraction *CompressedSparseRowMatrix `protobuf:"bytes,6,opt,name=people_interaction,json=peopleInteraction" json:"people_interaction,omitempty"`
+}
+
+func (m *BurndownAnalysisResults) Reset()                    { *m = BurndownAnalysisResults{} }
+func (m *BurndownAnalysisResults) String() string            { return proto.CompactTextString(m) }
+func (*BurndownAnalysisResults) ProtoMessage()               {}
+func (*BurndownAnalysisResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{3} }
+
+func (m *BurndownAnalysisResults) GetGranularity() int32 {
+	if m != nil {
+		return m.Granularity
+	}
+	return 0
+}
+
+func (m *BurndownAnalysisResults) GetSampling() int32 {
+	if m != nil {
+		return m.Sampling
+	}
+	return 0
+}
+
+func (m *BurndownAnalysisResults) GetProject() *BurndownSparseMatrix {
+	if m != nil {
+		return m.Project
+	}
+	return nil
+}
+
+func (m *BurndownAnalysisResults) GetFiles() []*BurndownSparseMatrix {
+	if m != nil {
+		return m.Files
+	}
+	return nil
+}
+
+func (m *BurndownAnalysisResults) GetPeople() []*BurndownSparseMatrix {
+	if m != nil {
+		return m.People
+	}
+	return nil
+}
+
+func (m *BurndownAnalysisResults) GetPeopleInteraction() *CompressedSparseRowMatrix {
+	if m != nil {
+		return m.PeopleInteraction
+	}
+	return nil
+}
+
 type CompressedSparseRowMatrix struct {
 	NumberOfRows    int32 `protobuf:"varint,1,opt,name=number_of_rows,json=numberOfRows,proto3" json:"number_of_rows,omitempty"`
 	NumberOfColumns int32 `protobuf:"varint,2,opt,name=number_of_columns,json=numberOfColumns,proto3" json:"number_of_columns,omitempty"`
@@ -176,7 +224,7 @@ type CompressedSparseRowMatrix struct {
 func (m *CompressedSparseRowMatrix) Reset()                    { *m = CompressedSparseRowMatrix{} }
 func (m *CompressedSparseRowMatrix) String() string            { return proto.CompactTextString(m) }
 func (*CompressedSparseRowMatrix) ProtoMessage()               {}
-func (*CompressedSparseRowMatrix) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{3} }
+func (*CompressedSparseRowMatrix) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{4} }
 
 func (m *CompressedSparseRowMatrix) GetNumberOfRows() int32 {
 	if m != nil {
@@ -223,7 +271,7 @@ type Couples struct {
 func (m *Couples) Reset()                    { *m = Couples{} }
 func (m *Couples) String() string            { return proto.CompactTextString(m) }
 func (*Couples) ProtoMessage()               {}
-func (*Couples) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{4} }
+func (*Couples) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{5} }
 
 func (m *Couples) GetIndex() []string {
 	if m != nil {
@@ -246,7 +294,7 @@ type TouchedFiles struct {
 func (m *TouchedFiles) Reset()                    { *m = TouchedFiles{} }
 func (m *TouchedFiles) String() string            { return proto.CompactTextString(m) }
 func (*TouchedFiles) ProtoMessage()               {}
-func (*TouchedFiles) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{5} }
+func (*TouchedFiles) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{6} }
 
 func (m *TouchedFiles) GetFiles() []int32 {
 	if m != nil {
@@ -263,7 +311,7 @@ type DeveloperTouchedFiles struct {
 func (m *DeveloperTouchedFiles) Reset()                    { *m = DeveloperTouchedFiles{} }
 func (m *DeveloperTouchedFiles) String() string            { return proto.CompactTextString(m) }
 func (*DeveloperTouchedFiles) ProtoMessage()               {}
-func (*DeveloperTouchedFiles) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{6} }
+func (*DeveloperTouchedFiles) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{7} }
 
 func (m *DeveloperTouchedFiles) GetDevelopers() []*TouchedFiles {
 	if m != nil {
@@ -272,79 +320,123 @@ func (m *DeveloperTouchedFiles) GetDevelopers() []*TouchedFiles {
 	return nil
 }
 
-type AnalysisResults struct {
-	// these two are always included
-	Header          *Metadata             `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
-	BurndownProject *BurndownSparseMatrix `protobuf:"bytes,2,opt,name=burndown_project,json=burndownProject" json:"burndown_project,omitempty"`
-	// this is included if `-files` was specified
-	BurndownFiles []*BurndownSparseMatrix `protobuf:"bytes,3,rep,name=burndown_files,json=burndownFiles" json:"burndown_files,omitempty"`
-	// these two are included if `-people` was specified
-	BurndownDevelopers []*BurndownSparseMatrix `protobuf:"bytes,4,rep,name=burndown_developers,json=burndownDevelopers" json:"burndown_developers,omitempty"`
-	// rows and cols order correspond to `burndown_developer`
-	DevelopersInteraction *CompressedSparseRowMatrix `protobuf:"bytes,5,opt,name=developers_interaction,json=developersInteraction" json:"developers_interaction,omitempty"`
-	// these three are included if `-couples` was specified
+type CouplesResults struct {
 	FileCouples      *Couples               `protobuf:"bytes,6,opt,name=file_couples,json=fileCouples" json:"file_couples,omitempty"`
 	DeveloperCouples *Couples               `protobuf:"bytes,7,opt,name=developer_couples,json=developerCouples" json:"developer_couples,omitempty"`
 	TouchedFiles     *DeveloperTouchedFiles `protobuf:"bytes,8,opt,name=touched_files,json=touchedFiles" json:"touched_files,omitempty"`
 }
 
-func (m *AnalysisResults) Reset()                    { *m = AnalysisResults{} }
-func (m *AnalysisResults) String() string            { return proto.CompactTextString(m) }
-func (*AnalysisResults) ProtoMessage()               {}
-func (*AnalysisResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{7} }
+func (m *CouplesResults) Reset()                    { *m = CouplesResults{} }
+func (m *CouplesResults) String() string            { return proto.CompactTextString(m) }
+func (*CouplesResults) ProtoMessage()               {}
+func (*CouplesResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{8} }
 
-func (m *AnalysisResults) GetHeader() *Metadata {
+func (m *CouplesResults) GetFileCouples() *Couples {
 	if m != nil {
-		return m.Header
+		return m.FileCouples
 	}
 	return nil
 }
 
-func (m *AnalysisResults) GetBurndownProject() *BurndownSparseMatrix {
+func (m *CouplesResults) GetDeveloperCouples() *Couples {
 	if m != nil {
-		return m.BurndownProject
+		return m.DeveloperCouples
 	}
 	return nil
 }
 
-func (m *AnalysisResults) GetBurndownFiles() []*BurndownSparseMatrix {
+func (m *CouplesResults) GetTouchedFiles() *DeveloperTouchedFiles {
 	if m != nil {
-		return m.BurndownFiles
+		return m.TouchedFiles
 	}
 	return nil
 }
 
-func (m *AnalysisResults) GetBurndownDevelopers() []*BurndownSparseMatrix {
+type UASTChange struct {
+	FileName   string `protobuf:"bytes,1,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"`
+	SrcBefore  string `protobuf:"bytes,2,opt,name=src_before,json=srcBefore,proto3" json:"src_before,omitempty"`
+	SrcAfter   string `protobuf:"bytes,3,opt,name=src_after,json=srcAfter,proto3" json:"src_after,omitempty"`
+	UastBefore string `protobuf:"bytes,4,opt,name=uast_before,json=uastBefore,proto3" json:"uast_before,omitempty"`
+	UastAfter  string `protobuf:"bytes,5,opt,name=uast_after,json=uastAfter,proto3" json:"uast_after,omitempty"`
+}
+
+func (m *UASTChange) Reset()                    { *m = UASTChange{} }
+func (m *UASTChange) String() string            { return proto.CompactTextString(m) }
+func (*UASTChange) ProtoMessage()               {}
+func (*UASTChange) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{9} }
+
+func (m *UASTChange) GetFileName() string {
 	if m != nil {
-		return m.BurndownDevelopers
+		return m.FileName
 	}
-	return nil
+	return ""
 }
 
-func (m *AnalysisResults) GetDevelopersInteraction() *CompressedSparseRowMatrix {
+func (m *UASTChange) GetSrcBefore() string {
 	if m != nil {
-		return m.DevelopersInteraction
+		return m.SrcBefore
 	}
-	return nil
+	return ""
 }
 
-func (m *AnalysisResults) GetFileCouples() *Couples {
+func (m *UASTChange) GetSrcAfter() string {
 	if m != nil {
-		return m.FileCouples
+		return m.SrcAfter
+	}
+	return ""
+}
+
+func (m *UASTChange) GetUastBefore() string {
+	if m != nil {
+		return m.UastBefore
+	}
+	return ""
+}
+
+func (m *UASTChange) GetUastAfter() string {
+	if m != nil {
+		return m.UastAfter
+	}
+	return ""
+}
+
+type UASTChangesSaverResults struct {
+	Changes []*UASTChange `protobuf:"bytes,1,rep,name=changes" json:"changes,omitempty"`
+}
+
+func (m *UASTChangesSaverResults) Reset()                    { *m = UASTChangesSaverResults{} }
+func (m *UASTChangesSaverResults) String() string            { return proto.CompactTextString(m) }
+func (*UASTChangesSaverResults) ProtoMessage()               {}
+func (*UASTChangesSaverResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{10} }
+
+func (m *UASTChangesSaverResults) GetChanges() []*UASTChange {
+	if m != nil {
+		return m.Changes
 	}
 	return nil
 }
 
-func (m *AnalysisResults) GetDeveloperCouples() *Couples {
+type AnalysisResults struct {
+	Header *Metadata `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
+	// the mapped values are dynamic messages which require the second parsing pass.
+	Contents map[string][]byte `protobuf:"bytes,2,rep,name=contents" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+}
+
+func (m *AnalysisResults) Reset()                    { *m = AnalysisResults{} }
+func (m *AnalysisResults) String() string            { return proto.CompactTextString(m) }
+func (*AnalysisResults) ProtoMessage()               {}
+func (*AnalysisResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{11} }
+
+func (m *AnalysisResults) GetHeader() *Metadata {
 	if m != nil {
-		return m.DeveloperCouples
+		return m.Header
 	}
 	return nil
 }
 
-func (m *AnalysisResults) GetTouchedFiles() *DeveloperTouchedFiles {
+func (m *AnalysisResults) GetContents() map[string][]byte {
 	if m != nil {
-		return m.TouchedFiles
+		return m.Contents
 	}
 	return nil
 }
@@ -353,55 +445,69 @@ func init() {
 	proto.RegisterType((*Metadata)(nil), "Metadata")
 	proto.RegisterType((*BurndownSparseMatrixRow)(nil), "BurndownSparseMatrixRow")
 	proto.RegisterType((*BurndownSparseMatrix)(nil), "BurndownSparseMatrix")
+	proto.RegisterType((*BurndownAnalysisResults)(nil), "BurndownAnalysisResults")
 	proto.RegisterType((*CompressedSparseRowMatrix)(nil), "CompressedSparseRowMatrix")
 	proto.RegisterType((*Couples)(nil), "Couples")
 	proto.RegisterType((*TouchedFiles)(nil), "TouchedFiles")
 	proto.RegisterType((*DeveloperTouchedFiles)(nil), "DeveloperTouchedFiles")
+	proto.RegisterType((*CouplesResults)(nil), "CouplesResults")
+	proto.RegisterType((*UASTChange)(nil), "UASTChange")
+	proto.RegisterType((*UASTChangesSaverResults)(nil), "UASTChangesSaverResults")
 	proto.RegisterType((*AnalysisResults)(nil), "AnalysisResults")
 }
 
 func init() { proto.RegisterFile("pb/pb.proto", fileDescriptorPb) }
 
 var fileDescriptorPb = []byte{
-	// 634 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x4f, 0x6f, 0xd3, 0x4e,
-	0x10, 0x95, 0x6b, 0x3b, 0x7f, 0xc6, 0x49, 0xd3, 0xee, 0xaf, 0xed, 0xcf, 0xf4, 0x80, 0x8c, 0x55,
-	0xa1, 0x88, 0x3f, 0x46, 0x4a, 0xc5, 0x09, 0x0e, 0x40, 0x51, 0x25, 0x0e, 0x15, 0xb0, 0x2d, 0x67,
-	0xcb, 0x89, 0xb7, 0xed, 0x22, 0x7b, 0xd7, 0xda, 0x5d, 0x37, 0xe9, 0x57, 0xe2, 0xca, 0x97, 0xe2,
-	0xc8, 0x47, 0x40, 0x5e, 0x7b, 0x1d, 0x03, 0x29, 0x70, 0xf3, 0x9b, 0x79, 0x6f, 0x3c, 0x6f, 0x66,
-	0x6c, 0xf0, 0x8a, 0xf9, 0xb3, 0x62, 0x1e, 0x15, 0x82, 0x2b, 0x1e, 0x7e, 0xb3, 0x60, 0x70, 0x46,
-	0x54, 0x92, 0x26, 0x2a, 0x41, 0x3e, 0xf4, 0x6f, 0x88, 0x90, 0x94, 0x33, 0xdf, 0x0a, 0xac, 0xa9,
-	0x8b, 0x0d, 0xac, 0x32, 0x8b, 0x3c, 0xcd, 0x28, 0x23, 0xfe, 0x56, 0x60, 0x4d, 0x87, 0xd8, 0x40,
-	0x74, 0x1f, 0x40, 0x90, 0x82, 0x4b, 0xaa, 0xb8, 0xb8, 0xf5, 0x6d, 0x9d, 0xec, 0x44, 0xd0, 0x43,
-	0x98, 0xcc, 0xc9, 0x15, 0x65, 0x71, 0xc9, 0xe8, 0x2a, 0x56, 0x34, 0x27, 0xbe, 0x13, 0x58, 0x53,
-	0x1b, 0x8f, 0x75, 0xf8, 0x13, 0xa3, 0xab, 0x0b, 0x9a, 0x13, 0x14, 0xc2, 0x98, 0xb0, 0xb4, 0xc3,
-	0x72, 0x35, 0xcb, 0x23, 0x2c, 0x6d, 0x39, 0x01, 0x78, 0x57, 0x22, 0x61, 0x65, 0x96, 0x08, 0xaa,
-	0x6e, 0xfd, 0x9e, 0xee, 0xb1, 0x1b, 0x42, 0x87, 0x30, 0x90, 0x49, 0x5e, 0x64, 0x94, 0x5d, 0xf9,
-	0x7d, 0x9d, 0x6e, 0x71, 0x78, 0x0c, 0xff, 0xbf, 0x29, 0x05, 0x4b, 0xf9, 0x92, 0x9d, 0x17, 0x89,
-	0x90, 0xe4, 0x2c, 0x51, 0x82, 0xae, 0x30, 0x5f, 0x6a, 0x7b, 0x3c, 0x2b, 0x73, 0x26, 0x7d, 0x2b,
-	0xb0, 0xa7, 0x63, 0x6c, 0x60, 0xf8, 0xc5, 0x82, 0xbd, 0x4d, 0x2a, 0x84, 0xc0, 0x61, 0x49, 0x4e,
-	0xf4, 0xa0, 0x86, 0x58, 0x3f, 0xa3, 0x23, 0xd8, 0x66, 0x65, 0x3e, 0x27, 0x22, 0xe6, 0x97, 0xb1,
-	0xe0, 0x4b, 0xa9, 0x87, 0xe5, 0xe2, 0x51, 0x1d, 0x7d, 0x7f, 0x89, 0xf9, 0x52, 0xa2, 0x47, 0xb0,
-	0xbb, 0x66, 0x99, 0xd7, 0xda, 0x9a, 0x38, 0x31, 0xc4, 0x93, 0x3a, 0x8c, 0x9e, 0x80, 0xa3, 0xeb,
-	0x38, 0x81, 0x3d, 0xf5, 0x66, 0x7e, 0x74, 0x87, 0x01, 0xac, 0x59, 0xe1, 0x57, 0x0b, 0xee, 0x9d,
-	0xf0, 0xbc, 0x10, 0x44, 0x4a, 0x92, 0xd6, 0x1c, 0xcc, 0x97, 0x4d, 0xc7, 0xbf, 0x77, 0x67, 0xfd,
-	0x6b, 0x77, 0x5b, 0x9b, 0xbb, 0x43, 0xe0, 0x54, 0x77, 0xe3, 0xdb, 0x81, 0x3d, 0xb5, 0xb1, 0x63,
-	0x6e, 0x88, 0xb2, 0x94, 0x2e, 0x48, 0xdd, 0xb4, 0x8b, 0x0d, 0x44, 0x07, 0xd0, 0xa3, 0x2c, 0x2d,
-	0x94, 0xf0, 0x5d, 0xcd, 0x6f, 0x50, 0x78, 0x0e, 0xfd, 0x13, 0x5e, 0x16, 0x19, 0x91, 0x68, 0x0f,
-	0x5c, 0xca, 0x52, 0xb2, 0xd2, 0x5b, 0x18, 0xe2, 0x1a, 0xa0, 0x19, 0xf4, 0x72, 0x6d, 0x41, 0xf7,
-	0xe1, 0xcd, 0x0e, 0xa3, 0x3b, 0x4d, 0xe2, 0x86, 0x19, 0x1e, 0xc1, 0xe8, 0x82, 0x97, 0x8b, 0x6b,
-	0x92, 0x9e, 0xd2, 0xa6, 0xf2, 0x65, 0xf5, 0xa0, 0x2b, 0xbb, 0xb8, 0x06, 0xe1, 0x29, 0xec, 0xbf,
-	0x25, 0x37, 0x24, 0xe3, 0x05, 0x11, 0x3f, 0xd1, 0x9f, 0x02, 0xa4, 0x26, 0x51, 0x6b, 0xbc, 0xd9,
-	0x38, 0xea, 0x52, 0x70, 0x87, 0x10, 0x7e, 0xb7, 0x61, 0xf2, 0x9a, 0x25, 0xd9, 0xad, 0xa4, 0x12,
-	0x13, 0x59, 0x66, 0x4a, 0xa2, 0x07, 0xd0, 0xbb, 0x26, 0x49, 0x4a, 0x84, 0x1e, 0xb3, 0x37, 0x1b,
-	0x46, 0xe6, 0x3b, 0xc3, 0x4d, 0x02, 0xbd, 0x82, 0x9d, 0x79, 0xb3, 0xd0, 0xb8, 0x10, 0xfc, 0x33,
-	0x59, 0xa8, 0xc6, 0xe2, 0xfe, 0xe6, 0x4d, 0x4f, 0x0c, 0xfd, 0x43, 0xcd, 0x46, 0x2f, 0x61, 0xbb,
-	0xad, 0x50, 0xfb, 0xb3, 0x75, 0xaf, 0x77, 0xe8, 0xc7, 0x86, 0x5c, 0xbb, 0x3c, 0x85, 0xff, 0x5a,
-	0x75, 0xc7, 0xae, 0xf3, 0xa7, 0x12, 0xc8, 0x28, 0xda, 0xc1, 0x49, 0xf4, 0x11, 0x0e, 0xd6, 0xf2,
-	0x98, 0x32, 0x45, 0x44, 0xb2, 0x50, 0xd5, 0x6f, 0xc4, 0xfd, 0xeb, 0xc2, 0xf6, 0xd7, 0xca, 0x77,
-	0x6b, 0x21, 0x7a, 0x0c, 0xa3, 0xca, 0x4f, 0xbc, 0xa8, 0x2f, 0x43, 0x7f, 0xeb, 0xde, 0x6c, 0x10,
-	0x35, 0x97, 0x82, 0xbd, 0x2a, 0x6b, 0xce, 0xe6, 0x39, 0xec, 0xb6, 0x55, 0x5a, 0x45, 0xff, 0x17,
-	0xc5, 0x4e, 0x4b, 0x31, 0xb2, 0x17, 0x30, 0x56, 0xf5, 0x46, 0x9b, 0xd9, 0x0d, 0xb4, 0xe4, 0x20,
-	0xda, 0x78, 0x13, 0x78, 0xa4, 0x3a, 0x68, 0xde, 0xd3, 0xff, 0xcf, 0xe3, 0x1f, 0x01, 0x00, 0x00,
-	0xff, 0xff, 0x0e, 0x2b, 0xec, 0x5d, 0x4e, 0x05, 0x00, 0x00,
+	// 789 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x55, 0x5b, 0x8b, 0x1b, 0x37,
+	0x14, 0x66, 0x76, 0x7c, 0x3d, 0x63, 0x67, 0xb3, 0x22, 0x17, 0x77, 0x4b, 0x52, 0x77, 0x48, 0x8b,
+	0x69, 0x9a, 0x09, 0x38, 0x14, 0x4a, 0xf2, 0xd2, 0x8d, 0xdb, 0x40, 0x1e, 0xd2, 0x82, 0x76, 0xf3,
+	0x6c, 0xe4, 0x19, 0x79, 0x57, 0xed, 0x8c, 0x34, 0x48, 0x9a, 0xb5, 0xfd, 0x83, 0xfa, 0x52, 0x28,
+	0xa5, 0xf4, 0x0f, 0x16, 0xdd, 0x6c, 0x67, 0xf1, 0x2e, 0x79, 0xd3, 0x77, 0xce, 0xf7, 0x9d, 0x39,
+	0x17, 0xe9, 0x0c, 0x24, 0xf5, 0xe2, 0x65, 0xbd, 0xc8, 0x6a, 0x29, 0xb4, 0x48, 0xff, 0x8a, 0xa0,
+	0xf7, 0x81, 0x6a, 0x52, 0x10, 0x4d, 0xd0, 0x08, 0xba, 0xd7, 0x54, 0x2a, 0x26, 0xf8, 0x28, 0x1a,
+	0x47, 0x93, 0x36, 0x0e, 0xd0, 0x78, 0xf2, 0xaa, 0x28, 0x19, 0xa7, 0xa3, 0xa3, 0x71, 0x34, 0xe9,
+	0xe3, 0x00, 0xd1, 0x53, 0x00, 0x49, 0x6b, 0xa1, 0x98, 0x16, 0x72, 0x33, 0x8a, 0xad, 0x73, 0xcf,
+	0x82, 0xbe, 0x85, 0xe3, 0x05, 0xbd, 0x64, 0x7c, 0xde, 0x70, 0xb6, 0x9e, 0x6b, 0x56, 0xd1, 0x51,
+	0x6b, 0x1c, 0x4d, 0x62, 0x3c, 0xb4, 0xe6, 0x8f, 0x9c, 0xad, 0x2f, 0x58, 0x45, 0x51, 0x0a, 0x43,
+	0xca, 0x8b, 0x3d, 0x56, 0xdb, 0xb2, 0x12, 0xca, 0x8b, 0xc0, 0x49, 0x5f, 0xc1, 0xe3, 0xb7, 0x8d,
+	0xe4, 0x85, 0x58, 0xf1, 0xf3, 0x9a, 0x48, 0x45, 0x3f, 0x10, 0x2d, 0xd9, 0x1a, 0x8b, 0x95, 0x4d,
+	0x50, 0x94, 0x4d, 0xc5, 0xd5, 0x28, 0x1a, 0xc7, 0x93, 0x21, 0x0e, 0xd0, 0x54, 0xf8, 0xe0, 0x90,
+	0x0a, 0x21, 0x68, 0x71, 0x52, 0x51, 0x5b, 0x6a, 0x1f, 0xdb, 0x33, 0x7a, 0x06, 0xf7, 0x78, 0x53,
+	0x2d, 0xa8, 0x9c, 0x8b, 0xe5, 0x5c, 0x8a, 0x95, 0xb2, 0xe5, 0xb6, 0xf1, 0xc0, 0x59, 0x7f, 0x5b,
+	0x62, 0xb1, 0x52, 0xe8, 0x3b, 0x38, 0xd9, 0xb1, 0xc2, 0x67, 0x63, 0x4b, 0x3c, 0x0e, 0xc4, 0x99,
+	0x33, 0xa3, 0xef, 0xa1, 0x65, 0xe3, 0xb4, 0xc6, 0xf1, 0x24, 0x99, 0x8e, 0xb2, 0x5b, 0x0a, 0xc0,
+	0x96, 0x95, 0xfe, 0x73, 0xb4, 0x2b, 0xf1, 0x8c, 0x93, 0x72, 0xa3, 0x98, 0xc2, 0x54, 0x35, 0xa5,
+	0x56, 0x68, 0x0c, 0xc9, 0xa5, 0x24, 0xbc, 0x29, 0x89, 0x64, 0x7a, 0xe3, 0x27, 0xb4, 0x6f, 0x42,
+	0xa7, 0xd0, 0x53, 0xa4, 0xaa, 0x4b, 0xc6, 0x2f, 0x7d, 0xde, 0x5b, 0x8c, 0x5e, 0x42, 0xb7, 0x96,
+	0xe2, 0x77, 0x9a, 0x6b, 0x9b, 0x69, 0x32, 0x7d, 0x78, 0x38, 0x95, 0xc0, 0x42, 0xcf, 0xa1, 0xbd,
+	0x64, 0x25, 0x0d, 0x99, 0xdf, 0x42, 0x77, 0x1c, 0xf4, 0x02, 0x3a, 0x35, 0x15, 0x75, 0x69, 0xc6,
+	0x76, 0x07, 0xdb, 0x93, 0xd0, 0x7b, 0x40, 0xee, 0x34, 0x67, 0x5c, 0x53, 0x49, 0x72, 0x6d, 0xee,
+	0x5c, 0xc7, 0xe6, 0x75, 0x9a, 0xcd, 0x44, 0x55, 0x4b, 0xaa, 0x14, 0x2d, 0x9c, 0x18, 0x8b, 0x95,
+	0xd7, 0x9f, 0x38, 0xd5, 0xfb, 0x9d, 0x28, 0xfd, 0x2f, 0x82, 0x2f, 0x6e, 0x15, 0x1c, 0x98, 0x67,
+	0xf4, 0xb9, 0xf3, 0x3c, 0x3a, 0x3c, 0x4f, 0x04, 0x2d, 0xf3, 0x56, 0x46, 0xf1, 0x38, 0x9e, 0xc4,
+	0xb8, 0x15, 0xde, 0x0d, 0xe3, 0x05, 0xcb, 0x7d, 0xb3, 0xda, 0x38, 0x40, 0xf4, 0x08, 0x3a, 0x8c,
+	0x17, 0xb5, 0x96, 0xb6, 0x2f, 0x31, 0xf6, 0x28, 0x3d, 0x87, 0xee, 0x4c, 0x34, 0xb5, 0x69, 0xdd,
+	0x03, 0x68, 0x33, 0x5e, 0xd0, 0xb5, 0xbd, 0xb7, 0x7d, 0xec, 0x00, 0x9a, 0x42, 0xa7, 0xb2, 0x25,
+	0xd8, 0x3c, 0xee, 0xee, 0x8a, 0x67, 0xa6, 0xcf, 0x60, 0x70, 0x21, 0x9a, 0xfc, 0x8a, 0x16, 0xef,
+	0x98, 0x8f, 0xec, 0x26, 0x18, 0xd9, 0xa4, 0x1c, 0x48, 0xdf, 0xc1, 0xc3, 0x9f, 0xe9, 0x35, 0x2d,
+	0x45, 0x4d, 0xe5, 0x27, 0xf4, 0x17, 0x00, 0x45, 0x70, 0x38, 0x4d, 0x32, 0x1d, 0x66, 0xfb, 0x14,
+	0xbc, 0x47, 0x48, 0xff, 0x8d, 0xe0, 0x9e, 0xaf, 0x21, 0xdc, 0xd0, 0xe7, 0x30, 0x30, 0xdf, 0x98,
+	0xe7, 0xce, 0xec, 0x07, 0xda, 0xcb, 0x02, 0x2d, 0x31, 0xde, 0x50, 0xf7, 0x0f, 0x70, 0xb2, 0x8d,
+	0xb6, 0x55, 0x74, 0x6f, 0x28, 0xee, 0x6f, 0x29, 0x41, 0xf6, 0x06, 0x86, 0xda, 0xa5, 0x34, 0x77,
+	0xc5, 0xf5, 0xac, 0xe4, 0x51, 0x76, 0xb0, 0x28, 0x3c, 0xd0, 0x7b, 0x28, 0xfd, 0x33, 0x02, 0xf8,
+	0x78, 0x76, 0x7e, 0x31, 0xbb, 0x22, 0xfc, 0x92, 0xa2, 0x2f, 0xa1, 0x6f, 0xf3, 0xdd, 0x5b, 0x03,
+	0x3d, 0x63, 0xf8, 0xd5, 0xac, 0x82, 0x27, 0x00, 0x4a, 0xe6, 0xf3, 0x05, 0x5d, 0x0a, 0x19, 0xb6,
+	0x5e, 0x5f, 0xc9, 0xfc, 0xad, 0x35, 0x18, 0xad, 0x71, 0x93, 0xa5, 0xa6, 0xd2, 0xaf, 0xbd, 0x9e,
+	0x92, 0xf9, 0x99, 0xc1, 0xe8, 0x2b, 0x48, 0x1a, 0xa2, 0x74, 0x10, 0xb7, 0xdc, 0x56, 0x34, 0x26,
+	0xaf, 0x7e, 0x02, 0x16, 0x79, 0x79, 0xdb, 0x05, 0x37, 0x16, 0xab, 0x4f, 0x7f, 0x82, 0xc7, 0xbb,
+	0x34, 0xd5, 0x39, 0xb9, 0xa6, 0x32, 0xf4, 0xf8, 0x1b, 0xe8, 0xe6, 0xce, 0xec, 0x47, 0x94, 0x64,
+	0x3b, 0x2a, 0x0e, 0xbe, 0xf4, 0xef, 0x08, 0x8e, 0x6f, 0x2e, 0x90, 0xaf, 0xa1, 0x73, 0x45, 0x49,
+	0x41, 0xa5, 0xad, 0x35, 0x99, 0xf6, 0xb3, 0xb0, 0xf9, 0xb1, 0x77, 0xa0, 0xd7, 0xd0, 0xcb, 0x05,
+	0xd7, 0x94, 0x6b, 0xf3, 0x00, 0x4c, 0xf8, 0xa7, 0xd9, 0x8d, 0x30, 0xd9, 0xcc, 0x13, 0x7e, 0xe1,
+	0x5a, 0x6e, 0xf0, 0x96, 0x7f, 0xfa, 0x06, 0x86, 0x9f, 0xb8, 0xd0, 0x7d, 0x88, 0xff, 0xa0, 0x1b,
+	0xdf, 0x58, 0x73, 0x34, 0x37, 0xf2, 0x9a, 0x94, 0x8d, 0x6b, 0xe7, 0x00, 0x3b, 0xf0, 0xfa, 0xe8,
+	0xc7, 0x68, 0xd1, 0xb1, 0xbf, 0xa3, 0x57, 0xff, 0x07, 0x00, 0x00, 0xff, 0xff, 0x38, 0x80, 0x22,
+	0x2d, 0x9d, 0x06, 0x00, 0x00,
 }

+ 34 - 18
pb/pb.proto

@@ -11,10 +11,6 @@ message Metadata {
     int64 begin_unix_time = 4;
     // timestamp of the last analysed commit
     int64 end_unix_time = 5;
-    // how many days are in each band [burndown_project, burndown_file, burndown_developer]
-    int32 granularity = 6;
-    // how frequently we measure the state of each band [burndown_project, burndown_file, burndown_developer]
-    int32 sampling = 7;
 }
 
 message BurndownSparseMatrixRow {
@@ -31,6 +27,21 @@ message BurndownSparseMatrix {
     repeated BurndownSparseMatrixRow rows = 4;
 }
 
+message BurndownAnalysisResults {
+    // how many days are in each band [burndown_project, burndown_file, burndown_developer]
+    int32 granularity = 1;
+    // how frequently we measure the state of each band [burndown_project, burndown_file, burndown_developer]
+    int32 sampling = 2;
+    // always exists
+    BurndownSparseMatrix project = 3;
+    // this is included if `-burndown-files` was specified
+    repeated BurndownSparseMatrix files = 4;
+    // these two are included if `-burndown-people` was specified
+    repeated BurndownSparseMatrix people = 5;
+    // rows and cols order correspond to `burndown_developer`
+    CompressedSparseRowMatrix people_interaction = 6;
+}
+
 message CompressedSparseRowMatrix {
     int32 number_of_rows = 1;
     int32 number_of_columns = 2;
@@ -56,21 +67,26 @@ message DeveloperTouchedFiles {
     repeated TouchedFiles developers = 1;
 }
 
-message AnalysisResults {
-    // these two are always included
-    Metadata header = 1;
-    BurndownSparseMatrix burndown_project = 2;
-
-    // this is included if `-files` was specified
-    repeated BurndownSparseMatrix burndown_files = 3;
-
-    // these two are included if `-people` was specified
-    repeated BurndownSparseMatrix burndown_developers = 4;
-    // rows and cols order correspond to `burndown_developer`
-    CompressedSparseRowMatrix developers_interaction = 5;
-
-    // these three are included if `-couples` was specified
+message CouplesResults {
     Couples file_couples = 6;
     Couples developer_couples = 7;
     DeveloperTouchedFiles touched_files = 8;
 }
+
+message UASTChange {
+    string file_name = 1;
+    string src_before = 2;
+	string src_after = 3;
+	string uast_before = 4;
+	string uast_after = 5;
+}
+
+message UASTChangesSaverResults {
+    repeated UASTChange changes = 1;
+}
+
+message AnalysisResults {
+    Metadata header = 1;
+    // the mapped values are dynamic messages which require the second parsing pass.
+    map<string, bytes> contents = 2;
+}

+ 12 - 8
pipeline.go

@@ -15,7 +15,7 @@ import (
 	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v2/toposort"
+	"gopkg.in/src-d/hercules.v3/toposort"
 )
 
 type ConfigurationOptionType int
@@ -75,25 +75,29 @@ type FeaturedPipelineItem interface {
 	Features() []string
 }
 
-// FinalizedPipelineItem corresponds to the top level pipeline items which produce the end results.
-type FinalizedPipelineItem interface {
+// LeafPipelineItem corresponds to the top level pipeline items which produce the end results.
+type LeafPipelineItem interface {
 	PipelineItem
-	// Finalize returns the result of the analysis.
-	Finalize() interface{}
 	// Flag returns the cmdline name of the item.
 	Flag() string
+	// Finalize returns the result of the analysis.
+	Finalize() interface{}
+	// Serialize encodes the object returned by Finalize() to Text or Protocol Buffers.
+	Serialize(result interface{}, binary bool, writer io.Writer) error
 }
 
+// PipelineItemRegistry contains all the known PipelineItem-s.
 type PipelineItemRegistry struct {
 	provided   map[string][]reflect.Type
 	registered map[string]reflect.Type
 	flags map[string]reflect.Type
 }
 
+// Register adds another PipelineItem to the registry.
 func (registry *PipelineItemRegistry) Register(example PipelineItem) {
 	t := reflect.TypeOf(example)
 	registry.registered[example.Name()] = t
-	if fpi, ok := interface{}(example).(FinalizedPipelineItem); ok {
+	if fpi, ok := interface{}(example).(LeafPipelineItem); ok {
 		registry.flags[fpi.Flag()] = t
 	}
 	for _, dep := range example.Provides() {
@@ -173,7 +177,7 @@ func (registry *PipelineItemRegistry) AddFlags() (map[string]interface{}, map[st
 				featureFlags.Choices[f] = true
 			}
 		}
-		if fpi, ok := itemIface.(FinalizedPipelineItem); ok {
+		if fpi, ok := itemIface.(LeafPipelineItem); ok {
 			deployed[fpi.Name()] = flag.Bool(
 				fpi.Flag(), false, fmt.Sprintf("Runs %s analysis.", fpi.Name()))
 		}
@@ -482,7 +486,7 @@ func (pipeline *Pipeline) Run(commits []*object.Commit) (map[PipelineItem]interf
 	onProgress(len(commits), len(commits))
 	result := map[PipelineItem]interface{}{}
 	for _, item := range pipeline.items {
-		if fpi, ok := interface{}(item).(FinalizedPipelineItem); ok {
+		if fpi, ok := interface{}(item).(LeafPipelineItem); ok {
 			result[item] = fpi.Finalize()
 		}
 	}

+ 7 - 101
stdout/utils.go

@@ -2,11 +2,9 @@ package stdout
 
 import (
 	"fmt"
-	"sort"
+	"io"
 	"strconv"
 	"strings"
-
-	"gopkg.in/src-d/hercules.v2"
 )
 
 func SafeString(str string) string {
@@ -15,7 +13,7 @@ func SafeString(str string) string {
 	return "\"" + str + "\""
 }
 
-func PrintMatrix(matrix [][]int64, name string, fixNegative bool) {
+func PrintMatrix(writer io.Writer, matrix [][]int64, indent int, name string, fixNegative bool) {
 	// determine the maximum length of each value
 	var maxnum int64 = -(1 << 32)
 	var minnum int64 = 1 << 32
@@ -37,15 +35,14 @@ func PrintMatrix(matrix [][]int64, name string, fixNegative bool) {
 		}
 	}
 	last := len(matrix[len(matrix)-1])
-	indent := 2
 	if name != "" {
-		fmt.Printf("  %s: |-\n", SafeString(name))
+		fmt.Fprintf(writer, "%s%s: |-\n", strings.Repeat(" ", indent), SafeString(name))
 		indent += 2
 	}
 	// print the resulting triangular matrix
 	first := true
 	for _, status := range matrix {
-		fmt.Print(strings.Repeat(" ", indent-1))
+		fmt.Fprint(writer, strings.Repeat(" ", indent-1))
 		for i := 0; i < last; i++ {
 			var val int64
 			if i < len(status) {
@@ -57,103 +54,12 @@ func PrintMatrix(matrix [][]int64, name string, fixNegative bool) {
 				}
 			}
 			if !first {
-				fmt.Printf(" %[1]*[2]d", width, val)
+				fmt.Fprintf(writer, " %[1]*[2]d", width, val)
 			} else {
 				first = false
-				fmt.Printf("%d%s", val, strings.Repeat(" ", width-len(strconv.FormatInt(val, 10))))
-			}
-		}
-		fmt.Println()
-	}
-}
-
-func PrintCouples(result *hercules.CouplesResult, peopleDict []string) {
-	fmt.Println("files_coocc:")
-	fmt.Println("  index:")
-	for _, file := range result.Files {
-		fmt.Printf("    - %s\n", SafeString(file))
-	}
-
-	fmt.Println("  matrix:")
-	for _, files := range result.FilesMatrix {
-		fmt.Print("    - {")
-		indices := []int{}
-		for file := range files {
-			indices = append(indices, file)
-		}
-		sort.Ints(indices)
-		for i, file := range indices {
-			fmt.Printf("%d: %d", file, files[file])
-			if i < len(indices)-1 {
-				fmt.Print(", ")
-			}
-		}
-		fmt.Println("}")
-	}
-
-	fmt.Println("people_coocc:")
-	fmt.Println("  index:")
-	for _, person := range peopleDict {
-		fmt.Printf("    - %s\n", SafeString(person))
-	}
-
-	fmt.Println("  matrix:")
-	for _, people := range result.PeopleMatrix {
-		fmt.Print("    - {")
-		indices := []int{}
-		for file := range people {
-			indices = append(indices, file)
-		}
-		sort.Ints(indices)
-		for i, person := range indices {
-			fmt.Printf("%d: %d", person, people[person])
-			if i < len(indices)-1 {
-				fmt.Print(", ")
+				fmt.Fprintf(writer, "%d%s", val, strings.Repeat(" ", width-len(strconv.FormatInt(val, 10))))
 			}
 		}
-		fmt.Println("}")
+		fmt.Fprintln(writer)
 	}
-
-	fmt.Println("  author_files:") // sorted by number of files each author changed
-	peopleFiles := sortByNumberOfFiles(result.PeopleFiles, peopleDict, result.Files)
-	for _, authorFiles := range peopleFiles {
-		fmt.Printf("    - %s:\n", SafeString(authorFiles.Author))
-		sort.Strings(authorFiles.Files)
-		for _, file := range authorFiles.Files {
-			fmt.Printf("      - %s\n", SafeString(file)) // sorted by path
-		}
-	}
-}
-
-func sortByNumberOfFiles(
-	peopleFiles [][]int, peopleDict []string, filesDict []string) authorFilesList {
-	var pfl authorFilesList
-	for peopleIdx, files := range peopleFiles {
-		if peopleIdx < len(peopleDict) {
-			fileNames := make([]string, len(files))
-			for i, fi := range files {
-				fileNames[i] = filesDict[fi]
-			}
-			pfl = append(pfl, authorFiles{peopleDict[peopleIdx], fileNames})
-		}
-	}
-	sort.Sort(pfl)
-	return pfl
-}
-
-type authorFiles struct {
-	Author string
-	Files  []string
-}
-
-type authorFilesList []authorFiles
-
-func (s authorFilesList) Len() int {
-	return len(s)
-}
-func (s authorFilesList) Swap(i, j int) {
-	s[i], s[j] = s[j], s[i]
-}
-func (s authorFilesList) Less(i, j int) bool {
-	return len(s[i].Files) < len(s[j].Files)
 }

+ 89 - 2
uast.go

@@ -5,13 +5,14 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io"
+	goioutil "io/ioutil"
 	"os"
 	"runtime"
 	"strings"
 	"sync"
 	"time"
 
-	"github.com/jeffail/tunny"
 	"gopkg.in/bblfsh/client-go.v1"
 	"gopkg.in/bblfsh/sdk.v1/protocol"
 	"gopkg.in/bblfsh/sdk.v1/uast"
@@ -21,6 +22,9 @@ import (
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
 	"gopkg.in/src-d/go-git.v4/utils/ioutil"
 	"gopkg.in/src-d/go-git.v4/utils/merkletrie"
+	"gopkg.in/src-d/hercules.v3/pb"
+	"github.com/jeffail/tunny"
+	"github.com/gogo/protobuf/proto"
 )
 
 type UASTExtractor struct {
@@ -353,9 +357,17 @@ func (uc *UASTChanges) Consume(deps map[string]interface{}) (map[string]interfac
 }
 
 type UASTChangesSaver struct {
+	// OutputPath points to the target directory with UASTs
+	OutputPath string
+
+	repository *git.Repository
 	result [][]UASTChange
 }
 
+const (
+	ConfigUASTChangesSaverOutputPath = "UASTChangesSaver.OutputPath"
+)
+
 func (saver *UASTChangesSaver) Name() string {
 	return "UASTChangesSaver"
 }
@@ -375,12 +387,24 @@ func (saver *UASTChangesSaver) Features() []string {
 }
 
 func (saver *UASTChangesSaver) ListConfigurationOptions() []ConfigurationOption {
-	return []ConfigurationOption{}
+	options := [...]ConfigurationOption{{
+		Name:        ConfigUASTChangesSaverOutputPath,
+		Description: "The target directory where to store the changed UAST files.",
+		Flag:        "changed-uast-dir",
+		Type:        StringConfigurationOption,
+		Default:     "."},
+	}
+	return options[:]
+}
+
+func (saver *UASTChangesSaver) Flag() string {
+	return "dump-uast-changes"
 }
 
 func (saver *UASTChangesSaver) Configure(facts map[string]interface{}) {}
 
 func (saver *UASTChangesSaver) Initialize(repository *git.Repository) {
+	saver.repository = repository
 	saver.result = [][]UASTChange{}
 }
 
@@ -394,6 +418,69 @@ func (saver *UASTChangesSaver) Finalize() interface{} {
 	return saver.result
 }
 
+func (saver *UASTChangesSaver) Serialize(result interface{}, binary bool, writer io.Writer) error {
+	saverResult := result.([][]UASTChange)
+	fileNames := saver.dumpFiles(saverResult)
+	if binary {
+		return saver.serializeBinary(fileNames, writer)
+	}
+	saver.serializeText(fileNames, writer)
+	return nil
+}
+
+func (saver *UASTChangesSaver) dumpFiles(result [][]UASTChange) []*pb.UASTChange {
+	fileNames := []*pb.UASTChange{}
+	for i, changes := range result {
+		for j, change := range changes {
+			if change.Before == nil || change.After == nil {
+				continue
+			}
+			record := &pb.UASTChange{FileName: change.Change.To.Name}
+			bs, _ := change.Before.Marshal()
+			record.UastBefore = fmt.Sprintf(
+				"%d_%d_before_%s.pb", i, j, change.Change.From.TreeEntry.Hash.String())
+			goioutil.WriteFile(record.UastBefore, bs, 0666)
+			blob, _ := saver.repository.BlobObject(change.Change.From.TreeEntry.Hash)
+			s, _ := (&object.File{Blob: *blob}).Contents()
+			record.SrcBefore = fmt.Sprintf(
+				"%d_%d_before_%s.src", i, j, change.Change.From.TreeEntry.Hash.String())
+			goioutil.WriteFile(record.SrcBefore, []byte(s), 0666)
+			bs, _ = change.After.Marshal()
+			record.UastAfter = fmt.Sprintf(
+				"%d_%d_after_%s.pb", i, j, change.Change.To.TreeEntry.Hash.String())
+			goioutil.WriteFile(record.UastAfter, bs, 0666)
+			blob, _ = saver.repository.BlobObject(change.Change.To.TreeEntry.Hash)
+			s, _ = (&object.File{Blob: *blob}).Contents()
+			record.SrcAfter = fmt.Sprintf(
+				"%d_%d_after_%s.src", i, j, change.Change.To.TreeEntry.Hash.String())
+			goioutil.WriteFile(record.SrcAfter, []byte(s), 0666)
+			fileNames = append(fileNames, record)
+		}
+	}
+	return fileNames
+}
+
+func (saver *UASTChangesSaver) serializeText(result []*pb.UASTChange, writer io.Writer) {
+	for _, sc := range result {
+		kv := [...]string{
+			"file: " + sc.FileName,
+			"src0: " + sc.SrcBefore, "src1: " + sc.SrcAfter,
+			"uast0: " + sc.UastBefore, "uast1: " + sc.UastAfter,
+		}
+		fmt.Fprintf(writer, "  - {%s}\n", strings.Join(kv[:], ", "))
+	}
+}
+
+func (saver *UASTChangesSaver) serializeBinary(result []*pb.UASTChange, writer io.Writer) error {
+  message := pb.UASTChangesSaverResults{Changes: result}
+	serialized, err := proto.Marshal(&message)
+	if err != nil {
+		return err
+	}
+  writer.Write(serialized)
+	return nil
+}
+
 func init() {
 	Registry.Register(&UASTExtractor{})
 	Registry.Register(&UASTChanges{})