Pārlūkot izejas kodu

Merge pull request #175 from vmarkovtsev/master

Language stats
Vadim Markovtsev 6 gadi atpakaļ
vecāks
revīzija
eac4d7b69f
55 mainītis faili ar 827 papildinājumiem un 342 dzēšanām
  1. 5 5
      .appveyor.yml
  2. 8 8
      .travis.yml
  3. 4 4
      Dockerfile
  4. 2 2
      Makefile
  5. 3 3
      README.md
  6. 2 2
      cmd/hercules/combine.go
  7. 1 1
      cmd/hercules/plugin.template
  8. 2 2
      cmd/hercules/root.go
  9. 1 1
      contrib/_plugin_example/churn_analysis.go
  10. 6 6
      core.go
  11. 2 2
      internal/burndown/file.go
  12. 1 1
      internal/burndown/file_test.go
  13. 1 1
      internal/core/forks.go
  14. 2 2
      internal/core/pipeline.go
  15. 2 2
      internal/core/pipeline_test.go
  16. 1 1
      internal/core/registry_test.go
  17. 4 4
      internal/global_test.go
  18. 130 100
      internal/pb/pb.pb.go
  19. 8 3
      internal/pb/pb.proto
  20. 126 30
      internal/pb/pb_pb2.py
  21. 2 2
      internal/plumbing/blob_cache.go
  22. 3 3
      internal/plumbing/blob_cache_test.go
  23. 1 1
      internal/plumbing/day.go
  24. 2 2
      internal/plumbing/day_test.go
  25. 1 1
      internal/plumbing/diff.go
  26. 6 6
      internal/plumbing/diff_test.go
  27. 1 1
      internal/plumbing/identity/identity.go
  28. 2 2
      internal/plumbing/identity/identity_test.go
  29. 109 0
      internal/plumbing/languages.go
  30. 105 0
      internal/plumbing/languages_test.go
  31. 2 2
      internal/plumbing/renames.go
  32. 2 2
      internal/plumbing/renames_test.go
  33. 2 2
      internal/plumbing/tree_diff.go
  34. 2 2
      internal/plumbing/tree_diff_test.go
  35. 2 2
      internal/plumbing/uast/changes_xpather_test.go
  36. 2 2
      internal/plumbing/uast/diff_refiner.go
  37. 3 3
      internal/plumbing/uast/diff_refiner_test.go
  38. 1 1
      internal/plumbing/uast/test/utils.go
  39. 3 3
      internal/plumbing/uast/uast.go
  40. 4 4
      internal/plumbing/uast/uast_test.go
  41. 2 2
      internal/test/fixtures/fixtures.go
  42. 4 2
      labours.py
  43. 7 7
      leaves/burndown.go
  44. 6 6
      leaves/burndown_test.go
  45. 4 4
      leaves/comment_sentiment.go
  46. 6 6
      leaves/comment_sentiment_test.go
  47. 5 5
      leaves/couples.go
  48. 5 5
      leaves/couples_test.go
  49. 122 23
      leaves/devs.go
  50. 83 44
      leaves/devs_test.go
  51. 3 3
      leaves/file_history.go
  52. 4 4
      leaves/file_history_test.go
  53. 4 4
      leaves/shotness.go
  54. 5 5
      leaves/shotness_test.go
  55. 1 1
      version_test.go

+ 5 - 5
.appveyor.yml

@@ -2,7 +2,7 @@ version: "{build}"
 platform: x64
 image: Visual Studio 2017
 
-clone_folder: c:\gopath\src\gopkg.in\src-d\hercules.v6
+clone_folder: c:\gopath\src\gopkg.in\src-d\hercules.v7
 
 environment:
   GOPATH: c:\gopath
@@ -16,14 +16,14 @@ build_script:
   - go version
   - set PATH=C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\bin;C:\msys64\usr\bin;C:\msys64\mingw64\bin;%GOPATH%\bin;%PATH%
   - set PATH=%PATH:C:\Program Files\Git\usr\bin;=%
-  - cd %GOPATH%\src\gopkg.in\src-d\hercules.v6
+  - cd %GOPATH%\src\gopkg.in\src-d\hercules.v7
   - go get -v github.com/golang/dep/cmd/dep
   - make
-  - 7z a c:\gopath\src\gopkg.in\src-d\hercules.v6\hercules.win64.zip %GOPATH%\bin\hercules.exe
+  - 7z a c:\gopath\src\gopkg.in\src-d\hercules.v7\hercules.win64.zip %GOPATH%\bin\hercules.exe
 
 test_script:
-  - go get -v -t -d gopkg.in/src-d/hercules.v6/...
-  - go test -v -tags disable_babelfish gopkg.in/src-d/hercules.v6/...
+  - go get -v -t -d gopkg.in/src-d/hercules.v7/...
+  - go test -v -tags disable_babelfish gopkg.in/src-d/hercules.v7/...
 
 artifacts:
   - name: hercules.win64.zip

+ 8 - 8
.travis.yml

@@ -5,7 +5,7 @@ git:
   depth: 9999999
 
 language: go
-go_import_path: gopkg.in/src-d/hercules.v6
+go_import_path: gopkg.in/src-d/hercules.v7
 go:
   - 1.10.x
   - 1.11.x
@@ -24,10 +24,10 @@ cache:
     - $HOME/gopath/src
 before_cache:
   - cd $HOME/gopath
-  - mv $HOME/gopath/src/gopkg.in/src-d/hercules.v6/vendor $HOME/gopath/src/gopkg.in/src-d/.vendor
-  - rm -rf $HOME/gopath/src/gopkg.in/src-d/hercules.v6
-  - mkdir $HOME/gopath/src/gopkg.in/src-d/hercules.v6
-  - mv $HOME/gopath/src/gopkg.in/src-d/.vendor $HOME/gopath/src/gopkg.in/src-d/hercules.v6/vendor
+  - mv $HOME/gopath/src/gopkg.in/src-d/hercules.v7/vendor $HOME/gopath/src/gopkg.in/src-d/.vendor
+  - rm -rf $HOME/gopath/src/gopkg.in/src-d/hercules.v7
+  - mkdir $HOME/gopath/src/gopkg.in/src-d/hercules.v7
+  - mv $HOME/gopath/src/gopkg.in/src-d/.vendor $HOME/gopath/src/gopkg.in/src-d/hercules.v7/vendor
 
 matrix:
   fast_finish: true
@@ -69,7 +69,7 @@ script:
   - go vet -tags tensorflow ./...
   - golint -set_exit_status $(go list ./... | grep -v /vendor/)
   - flake8
-  - go test -coverpkg=all -v -coverprofile=coverage.txt -covermode=count gopkg.in/src-d/hercules.v6/... && sed -i '/cmd\/hercules\|core.go/d' coverage.txt
+  - go test -coverpkg=all -v -coverprofile=coverage.txt -covermode=count gopkg.in/src-d/hercules.v7/... && sed -i '/cmd\/hercules\|core.go/d' coverage.txt
   - $GOPATH/bin/hercules version
   - $GOPATH/bin/hercules --burndown --couples --devs --quiet --pb https://github.com/src-d/hercules > 1.pb
   - cp 1.pb 2.pb
@@ -95,7 +95,7 @@ jobs:
       os: osx
       osx_image: xcode9.3
       go: 1.11.x
-      go_import_path: gopkg.in/src-d/hercules.v6
+      go_import_path: gopkg.in/src-d/hercules.v7
       before_install:
         - wget -O protoc.zip https://github.com/google/protobuf/releases/download/v$PROTOC_VERSION/protoc-$PROTOC_VERSION-osx-x86_64.zip
         - unzip -d ~/.local protoc.zip && rm protoc.zip
@@ -117,7 +117,7 @@ jobs:
     - stage: deploy
       os: linux
       go: 1.11.x
-      go_import_path: gopkg.in/src-d/hercules.v6
+      go_import_path: gopkg.in/src-d/hercules.v7
       before_install:
         - wget -O protoc.zip https://github.com/google/protobuf/releases/download/v$PROTOC_VERSION/protoc-$PROTOC_VERSION-linux-x86_64.zip
         - unzip -d ~/.local protoc.zip && rm protoc.zip

+ 4 - 4
Dockerfile

@@ -3,7 +3,7 @@ FROM ubuntu:18.04
 ENV GOPATH /root
 ENV PROTOBUF_VERSION 3.5.1
 
-COPY . /root/src/gopkg.in/src-d/hercules.v6
+COPY . /root/src/gopkg.in/src-d/hercules.v7
 RUN apt-get update && \
     apt-get install -y --no-install-suggests --no-install-recommends software-properties-common && \
     add-apt-repository -y ppa:gophers/archive && \
@@ -14,13 +14,13 @@ RUN apt-get update && \
     locale-gen en_US.UTF-8 && \
     export PATH=/usr/lib/go-1.10/bin:/root/bin:$PATH && \
     go get -v github.com/golang/dep/cmd/dep && \
-    cd /root/src/gopkg.in/src-d/hercules.v6 && \
+    cd /root/src/gopkg.in/src-d/hercules.v7 && \
     export CGO_CXXFLAGS=-std=c++14 && \
     curl -L "https://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-cpu-$(go env GOOS)-x86_64-1.7.0.tar.gz" | tar -C /usr/local -xz && \
     make && \
     rm /usr/local/bin/protoc && rm /usr/local/readme.txt && rm -rf /usr/local/include/google && \
     cp /root/bin/hercules /usr/local/bin && \
-    cp -r /root/src/gopkg.in/src-d/hercules.v6/*.py /root/src/gopkg.in/src-d/hercules.v6/internal /usr/local/bin && \
+    cp -r /root/src/gopkg.in/src-d/hercules.v7/*.py /root/src/gopkg.in/src-d/hercules.v7/internal /usr/local/bin && \
     sed -i 's/parser.add_argument("--backend",/parser.add_argument("--backend", default="Agg",/' /usr/local/bin/labours.py && \
     echo '#!/bin/bash\n\
 \n\
@@ -30,7 +30,7 @@ echo\n\' > /browser && \
     chmod +x /browser && \
     curl https://bootstrap.pypa.io/get-pip.py | python3 - pip==18.1 && \
     pip3 install --no-cache-dir --no-build-isolation cython && \
-    pip3 install --no-cache-dir --no-build-isolation -r /root/src/gopkg.in/src-d/hercules.v6/requirements.txt https://github.com/mind/wheels/releases/download/tf1.7-cpu/tensorflow-1.7.0-cp36-cp36m-linux_x86_64.whl && \
+    pip3 install --no-cache-dir --no-build-isolation -r /root/src/gopkg.in/src-d/hercules.v7/requirements.txt https://github.com/mind/wheels/releases/download/tf1.7-cpu/tensorflow-1.7.0-cp36-cp36m-linux_x86_64.whl && \
     rm -rf /root/* && \
     apt-get remove -y software-properties-common golang-1.10-go python3-dev libyaml-dev libxml2-dev curl git make unzip g++ && \
     apt-get remove -y *-doc *-man && \

+ 2 - 2
Makefile

@@ -11,7 +11,7 @@ BBLFSH_DEP =
 all: ${GOPATH}/bin/hercules${EXE}
 
 test: all
-	go test gopkg.in/src-d/hercules.v6
+	go test gopkg.in/src-d/hercules.v7
 
 ${GOPATH}/bin/protoc-gen-gogo${EXE}:
 	go get -v github.com/gogo/protobuf/protoc-gen-gogo
@@ -42,4 +42,4 @@ vendor/gopkg.in/bblfsh/client-go.v2/tools/include:
 endif
 
 ${GOPATH}/bin/hercules${EXE}: vendor *.go */*.go */*/*.go */*/*/*.go internal/pb/pb.pb.go internal/pb/pb_pb2.py cmd/hercules/plugin_template_source.go ${BBLFSH_DEP}
-	go get -tags "$(TAGS)" -ldflags "-X gopkg.in/src-d/hercules.v6.BinaryGitHash=$(shell git rev-parse HEAD)" gopkg.in/src-d/hercules.v6/cmd/hercules
+	go get -tags "$(TAGS)" -ldflags "-X gopkg.in/src-d/hercules.v7.BinaryGitHash=$(shell git rev-parse HEAD)" gopkg.in/src-d/hercules.v7/cmd/hercules

+ 3 - 3
README.md

@@ -4,7 +4,7 @@
 <h1 align="center">Hercules</h1>
 <p align="center">
       Fast, insightful and highly customizable Git history analysis.<br><br>
-      <a href="http://godoc.org/gopkg.in/src-d/hercules.v6"><img src="https://godoc.org/gopkg.in/src-d/hercules.v6?status.svg" alt="GoDoc"></a>
+      <a href="http://godoc.org/gopkg.in/src-d/hercules.v7"><img src="https://godoc.org/gopkg.in/src-d/hercules.v7?status.svg" alt="GoDoc"></a>
       <a href="https://travis-ci.org/src-d/hercules"><img src="https://travis-ci.org/src-d/hercules.svg?branch=master" alt="Travis build Status"></a>
       <a href="https://ci.appveyor.com/project/vmarkovtsev/hercules"><img src="https://ci.appveyor.com/api/projects/status/49f0lm3v2y6xyph3?svg=true" alt="AppVeyor build status"></a>
       <a href="https://hub.docker.com/r/srcd/hercules"><img src="https://img.shields.io/docker/build/srcd/hercules.svg" alt="Docker build status"></a>
@@ -59,8 +59,8 @@ Numpy and Scipy can be installed on Windows using http://www.lfd.uci.edu/~gohlke
 You are going to need Go (>= v1.10), [`protoc`](https://github.com/google/protobuf/releases),
 and [`dep`](https://github.com/golang/dep).
 ```
-go get -d gopkg.in/src-d/hercules.v6/cmd/hercules
-cd $GOPATH/src/gopkg.in/src-d/hercules.v6
+go get -d gopkg.in/src-d/hercules.v7/cmd/hercules
+cd $GOPATH/src/gopkg.in/src-d/hercules.v7
 make
 ```
 

+ 2 - 2
cmd/hercules/combine.go

@@ -12,8 +12,8 @@ import (
 
 	"github.com/gogo/protobuf/proto"
 	"github.com/spf13/cobra"
-	"gopkg.in/src-d/hercules.v6"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
+	"gopkg.in/src-d/hercules.v7"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
 )
 
 // combineCmd represents the combine command

+ 1 - 1
cmd/hercules/plugin.template

@@ -24,7 +24,7 @@ import (
 
   "github.com/gogo/protobuf/proto"
   "gopkg.in/src-d/go-git.v4"
-  "gopkg.in/src-d/hercules.v6"
+  "gopkg.in/src-d/hercules.v7"
 )
 
 // {{.name}} contains the intermediate state which is mutated by Consume(). It should implement

+ 2 - 2
cmd/hercules/root.go

@@ -34,8 +34,8 @@ 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.v6"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
+	"gopkg.in/src-d/hercules.v7"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
 )
 
 // oneLineWriter splits the output data by lines and outputs one on top of another using '\r'.

+ 1 - 1
contrib/_plugin_example/churn_analysis.go

@@ -13,7 +13,7 @@ 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.v6"
+	"gopkg.in/src-d/hercules.v7"
 )
 
 // ChurnAnalysis contains the intermediate state which is mutated by Consume(). It should implement

+ 6 - 6
core.go

@@ -4,12 +4,12 @@ import (
 	"github.com/spf13/pflag"
 	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing/identity"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing/uast"
-	"gopkg.in/src-d/hercules.v6/internal/yaml"
-	"gopkg.in/src-d/hercules.v6/leaves"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing/uast"
+	"gopkg.in/src-d/hercules.v7/internal/yaml"
+	"gopkg.in/src-d/hercules.v7/leaves"
 )
 
 // ConfigurationOptionType represents the possible types of a ConfigurationOption's value.

+ 2 - 2
internal/burndown/file.go

@@ -5,8 +5,8 @@ import (
 	"log"
 	"math"
 
-	"gopkg.in/src-d/hercules.v6/internal"
-	"gopkg.in/src-d/hercules.v6/internal/rbtree"
+	"gopkg.in/src-d/hercules.v7/internal"
+	"gopkg.in/src-d/hercules.v7/internal/rbtree"
 )
 
 // Updater is the function which is called back on File.Update().

+ 1 - 1
internal/burndown/file_test.go

@@ -6,7 +6,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"gopkg.in/src-d/hercules.v6/internal/rbtree"
+	"gopkg.in/src-d/hercules.v7/internal/rbtree"
 )
 
 func updateStatusFile(status map[int]int64, _, previousTime, delta int) {

+ 1 - 1
internal/core/forks.go

@@ -9,7 +9,7 @@ import (
 
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/toposort"
+	"gopkg.in/src-d/hercules.v7/internal/toposort"
 )
 
 // OneShotMergeProcessor provides the convenience method to consume merges only once.

+ 2 - 2
internal/core/pipeline.go

@@ -18,8 +18,8 @@ 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/plumbing/storer"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	"gopkg.in/src-d/hercules.v6/internal/toposort"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	"gopkg.in/src-d/hercules.v7/internal/toposort"
 )
 
 // ConfigurationOptionType represents the possible types of a ConfigurationOption's value.

+ 2 - 2
internal/core/pipeline_test.go

@@ -14,8 +14,8 @@ 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.v6/internal/pb"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 type testPipelineItem struct {

+ 1 - 1
internal/core/registry_test.go

@@ -9,7 +9,7 @@ import (
 	"github.com/spf13/pflag"
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/src-d/go-git.v4"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func getRegistry() *PipelineItemRegistry {

+ 4 - 4
internal/global_test.go

@@ -7,10 +7,10 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	uast_items "gopkg.in/src-d/hercules.v6/internal/plumbing/uast"
-	"gopkg.in/src-d/hercules.v6/internal/test"
-	"gopkg.in/src-d/hercules.v6/leaves"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	uast_items "gopkg.in/src-d/hercules.v7/internal/plumbing/uast"
+	"gopkg.in/src-d/hercules.v7/internal/test"
+	"gopkg.in/src-d/hercules.v7/leaves"
 )
 
 func TestPipelineSerialize(t *testing.T) {

+ 130 - 100
internal/pb/pb.pb.go

@@ -22,6 +22,7 @@ It has these top-level messages:
 	ShotnessAnalysisResults
 	FileHistory
 	FileHistoryResultMessage
+	LineStats
 	DevDay
 	DayDevs
 	DevsAnalysisResults
@@ -531,46 +532,70 @@ func (m *FileHistoryResultMessage) GetFiles() map[string]*FileHistory {
 	return nil
 }
 
-type DevDay struct {
-	Commits int32 `protobuf:"varint,1,opt,name=commits,proto3" json:"commits,omitempty"`
-	Added   int32 `protobuf:"varint,2,opt,name=added,proto3" json:"added,omitempty"`
-	Removed int32 `protobuf:"varint,3,opt,name=removed,proto3" json:"removed,omitempty"`
-	Changed int32 `protobuf:"varint,4,opt,name=changed,proto3" json:"changed,omitempty"`
+type LineStats struct {
+	Added   int32 `protobuf:"varint,1,opt,name=added,proto3" json:"added,omitempty"`
+	Removed int32 `protobuf:"varint,2,opt,name=removed,proto3" json:"removed,omitempty"`
+	Changed int32 `protobuf:"varint,3,opt,name=changed,proto3" json:"changed,omitempty"`
 }
 
-func (m *DevDay) Reset()                    { *m = DevDay{} }
-func (m *DevDay) String() string            { return proto.CompactTextString(m) }
-func (*DevDay) ProtoMessage()               {}
-func (*DevDay) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{14} }
+func (m *LineStats) Reset()                    { *m = LineStats{} }
+func (m *LineStats) String() string            { return proto.CompactTextString(m) }
+func (*LineStats) ProtoMessage()               {}
+func (*LineStats) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{14} }
 
-func (m *DevDay) GetCommits() int32 {
+func (m *LineStats) GetAdded() int32 {
 	if m != nil {
-		return m.Commits
+		return m.Added
 	}
 	return 0
 }
 
-func (m *DevDay) GetAdded() int32 {
+func (m *LineStats) GetRemoved() int32 {
 	if m != nil {
-		return m.Added
+		return m.Removed
 	}
 	return 0
 }
 
-func (m *DevDay) GetRemoved() int32 {
+func (m *LineStats) GetChanged() int32 {
 	if m != nil {
-		return m.Removed
+		return m.Changed
 	}
 	return 0
 }
 
-func (m *DevDay) GetChanged() int32 {
+type DevDay struct {
+	Commits   int32                 `protobuf:"varint,1,opt,name=commits,proto3" json:"commits,omitempty"`
+	Stats     *LineStats            `protobuf:"bytes,2,opt,name=stats" json:"stats,omitempty"`
+	Languages map[string]*LineStats `protobuf:"bytes,3,rep,name=languages" json:"languages,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
+}
+
+func (m *DevDay) Reset()                    { *m = DevDay{} }
+func (m *DevDay) String() string            { return proto.CompactTextString(m) }
+func (*DevDay) ProtoMessage()               {}
+func (*DevDay) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{15} }
+
+func (m *DevDay) GetCommits() int32 {
 	if m != nil {
-		return m.Changed
+		return m.Commits
 	}
 	return 0
 }
 
+func (m *DevDay) GetStats() *LineStats {
+	if m != nil {
+		return m.Stats
+	}
+	return nil
+}
+
+func (m *DevDay) GetLanguages() map[string]*LineStats {
+	if m != nil {
+		return m.Languages
+	}
+	return nil
+}
+
 type DayDevs struct {
 	Devs map[int32]*DevDay `protobuf:"bytes,1,rep,name=devs" json:"devs,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
 }
@@ -578,7 +603,7 @@ type DayDevs struct {
 func (m *DayDevs) Reset()                    { *m = DayDevs{} }
 func (m *DayDevs) String() string            { return proto.CompactTextString(m) }
 func (*DayDevs) ProtoMessage()               {}
-func (*DayDevs) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{15} }
+func (*DayDevs) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{16} }
 
 func (m *DayDevs) GetDevs() map[int32]*DevDay {
 	if m != nil {
@@ -595,7 +620,7 @@ type DevsAnalysisResults struct {
 func (m *DevsAnalysisResults) Reset()                    { *m = DevsAnalysisResults{} }
 func (m *DevsAnalysisResults) String() string            { return proto.CompactTextString(m) }
 func (*DevsAnalysisResults) ProtoMessage()               {}
-func (*DevsAnalysisResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{16} }
+func (*DevsAnalysisResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{17} }
 
 func (m *DevsAnalysisResults) GetDays() map[int32]*DayDevs {
 	if m != nil {
@@ -620,7 +645,7 @@ type Sentiment struct {
 func (m *Sentiment) Reset()                    { *m = Sentiment{} }
 func (m *Sentiment) String() string            { return proto.CompactTextString(m) }
 func (*Sentiment) ProtoMessage()               {}
-func (*Sentiment) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{17} }
+func (*Sentiment) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{18} }
 
 func (m *Sentiment) GetValue() float32 {
 	if m != nil {
@@ -650,7 +675,7 @@ type CommentSentimentResults struct {
 func (m *CommentSentimentResults) Reset()                    { *m = CommentSentimentResults{} }
 func (m *CommentSentimentResults) String() string            { return proto.CompactTextString(m) }
 func (*CommentSentimentResults) ProtoMessage()               {}
-func (*CommentSentimentResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{18} }
+func (*CommentSentimentResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{19} }
 
 func (m *CommentSentimentResults) GetSentimentByDay() map[int32]*Sentiment {
 	if m != nil {
@@ -668,7 +693,7 @@ type AnalysisResults struct {
 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{19} }
+func (*AnalysisResults) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{20} }
 
 func (m *AnalysisResults) GetHeader() *Metadata {
 	if m != nil {
@@ -699,6 +724,7 @@ func init() {
 	proto.RegisterType((*ShotnessAnalysisResults)(nil), "ShotnessAnalysisResults")
 	proto.RegisterType((*FileHistory)(nil), "FileHistory")
 	proto.RegisterType((*FileHistoryResultMessage)(nil), "FileHistoryResultMessage")
+	proto.RegisterType((*LineStats)(nil), "LineStats")
 	proto.RegisterType((*DevDay)(nil), "DevDay")
 	proto.RegisterType((*DayDevs)(nil), "DayDevs")
 	proto.RegisterType((*DevsAnalysisResults)(nil), "DevsAnalysisResults")
@@ -710,82 +736,86 @@ func init() {
 func init() { proto.RegisterFile("pb.proto", fileDescriptorPb) }
 
 var fileDescriptorPb = []byte{
-	// 1232 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0xcd, 0x72, 0x1b, 0x45,
-	0x10, 0xae, 0xd5, 0xbf, 0x7a, 0x25, 0x3b, 0x99, 0x98, 0x58, 0x11, 0xe5, 0x20, 0x96, 0x10, 0x0c,
-	0x09, 0x1b, 0x4a, 0xb9, 0x40, 0xb8, 0xc4, 0xb1, 0x49, 0xc5, 0x07, 0x03, 0x35, 0x4e, 0xe0, 0xb8,
-	0x35, 0xd6, 0xb6, 0xed, 0x05, 0xed, 0xac, 0x6a, 0x66, 0x57, 0xb6, 0x5e, 0x86, 0x1b, 0x55, 0x40,
-	0x15, 0xc5, 0x81, 0x17, 0xe0, 0x69, 0xb8, 0xf0, 0x12, 0xd4, 0xfc, 0x49, 0x2b, 0xd5, 0x3a, 0x70,
-	0xd9, 0xda, 0xee, 0xfe, 0x7a, 0xe6, 0xeb, 0x9f, 0xe9, 0x19, 0xe8, 0xcc, 0xce, 0xc2, 0x99, 0xc8,
-	0xf2, 0x2c, 0xf8, 0xbb, 0x06, 0x9d, 0x13, 0xcc, 0x59, 0xcc, 0x72, 0x46, 0x06, 0xd0, 0x9e, 0xa3,
-	0x90, 0x49, 0xc6, 0x07, 0xde, 0xc8, 0xdb, 0x6f, 0x52, 0x27, 0x12, 0x02, 0x8d, 0x4b, 0x26, 0x2f,
-	0x07, 0xb5, 0x91, 0xb7, 0xdf, 0xa5, 0xfa, 0x9f, 0xdc, 0x07, 0x10, 0x38, 0xcb, 0x64, 0x92, 0x67,
-	0x62, 0x31, 0xa8, 0x6b, 0x4b, 0x49, 0x43, 0x1e, 0xc2, 0xf6, 0x19, 0x5e, 0x24, 0x3c, 0x2a, 0x78,
-	0x72, 0x1d, 0xe5, 0x49, 0x8a, 0x83, 0xc6, 0xc8, 0xdb, 0xaf, 0xd3, 0xbe, 0x56, 0xbf, 0xe1, 0xc9,
-	0xf5, 0xeb, 0x24, 0x45, 0x12, 0x40, 0x1f, 0x79, 0x5c, 0x42, 0x35, 0x35, 0xca, 0x47, 0x1e, 0x2f,
-	0x31, 0x03, 0x68, 0x4f, 0xb2, 0x34, 0x4d, 0x72, 0x39, 0x68, 0x19, 0x66, 0x56, 0x24, 0xf7, 0xa0,
-	0x23, 0x0a, 0x6e, 0x1c, 0xdb, 0xda, 0xb1, 0x2d, 0x0a, 0xae, 0x9d, 0x5e, 0xc1, 0x6d, 0x67, 0x8a,
-	0x66, 0x28, 0xa2, 0x24, 0xc7, 0x74, 0xd0, 0x19, 0xd5, 0xf7, 0xfd, 0xf1, 0x5e, 0xe8, 0x82, 0x0e,
-	0xa9, 0x41, 0x7f, 0x8b, 0xe2, 0x38, 0xc7, 0xf4, 0x2b, 0x9e, 0x8b, 0x05, 0xdd, 0x12, 0x6b, 0xca,
-	0xe1, 0x01, 0xdc, 0xa9, 0x80, 0x91, 0x5b, 0x50, 0xff, 0x11, 0x17, 0x3a, 0x57, 0x5d, 0xaa, 0x7e,
-	0xc9, 0x0e, 0x34, 0xe7, 0x6c, 0x5a, 0xa0, 0x4e, 0x94, 0x47, 0x8d, 0xf0, 0xac, 0xf6, 0xb9, 0x17,
-	0x3c, 0x85, 0xdd, 0x17, 0x85, 0xe0, 0x71, 0x76, 0xc5, 0x4f, 0x67, 0x4c, 0x48, 0x3c, 0x61, 0xb9,
-	0x48, 0xae, 0x69, 0x76, 0x65, 0x82, 0x9b, 0x16, 0x29, 0x97, 0x03, 0x6f, 0x54, 0xdf, 0xef, 0x53,
-	0x27, 0x06, 0xbf, 0x79, 0xb0, 0x53, 0xe5, 0xa5, 0xea, 0xc1, 0x59, 0x8a, 0x76, 0x6b, 0xfd, 0x4f,
-	0x1e, 0xc0, 0x16, 0x2f, 0xd2, 0x33, 0x14, 0x51, 0x76, 0x1e, 0x89, 0xec, 0x4a, 0x6a, 0x12, 0x4d,
-	0xda, 0x33, 0xda, 0x6f, 0xce, 0x69, 0x76, 0x25, 0xc9, 0x27, 0x70, 0x7b, 0x85, 0x72, 0xdb, 0xd6,
-	0x35, 0x70, 0xdb, 0x01, 0x0f, 0x8d, 0x9a, 0x3c, 0x86, 0x86, 0x5e, 0xa7, 0xa1, 0x73, 0x36, 0x08,
-	0x6f, 0x08, 0x80, 0x6a, 0x54, 0xf0, 0x47, 0x6d, 0x15, 0xe2, 0x01, 0x67, 0xd3, 0x85, 0x4c, 0x24,
-	0x45, 0x59, 0x4c, 0x73, 0x49, 0x46, 0xe0, 0x5f, 0x08, 0xc6, 0x8b, 0x29, 0x13, 0x49, 0xbe, 0xb0,
-	0xdd, 0x55, 0x56, 0x91, 0x21, 0x74, 0x24, 0x4b, 0x67, 0xd3, 0x84, 0x5f, 0x58, 0xde, 0x4b, 0x99,
-	0x3c, 0x81, 0xf6, 0x4c, 0x64, 0x3f, 0xe0, 0x24, 0xd7, 0x4c, 0xfd, 0xf1, 0x3b, 0xd5, 0x54, 0x1c,
-	0x8a, 0x3c, 0x82, 0xe6, 0x79, 0x32, 0x45, 0xc7, 0xfc, 0x06, 0xb8, 0xc1, 0x90, 0x4f, 0xa1, 0x35,
-	0xc3, 0x6c, 0x36, 0x55, 0x8d, 0xf7, 0x16, 0xb4, 0x05, 0x91, 0x63, 0x20, 0xe6, 0x2f, 0x4a, 0x78,
-	0x8e, 0x82, 0x4d, 0x72, 0x75, 0x5e, 0x5a, 0x9a, 0xd7, 0x30, 0x3c, 0xcc, 0xd2, 0x99, 0x40, 0x29,
-	0x31, 0x36, 0xce, 0x34, 0xbb, 0xb2, 0xfe, 0xb7, 0x8d, 0xd7, 0xf1, 0xca, 0x29, 0xf8, 0xd3, 0x83,
-	0x7b, 0x37, 0x3a, 0x54, 0xd4, 0xd3, 0xfb, 0xbf, 0xf5, 0xac, 0x55, 0xd7, 0x93, 0x40, 0x43, 0xb5,
-	0xfc, 0xa0, 0x3e, 0xaa, 0xef, 0xd7, 0x69, 0xc3, 0x9d, 0xf9, 0x84, 0xc7, 0xc9, 0xc4, 0x26, 0xab,
-	0x49, 0x9d, 0x48, 0xee, 0x42, 0x2b, 0xe1, 0xf1, 0x2c, 0x17, 0x3a, 0x2f, 0x75, 0x6a, 0xa5, 0xe0,
-	0x14, 0xda, 0x87, 0x59, 0x31, 0x53, 0xa9, 0xdb, 0x81, 0x66, 0xc2, 0x63, 0xbc, 0xd6, 0x7d, 0xdb,
-	0xa5, 0x46, 0x20, 0x63, 0x68, 0xa5, 0x3a, 0x04, 0xcd, 0xe3, 0xed, 0x59, 0xb1, 0xc8, 0xe0, 0x01,
-	0xf4, 0x5e, 0x67, 0xc5, 0xe4, 0x12, 0xe3, 0x97, 0x89, 0x5d, 0xd9, 0x54, 0xd0, 0xd3, 0xa4, 0x8c,
-	0x10, 0xfc, 0xe2, 0xc1, 0x5d, 0xbb, 0xf7, 0x66, 0x87, 0x3d, 0x82, 0x9e, 0xc2, 0x44, 0x13, 0x63,
-	0xb6, 0x05, 0xe9, 0x84, 0x16, 0x4e, 0x7d, 0x65, 0x75, 0xbc, 0x9f, 0xc0, 0x96, 0xad, 0xa1, 0x83,
-	0xb7, 0x37, 0xe0, 0x7d, 0x63, 0x77, 0x0e, 0x9f, 0x41, 0xcf, 0x3a, 0x18, 0x56, 0x66, 0x8a, 0xf4,
-	0xc3, 0x32, 0x67, 0xea, 0x1b, 0x88, 0x16, 0x82, 0x9f, 0x3d, 0x80, 0x37, 0x07, 0xa7, 0xaf, 0x0f,
-	0x2f, 0x19, 0xbf, 0x40, 0xf2, 0x2e, 0x74, 0x35, 0xbd, 0xd2, 0xa9, 0xed, 0x28, 0xc5, 0xd7, 0xea,
-	0xe4, 0xee, 0x01, 0x48, 0x31, 0x89, 0xce, 0xf0, 0x3c, 0x13, 0x68, 0x67, 0x6c, 0x57, 0x8a, 0xc9,
-	0x0b, 0xad, 0x50, 0xbe, 0xca, 0xcc, 0xce, 0x73, 0x14, 0x76, 0xce, 0x76, 0xa4, 0x98, 0x1c, 0x28,
-	0x99, 0xbc, 0x07, 0x7e, 0xc1, 0x64, 0xee, 0x9c, 0x1b, 0x66, 0x0c, 0x2b, 0x95, 0xf5, 0xde, 0x03,
-	0x2d, 0x59, 0xf7, 0xa6, 0x59, 0x5c, 0x69, 0xb4, 0x7f, 0xf0, 0x1c, 0x76, 0x57, 0x34, 0xe5, 0x29,
-	0x9b, 0xa3, 0x70, 0x29, 0xfd, 0x10, 0xda, 0x13, 0xa3, 0xd6, 0x55, 0xf0, 0xc7, 0x7e, 0xb8, 0x82,
-	0x52, 0x67, 0x0b, 0xfe, 0xf1, 0x60, 0xeb, 0xf4, 0x32, 0xcb, 0x39, 0x4a, 0x49, 0x71, 0x92, 0x89,
-	0x98, 0x7c, 0x00, 0x7d, 0x7d, 0x38, 0x38, 0x9b, 0x46, 0x22, 0x9b, 0xba, 0x88, 0x7b, 0x4e, 0x49,
-	0xb3, 0x29, 0xaa, 0x12, 0x2b, 0x9b, 0xea, 0x56, 0x5d, 0x62, 0x2d, 0x2c, 0x27, 0x5b, 0xbd, 0x34,
-	0xd9, 0x08, 0x34, 0x54, 0xae, 0x6c, 0x70, 0xfa, 0x9f, 0x7c, 0x01, 0x9d, 0x49, 0x56, 0xa8, 0xf5,
-	0xa4, 0x3d, 0xb7, 0x7b, 0xe1, 0x3a, 0x0b, 0x55, 0x4b, 0x6d, 0x37, 0x33, 0x7d, 0x09, 0x1f, 0x7e,
-	0x09, 0xfd, 0x35, 0x53, 0x79, 0x8e, 0x37, 0x2b, 0xe6, 0x78, 0xb3, 0x3c, 0xc7, 0x8f, 0x60, 0xd7,
-	0x6d, 0xb3, 0xd9, 0x82, 0x1f, 0x43, 0x5b, 0xe8, 0x9d, 0x5d, 0xbe, 0xb6, 0x37, 0x18, 0x51, 0x67,
-	0x0f, 0x3e, 0x02, 0x5f, 0xb5, 0xc9, 0xab, 0x44, 0xea, 0xab, 0xb2, 0x74, 0xbd, 0x99, 0x93, 0xe4,
-	0xc4, 0xe0, 0x27, 0x0f, 0x06, 0x25, 0xa4, 0xd9, 0xea, 0x04, 0xa5, 0x64, 0x17, 0x48, 0x9e, 0x95,
-	0x0f, 0x89, 0x3f, 0x7e, 0x10, 0xde, 0x84, 0xd4, 0x06, 0x9b, 0x07, 0xe3, 0x32, 0x7c, 0x09, 0xb0,
-	0x52, 0x56, 0xdc, 0x64, 0x41, 0x39, 0x03, 0xfe, 0xb8, 0xb7, 0xb6, 0x76, 0x29, 0x1f, 0x53, 0x68,
-	0x1d, 0xe1, 0xfc, 0x88, 0x6d, 0x04, 0xb1, 0x76, 0x47, 0xef, 0x40, 0x93, 0xc5, 0x31, 0xc6, 0x2e,
-	0x9b, 0x5a, 0x50, 0x78, 0x81, 0x69, 0x36, 0xc7, 0xd8, 0xde, 0x3f, 0x4e, 0xd4, 0x2b, 0xe9, 0xe6,
-	0x8a, 0x75, 0xc9, 0x9b, 0xae, 0xd7, 0xe2, 0x40, 0x42, 0xfb, 0x88, 0x2d, 0x8e, 0x70, 0x2e, 0xc9,
-	0x43, 0x68, 0xc4, 0x38, 0x77, 0xb1, 0x93, 0xd0, 0xea, 0x43, 0xf5, 0x31, 0x91, 0x6a, 0xfb, 0xf0,
-	0x39, 0x74, 0x97, 0xaa, 0x8a, 0x4a, 0xef, 0xad, 0xc7, 0xd9, 0x0e, 0x4d, 0x34, 0xe5, 0x10, 0x7f,
-	0xf5, 0xe0, 0x8e, 0x5a, 0x62, 0xb3, 0xde, 0x63, 0x35, 0x4e, 0x17, 0x8e, 0xc1, 0xfd, 0xb0, 0x02,
-	0xa3, 0x58, 0x2d, 0xd9, 0xb0, 0x85, 0x54, 0x67, 0x39, 0xc6, 0x79, 0x64, 0xa6, 0x66, 0x4d, 0xd7,
-	0xba, 0x13, 0xe3, 0xfc, 0x58, 0xc9, 0xc3, 0x03, 0xe8, 0x2e, 0xf1, 0x15, 0x54, 0xef, 0xaf, 0x53,
-	0xed, 0xb8, 0x90, 0xcb, 0x5c, 0xbf, 0x87, 0xee, 0x29, 0x72, 0xf5, 0xe4, 0xe1, 0xf9, 0xaa, 0x8b,
-	0xd5, 0x22, 0x35, 0x0b, 0x53, 0x37, 0xad, 0x2a, 0x0c, 0xf2, 0x5c, 0x3a, 0x06, 0x4e, 0x2e, 0xd7,
-	0xb0, 0xbe, 0xde, 0x88, 0x7f, 0x79, 0xb0, 0x7b, 0x68, 0x60, 0xcb, 0x0d, 0x5c, 0x22, 0xbe, 0x83,
-	0x5b, 0xd2, 0xe9, 0xa2, 0xb3, 0x45, 0x14, 0xb3, 0x85, 0x4d, 0xca, 0xe3, 0xf0, 0x06, 0x9f, 0x70,
-	0xa9, 0x78, 0xb1, 0x38, 0x62, 0x0b, 0xfb, 0xec, 0x92, 0x6b, 0xca, 0xe1, 0x09, 0xdc, 0xa9, 0x80,
-	0x55, 0x64, 0x66, 0xb4, 0x9e, 0x19, 0x58, 0xad, 0x5e, 0xce, 0xcd, 0xef, 0x1e, 0x6c, 0x6f, 0xd6,
-	0xf0, 0x7d, 0x68, 0x5d, 0x22, 0x8b, 0x51, 0xe8, 0xe5, 0xfc, 0x71, 0x77, 0xf9, 0x30, 0xa4, 0xd6,
-	0x40, 0x9e, 0xa9, 0x7c, 0xf1, 0x7c, 0x99, 0x2f, 0x55, 0xea, 0xcd, 0x32, 0x1f, 0x5a, 0xc0, 0x72,
-	0xd4, 0x18, 0xd1, 0x8c, 0x9a, 0x92, 0xe9, 0xbf, 0x9e, 0x8c, 0xbd, 0x12, 0xdf, 0xb3, 0x96, 0x7e,
-	0xa2, 0x3f, 0xfd, 0x37, 0x00, 0x00, 0xff, 0xff, 0xc2, 0x05, 0x8d, 0xb5, 0xae, 0x0b, 0x00, 0x00,
+	// 1294 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x57, 0xdf, 0x72, 0xdb, 0xc4,
+	0x17, 0x1e, 0x59, 0xfe, 0x7b, 0x64, 0x27, 0xed, 0xb6, 0xbf, 0xc6, 0xf5, 0x6f, 0x52, 0x8c, 0x28,
+	0x25, 0xd0, 0xa2, 0x32, 0x2e, 0x17, 0x50, 0x6e, 0x9a, 0x26, 0x74, 0x9a, 0x99, 0x06, 0x98, 0x75,
+	0x03, 0x97, 0x9a, 0x8d, 0xb5, 0x49, 0x04, 0xd6, 0xca, 0xb3, 0x2b, 0x39, 0xf1, 0xcb, 0x70, 0xc7,
+	0x0c, 0x30, 0xc3, 0x70, 0xc1, 0x0b, 0x70, 0xc1, 0xb3, 0x70, 0xc3, 0x4b, 0x30, 0xfb, 0x4f, 0x96,
+	0x8c, 0x52, 0xb8, 0xd3, 0x39, 0xe7, 0x3b, 0xbb, 0xdf, 0x7e, 0xe7, 0xec, 0x91, 0x04, 0xdd, 0xc5,
+	0x69, 0xb0, 0xe0, 0x69, 0x96, 0xfa, 0x7f, 0x36, 0xa0, 0x7b, 0x4c, 0x33, 0x12, 0x91, 0x8c, 0xa0,
+	0x21, 0x74, 0x96, 0x94, 0x8b, 0x38, 0x65, 0x43, 0x67, 0xec, 0xec, 0xb5, 0xb0, 0x35, 0x11, 0x82,
+	0xe6, 0x05, 0x11, 0x17, 0xc3, 0xc6, 0xd8, 0xd9, 0xeb, 0x61, 0xf5, 0x8c, 0xee, 0x01, 0x70, 0xba,
+	0x48, 0x45, 0x9c, 0xa5, 0x7c, 0x35, 0x74, 0x55, 0xa4, 0xe4, 0x41, 0x0f, 0x60, 0xfb, 0x94, 0x9e,
+	0xc7, 0x2c, 0xcc, 0x59, 0x7c, 0x15, 0x66, 0x71, 0x42, 0x87, 0xcd, 0xb1, 0xb3, 0xe7, 0xe2, 0x81,
+	0x72, 0x9f, 0xb0, 0xf8, 0xea, 0x75, 0x9c, 0x50, 0xe4, 0xc3, 0x80, 0xb2, 0xa8, 0x84, 0x6a, 0x29,
+	0x94, 0x47, 0x59, 0x54, 0x60, 0x86, 0xd0, 0x99, 0xa5, 0x49, 0x12, 0x67, 0x62, 0xd8, 0xd6, 0xcc,
+	0x8c, 0x89, 0xee, 0x42, 0x97, 0xe7, 0x4c, 0x27, 0x76, 0x54, 0x62, 0x87, 0xe7, 0x4c, 0x25, 0xbd,
+	0x84, 0x9b, 0x36, 0x14, 0x2e, 0x28, 0x0f, 0xe3, 0x8c, 0x26, 0xc3, 0xee, 0xd8, 0xdd, 0xf3, 0x26,
+	0xbb, 0x81, 0x3d, 0x74, 0x80, 0x35, 0xfa, 0x2b, 0xca, 0x8f, 0x32, 0x9a, 0x7c, 0xce, 0x32, 0xbe,
+	0xc2, 0x5b, 0xbc, 0xe2, 0x1c, 0xed, 0xc3, 0xad, 0x1a, 0x18, 0xba, 0x01, 0xee, 0x77, 0x74, 0xa5,
+	0xb4, 0xea, 0x61, 0xf9, 0x88, 0x6e, 0x43, 0x6b, 0x49, 0xe6, 0x39, 0x55, 0x42, 0x39, 0x58, 0x1b,
+	0x4f, 0x1b, 0x9f, 0x38, 0xfe, 0x13, 0xd8, 0x79, 0x9e, 0x73, 0x16, 0xa5, 0x97, 0x6c, 0xba, 0x20,
+	0x5c, 0xd0, 0x63, 0x92, 0xf1, 0xf8, 0x0a, 0xa7, 0x97, 0xfa, 0x70, 0xf3, 0x3c, 0x61, 0x62, 0xe8,
+	0x8c, 0xdd, 0xbd, 0x01, 0xb6, 0xa6, 0xff, 0xb3, 0x03, 0xb7, 0xeb, 0xb2, 0x64, 0x3d, 0x18, 0x49,
+	0xa8, 0xd9, 0x5a, 0x3d, 0xa3, 0xfb, 0xb0, 0xc5, 0xf2, 0xe4, 0x94, 0xf2, 0x30, 0x3d, 0x0b, 0x79,
+	0x7a, 0x29, 0x14, 0x89, 0x16, 0xee, 0x6b, 0xef, 0x97, 0x67, 0x38, 0xbd, 0x14, 0xe8, 0x03, 0xb8,
+	0xb9, 0x46, 0xd9, 0x6d, 0x5d, 0x05, 0xdc, 0xb6, 0xc0, 0x03, 0xed, 0x46, 0x8f, 0xa0, 0xa9, 0xd6,
+	0x69, 0x2a, 0xcd, 0x86, 0xc1, 0x35, 0x07, 0xc0, 0x0a, 0xe5, 0xff, 0xda, 0x58, 0x1f, 0x71, 0x9f,
+	0x91, 0xf9, 0x4a, 0xc4, 0x02, 0x53, 0x91, 0xcf, 0x33, 0x81, 0xc6, 0xe0, 0x9d, 0x73, 0xc2, 0xf2,
+	0x39, 0xe1, 0x71, 0xb6, 0x32, 0xdd, 0x55, 0x76, 0xa1, 0x11, 0x74, 0x05, 0x49, 0x16, 0xf3, 0x98,
+	0x9d, 0x1b, 0xde, 0x85, 0x8d, 0x1e, 0x43, 0x67, 0xc1, 0xd3, 0x6f, 0xe9, 0x2c, 0x53, 0x4c, 0xbd,
+	0xc9, 0xff, 0xea, 0xa9, 0x58, 0x14, 0x7a, 0x08, 0xad, 0xb3, 0x78, 0x4e, 0x2d, 0xf3, 0x6b, 0xe0,
+	0x1a, 0x83, 0x3e, 0x84, 0xf6, 0x82, 0xa6, 0x8b, 0xb9, 0x6c, 0xbc, 0x37, 0xa0, 0x0d, 0x08, 0x1d,
+	0x01, 0xd2, 0x4f, 0x61, 0xcc, 0x32, 0xca, 0xc9, 0x2c, 0x93, 0xf7, 0xa5, 0xad, 0x78, 0x8d, 0x82,
+	0x83, 0x34, 0x59, 0x70, 0x2a, 0x04, 0x8d, 0x74, 0x32, 0x4e, 0x2f, 0x4d, 0xfe, 0x4d, 0x9d, 0x75,
+	0xb4, 0x4e, 0xf2, 0x7f, 0x73, 0xe0, 0xee, 0xb5, 0x09, 0x35, 0xf5, 0x74, 0xfe, 0x6b, 0x3d, 0x1b,
+	0xf5, 0xf5, 0x44, 0xd0, 0x94, 0x2d, 0x3f, 0x74, 0xc7, 0xee, 0x9e, 0x8b, 0x9b, 0xf6, 0xce, 0xc7,
+	0x2c, 0x8a, 0x67, 0x46, 0xac, 0x16, 0xb6, 0x26, 0xba, 0x03, 0xed, 0x98, 0x45, 0x8b, 0x8c, 0x2b,
+	0x5d, 0x5c, 0x6c, 0x2c, 0x7f, 0x0a, 0x9d, 0x83, 0x34, 0x5f, 0x48, 0xe9, 0x6e, 0x43, 0x2b, 0x66,
+	0x11, 0xbd, 0x52, 0x7d, 0xdb, 0xc3, 0xda, 0x40, 0x13, 0x68, 0x27, 0xea, 0x08, 0x8a, 0xc7, 0x9b,
+	0x55, 0x31, 0x48, 0xff, 0x3e, 0xf4, 0x5f, 0xa7, 0xf9, 0xec, 0x82, 0x46, 0x2f, 0x62, 0xb3, 0xb2,
+	0xae, 0xa0, 0xa3, 0x48, 0x69, 0xc3, 0xff, 0xd1, 0x81, 0x3b, 0x66, 0xef, 0xcd, 0x0e, 0x7b, 0x08,
+	0x7d, 0x89, 0x09, 0x67, 0x3a, 0x6c, 0x0a, 0xd2, 0x0d, 0x0c, 0x1c, 0x7b, 0x32, 0x6a, 0x79, 0x3f,
+	0x86, 0x2d, 0x53, 0x43, 0x0b, 0xef, 0x6c, 0xc0, 0x07, 0x3a, 0x6e, 0x13, 0x3e, 0x82, 0xbe, 0x49,
+	0xd0, 0xac, 0xf4, 0x14, 0x19, 0x04, 0x65, 0xce, 0xd8, 0xd3, 0x10, 0x65, 0xf8, 0x3f, 0x38, 0x00,
+	0x27, 0xfb, 0xd3, 0xd7, 0x07, 0x17, 0x84, 0x9d, 0x53, 0xf4, 0x7f, 0xe8, 0x29, 0x7a, 0xa5, 0x5b,
+	0xdb, 0x95, 0x8e, 0x2f, 0xe4, 0xcd, 0xdd, 0x05, 0x10, 0x7c, 0x16, 0x9e, 0xd2, 0xb3, 0x94, 0x53,
+	0x33, 0x63, 0x7b, 0x82, 0xcf, 0x9e, 0x2b, 0x87, 0xcc, 0x95, 0x61, 0x72, 0x96, 0x51, 0x6e, 0xe6,
+	0x6c, 0x57, 0xf0, 0xd9, 0xbe, 0xb4, 0xd1, 0x5b, 0xe0, 0xe5, 0x44, 0x64, 0x36, 0xb9, 0xa9, 0xc7,
+	0xb0, 0x74, 0x99, 0xec, 0x5d, 0x50, 0x96, 0x49, 0x6f, 0xe9, 0xc5, 0xa5, 0x47, 0xe5, 0xfb, 0xcf,
+	0x60, 0x67, 0x4d, 0x53, 0x4c, 0xc9, 0x92, 0x72, 0x2b, 0xe9, 0xbb, 0xd0, 0x99, 0x69, 0xb7, 0xaa,
+	0x82, 0x37, 0xf1, 0x82, 0x35, 0x14, 0xdb, 0x98, 0xff, 0x97, 0x03, 0x5b, 0xd3, 0x8b, 0x34, 0x63,
+	0x54, 0x08, 0x4c, 0x67, 0x29, 0x8f, 0xd0, 0x3b, 0x30, 0x50, 0x97, 0x83, 0x91, 0x79, 0xc8, 0xd3,
+	0xb9, 0x3d, 0x71, 0xdf, 0x3a, 0x71, 0x3a, 0xa7, 0xb2, 0xc4, 0x32, 0x26, 0xbb, 0x55, 0x95, 0x58,
+	0x19, 0xc5, 0x64, 0x73, 0x4b, 0x93, 0x0d, 0x41, 0x53, 0x6a, 0x65, 0x0e, 0xa7, 0x9e, 0xd1, 0xa7,
+	0xd0, 0x9d, 0xa5, 0xb9, 0x5c, 0x4f, 0x98, 0x7b, 0xbb, 0x1b, 0x54, 0x59, 0xc8, 0x5a, 0xaa, 0xb8,
+	0x9e, 0xe9, 0x05, 0x7c, 0xf4, 0x19, 0x0c, 0x2a, 0xa1, 0xf2, 0x1c, 0x6f, 0xd5, 0xcc, 0xf1, 0x56,
+	0x79, 0x8e, 0x1f, 0xc2, 0x8e, 0xdd, 0x66, 0xb3, 0x05, 0xdf, 0x87, 0x0e, 0x57, 0x3b, 0x5b, 0xbd,
+	0xb6, 0x37, 0x18, 0x61, 0x1b, 0xf7, 0xdf, 0x03, 0x4f, 0xb6, 0xc9, 0xcb, 0x58, 0xa8, 0x57, 0x65,
+	0xe9, 0xf5, 0xa6, 0x6f, 0x92, 0x35, 0xfd, 0xef, 0x1d, 0x18, 0x96, 0x90, 0x7a, 0xab, 0x63, 0x2a,
+	0x04, 0x39, 0xa7, 0xe8, 0x69, 0xf9, 0x92, 0x78, 0x93, 0xfb, 0xc1, 0x75, 0x48, 0x15, 0x30, 0x3a,
+	0xe8, 0x94, 0xd1, 0x0b, 0x80, 0xb5, 0xb3, 0xe6, 0x4d, 0xe6, 0x97, 0x15, 0xf0, 0x26, 0xfd, 0xca,
+	0xda, 0x25, 0x3d, 0x4e, 0xa0, 0xf7, 0x2a, 0x66, 0x74, 0x9a, 0x91, 0x4c, 0xdd, 0x5a, 0x12, 0x45,
+	0x34, 0x32, 0x52, 0x6a, 0x43, 0x9e, 0x8e, 0xd3, 0x24, 0x5d, 0xd2, 0xc8, 0xc8, 0x69, 0x4d, 0x75,
+	0x6e, 0xd5, 0x45, 0x91, 0x79, 0x05, 0x59, 0xd3, 0xff, 0xc3, 0x81, 0xf6, 0x21, 0x5d, 0x1e, 0x92,
+	0x0d, 0x71, 0x2a, 0xef, 0xfe, 0x31, 0xb4, 0x84, 0xdc, 0xd7, 0x70, 0x84, 0xa0, 0x60, 0x82, 0x75,
+	0x00, 0x7d, 0x0c, 0xbd, 0x39, 0x61, 0xe7, 0x39, 0x91, 0x4d, 0xec, 0x2a, 0x95, 0xee, 0x04, 0x7a,
+	0xdd, 0xe0, 0x95, 0x0d, 0x68, 0x5d, 0xd6, 0xc0, 0xd1, 0x4b, 0xd8, 0xaa, 0x06, 0x6b, 0xf4, 0x19,
+	0x57, 0xf5, 0xa9, 0xec, 0xbd, 0x56, 0x47, 0x40, 0xe7, 0x90, 0xac, 0x0e, 0xe9, 0x52, 0xa0, 0x07,
+	0xd0, 0x8c, 0xe8, 0xd2, 0xd6, 0x0a, 0x05, 0xc6, 0x2f, 0xd9, 0x18, 0x06, 0x2a, 0x3e, 0x7a, 0x06,
+	0xbd, 0xc2, 0x55, 0xd3, 0x99, 0xbb, 0xd5, 0x7d, 0x3b, 0xe6, 0x34, 0xe5, 0x4d, 0x7f, 0x72, 0xe0,
+	0x96, 0x5c, 0x62, 0xb3, 0x3f, 0x27, 0x72, 0xfc, 0xaf, 0x2c, 0x83, 0x7b, 0x41, 0x0d, 0x46, 0xb2,
+	0x2a, 0xd8, 0x90, 0x95, 0x90, 0xb3, 0x27, 0xa2, 0xcb, 0x50, 0x4f, 0xf9, 0x86, 0xea, 0xcd, 0x6e,
+	0x44, 0x97, 0x47, 0xd2, 0x1e, 0xed, 0x43, 0xaf, 0xc0, 0xd7, 0x50, 0xbd, 0x57, 0xa5, 0xda, 0xb5,
+	0x47, 0x2e, 0x73, 0xfd, 0x06, 0x7a, 0x53, 0xca, 0xe4, 0x27, 0x1a, 0xcb, 0xd6, 0xb7, 0x4e, 0x2e,
+	0xd2, 0x30, 0x30, 0xf9, 0x65, 0x20, 0x0b, 0x4e, 0x59, 0x26, 0x2c, 0x03, 0x6b, 0x97, 0x7b, 0xc3,
+	0xad, 0x5e, 0x9c, 0xdf, 0x1d, 0xd8, 0x39, 0xd0, 0xb0, 0x62, 0x03, 0x2b, 0xc4, 0xd7, 0x70, 0x43,
+	0x58, 0x5f, 0x78, 0xba, 0x0a, 0x23, 0xb2, 0x32, 0xa2, 0x3c, 0x0a, 0xae, 0xc9, 0x09, 0x0a, 0xc7,
+	0xf3, 0xd5, 0x21, 0x59, 0x99, 0xcf, 0x44, 0x51, 0x71, 0x8e, 0x8e, 0xe1, 0x56, 0x0d, 0xac, 0x46,
+	0x99, 0x7f, 0x34, 0xcf, 0x7a, 0xbb, 0x92, 0x36, 0xbf, 0x38, 0xb0, 0xbd, 0x59, 0xc3, 0xb7, 0xa1,
+	0x7d, 0x41, 0x49, 0x44, 0xb9, 0x5a, 0xce, 0x9b, 0xf4, 0x8a, 0x0f, 0x59, 0x6c, 0x02, 0xe8, 0xa9,
+	0xd4, 0x8b, 0x65, 0x85, 0x5e, 0xb2, 0xd4, 0x9b, 0x65, 0x3e, 0x30, 0x80, 0x62, 0x34, 0x6a, 0x53,
+	0x8f, 0xc6, 0x52, 0xe8, 0xdf, 0x3e, 0x71, 0xfb, 0x25, 0xbe, 0xa7, 0x6d, 0xf5, 0x4b, 0xf1, 0xe4,
+	0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x31, 0x05, 0x6a, 0xf9, 0x5e, 0x0c, 0x00, 0x00,
 }

+ 8 - 3
internal/pb/pb.proto

@@ -107,11 +107,16 @@ message FileHistoryResultMessage {
     map<string, FileHistory> files = 1;
 }
 
+message LineStats {
+    int32 added = 1;
+    int32 removed = 2;
+    int32 changed = 3;
+}
+
 message DevDay {
     int32 commits = 1;
-    int32 added = 2;
-    int32 removed = 3;
-    int32 changed = 4;
+    LineStats stats = 2;
+    map<string, LineStats> languages = 3;
 }
 
 message DayDevs {

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 126 - 30
internal/pb/pb_pb2.py


+ 2 - 2
internal/plumbing/blob_cache.go

@@ -13,8 +13,8 @@ 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.v6/internal"
-	"gopkg.in/src-d/hercules.v6/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal"
+	"gopkg.in/src-d/hercules.v7/internal/core"
 )
 
 // ErrorBinary is raised in CachedBlob.CountLines() if the file is binary.

+ 3 - 3
internal/plumbing/blob_cache_test.go

@@ -6,9 +6,9 @@ import (
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureBlobCache() *BlobCache {

+ 1 - 1
internal/plumbing/day.go

@@ -6,7 +6,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.v6/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/core"
 )
 
 // DaysSinceStart provides the relative date information for every commit.

+ 2 - 2
internal/plumbing/day_test.go

@@ -5,8 +5,8 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/src-d/go-git.v4/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureDaysSinceStart() *DaysSinceStart {

+ 1 - 1
internal/plumbing/diff.go

@@ -8,7 +8,7 @@ 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.v6/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/core"
 )
 
 // FileDiff calculates the difference of files which were modified.

+ 6 - 6
internal/plumbing/diff_test.go

@@ -8,12 +8,12 @@ import (
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6"
-	"gopkg.in/src-d/hercules.v6/internal"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/test"
-	"gopkg.in/src-d/hercules.v6/internal/test/fixtures"
+	"gopkg.in/src-d/hercules.v7"
+	"gopkg.in/src-d/hercules.v7/internal"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/test/fixtures"
 )
 
 func TestFileDiffMeta(t *testing.T) {

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

@@ -9,7 +9,7 @@ import (
 	"github.com/pkg/errors"
 	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/core"
 )
 
 // Detector determines the author of a commit. Same person can commit under different

+ 2 - 2
internal/plumbing/identity/identity_test.go

@@ -14,8 +14,8 @@ 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/plumbing/storer"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureIdentityDetector() *Detector {

+ 109 - 0
internal/plumbing/languages.go

@@ -0,0 +1,109 @@
+package plumbing
+
+import (
+	"path"
+
+	"gopkg.in/src-d/enry.v1"
+	"gopkg.in/src-d/go-git.v4/utils/merkletrie"
+
+	"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.v7/internal/core"
+)
+
+// LanguagesDetection run programming language detection over the changed files.
+type LanguagesDetection struct {
+	core.NoopMerger
+}
+
+const (
+	// DependencyLanguages is the name of the dependency provided by LanguagesDetection.
+	DependencyLanguages = "languages"
+)
+
+// Name of this PipelineItem. Uniquely identifies the type, used for mapping keys, etc.
+func (langs *LanguagesDetection) Name() string {
+	return "LanguagesDetection"
+}
+
+// Provides returns the list of names of entities which are produced by this PipelineItem.
+// Each produced entity will be inserted into `deps` of dependent Consume()-s according
+// to this list. Also used by core.Registry to build the global map of providers.
+func (langs *LanguagesDetection) Provides() []string {
+	arr := [...]string{DependencyLanguages}
+	return arr[:]
+}
+
+// Requires returns the list of names of entities which are needed by this PipelineItem.
+// Each requested entity will be inserted into `deps` of Consume(). In turn, those
+// entities are Provides() upstream.
+func (langs *LanguagesDetection) Requires() []string {
+	arr := [...]string{DependencyTreeChanges, DependencyBlobCache}
+	return arr[:]
+}
+
+// ListConfigurationOptions returns the list of changeable public properties of this PipelineItem.
+func (langs *LanguagesDetection) ListConfigurationOptions() []core.ConfigurationOption {
+	return []core.ConfigurationOption{}
+}
+
+// Configure sets the properties previously published by ListConfigurationOptions().
+func (langs *LanguagesDetection) Configure(facts map[string]interface{}) error {
+	return nil
+}
+
+// Initialize resets the temporary caches and prepares this PipelineItem for a series of Consume()
+// calls. The repository which is going to be analysed is supplied as an argument.
+func (langs *LanguagesDetection) Initialize(repository *git.Repository) error {
+	return nil
+}
+
+// Consume runs this PipelineItem on the next commit 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 (langs *LanguagesDetection) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
+	changes := deps[DependencyTreeChanges].(object.Changes)
+	cache := deps[DependencyBlobCache].(map[plumbing.Hash]*CachedBlob)
+	result := map[plumbing.Hash]string{}
+	for _, change := range changes {
+		action, err := change.Action()
+		if err != nil {
+			return nil, err
+		}
+		switch action {
+		case merkletrie.Insert:
+			result[change.To.TreeEntry.Hash] = langs.detectLanguage(
+				change.To.Name, cache[change.To.TreeEntry.Hash])
+		case merkletrie.Delete:
+			result[change.From.TreeEntry.Hash] = langs.detectLanguage(
+				change.From.Name, cache[change.From.TreeEntry.Hash])
+		case merkletrie.Modify:
+			result[change.To.TreeEntry.Hash] = langs.detectLanguage(
+				change.To.Name, cache[change.To.TreeEntry.Hash])
+			result[change.From.TreeEntry.Hash] = langs.detectLanguage(
+				change.From.Name, cache[change.From.TreeEntry.Hash])
+		}
+	}
+	return map[string]interface{}{DependencyLanguages: result}, nil
+}
+
+// Fork clones this PipelineItem.
+func (langs *LanguagesDetection) Fork(n int) []core.PipelineItem {
+	return core.ForkSamePipelineItem(langs, n)
+}
+
+// detectLanguage returns the programming language of a blob.
+func (langs *LanguagesDetection) detectLanguage(name string, blob *CachedBlob) string {
+	_, err := blob.CountLines()
+	if err == ErrorBinary {
+		return ""
+	}
+	return enry.GetLanguage(path.Base(name), blob.Data)
+}
+
+func init() {
+	core.Registry.Register(&LanguagesDetection{})
+}

+ 105 - 0
internal/plumbing/languages_test.go

@@ -0,0 +1,105 @@
+package plumbing
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/src-d/go-git.v4/plumbing"
+	"gopkg.in/src-d/go-git.v4/plumbing/object"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/test"
+)
+
+func TestLanguagesDetectionMeta(t *testing.T) {
+	ls := &LanguagesDetection{}
+	assert.Equal(t, ls.Name(), "LanguagesDetection")
+	assert.Equal(t, len(ls.Provides()), 1)
+	assert.Equal(t, ls.Provides()[0], DependencyLanguages)
+	assert.Equal(t, len(ls.Requires()), 2)
+	assert.Equal(t, ls.Requires()[0], DependencyTreeChanges)
+	assert.Equal(t, ls.Requires()[1], DependencyBlobCache)
+	opts := ls.ListConfigurationOptions()
+	assert.Len(t, opts, 0)
+	assert.Nil(t, ls.Configure(nil))
+	assert.Nil(t, ls.Initialize(nil))
+}
+
+func TestLanguagesDetectionRegistration(t *testing.T) {
+	summoned := core.Registry.Summon((&LanguagesDetection{}).Name())
+	assert.Len(t, summoned, 1)
+	assert.Equal(t, summoned[0].Name(), "LanguagesDetection")
+	summoned = core.Registry.Summon((&LanguagesDetection{}).Provides()[0])
+	assert.True(t, len(summoned) >= 1)
+	matched := false
+	for _, tp := range summoned {
+		matched = matched || tp.Name() == "LanguagesDetection"
+	}
+	assert.True(t, matched)
+}
+
+func TestLanguagesDetectionConsume(t *testing.T) {
+	ls := &LanguagesDetection{}
+	changes := make(object.Changes, 3)
+	// 2b1ed978194a94edeabbca6de7ff3b5771d4d665
+	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
+		"96c6ece9b2f3c7c51b83516400d278dea5605100"))
+	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
+		"251f2094d7b523d5bcc60e663b6cf38151bf8844"))
+	changes[0] = &object.Change{From: object.ChangeEntry{
+		Name: "analyser.go",
+		Tree: treeFrom,
+		TreeEntry: object.TreeEntry{
+			Name: "analyser.go",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1"),
+		},
+	}, To: object.ChangeEntry{},
+	}
+	changes[1] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
+		Name: "burndown.bin",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: "burndown.bin",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("29c9fafd6a2fae8cd20298c3f60115bc31a4c0f2"),
+		},
+	},
+	}
+	changes[2] = &object.Change{From: object.ChangeEntry{
+		Name: "cmd/hercules/main.go",
+		Tree: treeFrom,
+		TreeEntry: object.TreeEntry{
+			Name: "cmd/hercules/main.go",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("c29112dbd697ad9b401333b80c18a63951bc18d9"),
+		},
+	}, To: object.ChangeEntry{
+		Name: "cmd/hercules/main2.go",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: "cmd/hercules/main.go",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("f7d918ec500e2f925ecde79b51cc007bac27de72"),
+		},
+	},
+	}
+	cache := map[plumbing.Hash]*CachedBlob{}
+	AddHash(t, cache, "baa64828831d174f40140e4b3cfa77d1e917a2c1")
+	cache[plumbing.NewHash("29c9fafd6a2fae8cd20298c3f60115bc31a4c0f2")] =
+		&CachedBlob{Data: make([]byte, 1000)}
+	AddHash(t, cache, "c29112dbd697ad9b401333b80c18a63951bc18d9")
+	AddHash(t, cache, "f7d918ec500e2f925ecde79b51cc007bac27de72")
+
+	deps := map[string]interface{}{}
+	deps[DependencyBlobCache] = cache
+	deps[DependencyTreeChanges] = changes
+	result, err := ls.Consume(deps)
+	assert.Nil(t, err)
+	langs := result[DependencyLanguages].(map[plumbing.Hash]string)
+	assert.Equal(t, "Go", langs[plumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1")])
+	assert.Equal(t, "Go", langs[plumbing.NewHash("c29112dbd697ad9b401333b80c18a63951bc18d9")])
+	assert.Equal(t, "Go", langs[plumbing.NewHash("f7d918ec500e2f925ecde79b51cc007bac27de72")])
+	lang, exists := langs[plumbing.NewHash("29c9fafd6a2fae8cd20298c3f60115bc31a4c0f2")]
+	assert.True(t, exists)
+	assert.Equal(t, "", lang)
+}

+ 2 - 2
internal/plumbing/renames.go

@@ -12,8 +12,8 @@ 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.v6/internal"
-	"gopkg.in/src-d/hercules.v6/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal"
+	"gopkg.in/src-d/hercules.v7/internal/core"
 )
 
 // RenameAnalysis improves TreeDiff's results by searching for changed blobs under different

+ 2 - 2
internal/plumbing/renames_test.go

@@ -7,8 +7,8 @@ import (
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureRenameAnalysis() *RenameAnalysis {

+ 2 - 2
internal/plumbing/tree_diff.go

@@ -13,7 +13,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.v6/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/core"
 )
 
 // TreeDiff generates the list of changes for a commit. A change can be either one or two blobs
@@ -45,7 +45,7 @@ const (
 	// which sets the list of programming languages to analyze. Language names are at
 	// https://doc.bblf.sh/languages.html Names are joined with a comma ",".
 	// "all" is the special name which disables this filter.
-	ConfigTreeDiffLanguages = "TreeDiff.Languages"
+	ConfigTreeDiffLanguages = "TreeDiff.LanguagesDetection"
 	// allLanguages denotes passing all files in.
 	allLanguages = "all"
 

+ 2 - 2
internal/plumbing/tree_diff_test.go

@@ -7,8 +7,8 @@ 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.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureTreeDiff() *TreeDiff {

+ 2 - 2
internal/plumbing/uast/changes_xpather_test.go

@@ -8,8 +8,8 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/bblfsh/client-go.v2"
-	uast_test "gopkg.in/src-d/hercules.v6/internal/plumbing/uast/test"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	uast_test "gopkg.in/src-d/hercules.v7/internal/plumbing/uast/test"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func TestChangesXPatherExtractChanged(t *testing.T) {

+ 2 - 2
internal/plumbing/uast/diff_refiner.go

@@ -6,8 +6,8 @@ import (
 	"github.com/sergi/go-diff/diffmatchpatch"
 	"gopkg.in/bblfsh/sdk.v1/uast"
 	"gopkg.in/src-d/go-git.v4"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing"
 )
 
 // FileDiffRefiner uses UASTs to improve the human interpretability of diffs.

+ 3 - 3
internal/plumbing/uast/diff_refiner_test.go

@@ -11,9 +11,9 @@ import (
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/bblfsh/sdk.v1/uast"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureFileDiffRefiner() *FileDiffRefiner {

+ 1 - 1
internal/plumbing/uast/test/utils.go

@@ -7,7 +7,7 @@ import (
 	"gopkg.in/bblfsh/client-go.v2"
 	"gopkg.in/bblfsh/sdk.v1/uast"
 	"gopkg.in/src-d/go-git.v4/plumbing"
-	core_test "gopkg.in/src-d/hercules.v6/internal/test"
+	core_test "gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 // ParseBlobFromTestRepo extracts the UAST from the file by it's hash and name.

+ 3 - 3
internal/plumbing/uast/uast.go

@@ -22,9 +22,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.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
 )
 
 // Extractor retrieves UASTs from Babelfish server which correspond to changed files in a commit.

+ 4 - 4
internal/plumbing/uast/uast_test.go

@@ -15,10 +15,10 @@ import (
 	"gopkg.in/bblfsh/sdk.v1/uast"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureUASTExtractor() *Extractor {

+ 2 - 2
internal/test/fixtures/fixtures.go

@@ -1,8 +1,8 @@
 package fixtures
 
 import (
-	"gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 // FileDiff initializes a new plumbing.FileDiff item for testing.

+ 4 - 2
labours.py

@@ -229,7 +229,8 @@ class YamlReader(Reader):
 
     def get_devs(self):
         people = self.data["Devs"]["people"]
-        days = {int(d): {int(dev): DevDay(*(int(x) for x in day)) for dev, day in devs.items()}
+        days = {int(d): {int(dev): DevDay(*(int(x) for x in day[:-1]))
+                         for dev, day in devs.items()}
                 for d, devs in self.data["Devs"]["days"].items()}
         return days, people
 
@@ -347,7 +348,8 @@ class ProtobufReader(Reader):
 
     def get_devs(self):
         people = list(self.contents["Devs"].dev_index)
-        days = {d: {dev: DevDay(stats.commits, stats.added, stats.removed, stats.changed)
+        days = {d: {dev: DevDay(stats.commits, stats.stats.added, stats.stats.removed,
+                                stats.stats.changed)
                     for dev, stats in day.devs.items()}
                 for d, day in self.contents["Devs"].days.items()}
         return days, people

+ 7 - 7
leaves/burndown.go

@@ -17,13 +17,13 @@ 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.v6/internal/burndown"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing/identity"
-	"gopkg.in/src-d/hercules.v6/internal/rbtree"
-	"gopkg.in/src-d/hercules.v6/internal/yaml"
+	"gopkg.in/src-d/hercules.v7/internal/burndown"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v7/internal/rbtree"
+	"gopkg.in/src-d/hercules.v7/internal/yaml"
 )
 
 // BurndownAnalysis allows to gather the line burndown statistics for a Git repository.

+ 6 - 6
leaves/burndown_test.go

@@ -7,17 +7,17 @@ import (
 	"path"
 	"testing"
 
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/test/fixtures"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/test/fixtures"
 
 	"github.com/gogo/protobuf/proto"
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing/identity"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func AddHash(t *testing.T, cache map[plumbing.Hash]*items.CachedBlob, hash string) {

+ 4 - 4
leaves/comment_sentiment.go

@@ -16,10 +16,10 @@ import (
 	progress "gopkg.in/cheggaaa/pb.v1"
 	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	uast_items "gopkg.in/src-d/hercules.v6/internal/plumbing/uast"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	uast_items "gopkg.in/src-d/hercules.v7/internal/plumbing/uast"
 	"gopkg.in/vmarkovtsev/BiDiSentiment.v1"
 )
 

+ 6 - 6
leaves/comment_sentiment_test.go

@@ -13,12 +13,12 @@ import (
 	"gopkg.in/bblfsh/client-go.v2"
 	"gopkg.in/bblfsh/client-go.v2/tools"
 	"gopkg.in/src-d/go-git.v4/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	uast_items "gopkg.in/src-d/hercules.v6/internal/plumbing/uast"
-	uast_test "gopkg.in/src-d/hercules.v6/internal/plumbing/uast/test"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	uast_items "gopkg.in/src-d/hercules.v7/internal/plumbing/uast"
+	uast_test "gopkg.in/src-d/hercules.v7/internal/plumbing/uast/test"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureCommentSentiment() *CommentSentimentAnalysis {

+ 5 - 5
leaves/couples.go

@@ -9,11 +9,11 @@ import (
 	"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.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing/identity"
-	"gopkg.in/src-d/hercules.v6/internal/yaml"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v7/internal/yaml"
 )
 
 // CouplesAnalysis calculates the number of common commits for files and authors.

+ 5 - 5
leaves/couples_test.go

@@ -11,11 +11,11 @@ import (
 	"github.com/stretchr/testify/assert"
 	gitplumbing "gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing/identity"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureCouples() *CouplesAnalysis {

+ 122 - 23
leaves/devs.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"io"
 	"sort"
+	"strings"
 	"unicode/utf8"
 
 	"github.com/gogo/protobuf/proto"
@@ -12,15 +13,16 @@ 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.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing/identity"
-	"gopkg.in/src-d/hercules.v6/internal/yaml"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v7/internal/yaml"
 )
 
 // DevsAnalysis calculates the number of commits through time per developer.
 // It also records the numbers of added, deleted and changed lines through time per developer.
+// Those numbers are additionally measured per language.
 type DevsAnalysis struct {
 	core.NoopMerger
 	core.OneShotMergeProcessor
@@ -44,10 +46,8 @@ type DevsResult struct {
 	reversedPeopleDict []string
 }
 
-// DevDay is the statistics for a development day and a particular developer.
-type DevDay struct {
-	// Commits is the number of commits made by a particular developer in a particular day.
-	Commits int
+// LineStats holds the numbers of inserted, deleted and changed lines.
+type LineStats struct {
 	// Added is the number of added lines by a particular developer in a particular day.
 	Added int
 	// Removed is the number of removed lines by a particular developer in a particular day.
@@ -56,6 +56,15 @@ type DevDay struct {
 	Changed int
 }
 
+// DevDay is the statistics for a development day and a particular developer.
+type DevDay struct {
+	// Commits is the number of commits made by a particular developer in a particular day.
+	Commits int
+	LineStats
+	// LanguagesDetection carries fine-grained line stats per programming language.
+	Languages map[string]LineStats
+}
+
 const (
 	// ConfigDevsConsiderEmptyCommits is the name of the option to set DevsAnalysis.ConsiderEmptyCommits.
 	ConfigDevsConsiderEmptyCommits = "Devs.ConsiderEmptyCommits"
@@ -79,7 +88,7 @@ func (devs *DevsAnalysis) Provides() []string {
 func (devs *DevsAnalysis) Requires() []string {
 	arr := [...]string{
 		identity.DependencyAuthor, items.DependencyTreeChanges, items.DependencyFileDiff,
-		items.DependencyBlobCache, items.DependencyDay}
+		items.DependencyBlobCache, items.DependencyDay, items.DependencyLanguages}
 	return arr[:]
 }
 
@@ -145,12 +154,13 @@ func (devs *DevsAnalysis) Consume(deps map[string]interface{}) (map[string]inter
 	}
 	dd, exists := devsDay[author]
 	if !exists {
-		dd = &DevDay{}
+		dd = &DevDay{Languages: map[string]LineStats{}}
 		devsDay[author] = dd
 	}
 	dd.Commits++
 	cache := deps[items.DependencyBlobCache].(map[plumbing.Hash]*items.CachedBlob)
 	fileDiffs := deps[items.DependencyFileDiff].(map[string]items.FileDiffData)
+	langs := deps[items.DependencyLanguages].(map[plumbing.Hash]string)
 	for _, change := range treeDiff {
 		action, err := change.Action()
 		if err != nil {
@@ -165,6 +175,13 @@ func (devs *DevsAnalysis) Consume(deps map[string]interface{}) (map[string]inter
 				continue
 			}
 			dd.Added += lines
+			lang := langs[change.To.TreeEntry.Hash]
+			langStats := dd.Languages[lang]
+			dd.Languages[lang] = LineStats{
+				Added:   langStats.Added + lines,
+				Removed: langStats.Removed,
+				Changed: langStats.Changed,
+			}
 		case merkletrie.Delete:
 			blob := cache[change.From.TreeEntry.Hash]
 			lines, err := blob.CountLines()
@@ -173,7 +190,15 @@ func (devs *DevsAnalysis) Consume(deps map[string]interface{}) (map[string]inter
 				continue
 			}
 			dd.Removed += lines
+			lang := langs[change.From.TreeEntry.Hash]
+			langStats := dd.Languages[lang]
+			dd.Languages[lang] = LineStats{
+				Added:   langStats.Added,
+				Removed: langStats.Removed + lines,
+				Changed: langStats.Changed,
+			}
 		case merkletrie.Modify:
+			lang := langs[change.To.TreeEntry.Hash]
 			thisDiffs := fileDiffs[change.To.Name]
 			var removedPending int
 			for _, edit := range thisDiffs.Diffs {
@@ -181,16 +206,36 @@ func (devs *DevsAnalysis) Consume(deps map[string]interface{}) (map[string]inter
 				case diffmatchpatch.DiffEqual:
 					if removedPending > 0 {
 						dd.Removed += removedPending
+						langStats := dd.Languages[lang]
+						dd.Languages[lang] = LineStats{
+							Added:   langStats.Added,
+							Removed: langStats.Removed + removedPending,
+							Changed: langStats.Changed,
+						}
 					}
 					removedPending = 0
 				case diffmatchpatch.DiffInsert:
 					added := utf8.RuneCountInString(edit.Text)
 					if removedPending > added {
+						removed := removedPending - added
 						dd.Changed += added
-						dd.Removed += removedPending - added
+						dd.Removed += removed
+						langStats := dd.Languages[lang]
+						dd.Languages[lang] = LineStats{
+							Added:   langStats.Added,
+							Removed: langStats.Removed + removed,
+							Changed: langStats.Changed + added,
+						}
 					} else {
+						added := added - removedPending
 						dd.Changed += removedPending
-						dd.Added += added - removedPending
+						dd.Added += added
+						langStats := dd.Languages[lang]
+						dd.Languages[lang] = LineStats{
+							Added:   langStats.Added + added,
+							Removed: langStats.Removed,
+							Changed: langStats.Changed + removedPending,
+						}
 					}
 					removedPending = 0
 				case diffmatchpatch.DiffDelete:
@@ -199,6 +244,12 @@ func (devs *DevsAnalysis) Consume(deps map[string]interface{}) (map[string]inter
 			}
 			if removedPending > 0 {
 				dd.Removed += removedPending
+				langStats := dd.Languages[lang]
+				dd.Languages[lang] = LineStats{
+					Added:   langStats.Added,
+					Removed: langStats.Removed + removedPending,
+					Changed: langStats.Changed,
+				}
 			}
 		}
 	}
@@ -244,11 +295,22 @@ func (devs *DevsAnalysis) Deserialize(pbmessage []byte) (interface{}, error) {
 			if dev == -1 {
 				dev = identity.AuthorMissing
 			}
+			languages := map[string]LineStats{}
 			rdd[int(dev)] = &DevDay{
 				Commits: int(stats.Commits),
-				Added:   int(stats.Added),
-				Removed: int(stats.Removed),
-				Changed: int(stats.Changed),
+				LineStats: LineStats{
+					Added:   int(stats.Stats.Added),
+					Removed: int(stats.Stats.Removed),
+					Changed: int(stats.Stats.Changed),
+				},
+				Languages: languages,
+			}
+			for lang, ls := range stats.Languages {
+				languages[lang] = LineStats{
+					Added:   int(ls.Added),
+					Removed: int(ls.Removed),
+					Changed: int(ls.Changed),
+				}
 			}
 		}
 	}
@@ -311,13 +373,21 @@ func (devs *DevsAnalysis) MergeResults(r1, r2 interface{}, c1, c2 *core.CommonAn
 			}
 			newstats, exists := newdd[newdev]
 			if !exists {
-				newstats = &DevDay{}
+				newstats = &DevDay{Languages: map[string]LineStats{}}
 				newdd[newdev] = newstats
 			}
 			newstats.Commits += stats.Commits
 			newstats.Added += stats.Added
 			newstats.Removed += stats.Removed
 			newstats.Changed += stats.Changed
+			for lang, ls := range stats.Languages {
+				prev := newstats.Languages[lang]
+				newstats.Languages[lang] = LineStats{
+					Added:   prev.Added + ls.Added,
+					Removed: prev.Removed + ls.Removed,
+					Changed: prev.Changed + ls.Changed,
+				}
+			}
 		}
 	}
 	for day, dd := range cr2.Days {
@@ -333,13 +403,21 @@ func (devs *DevsAnalysis) MergeResults(r1, r2 interface{}, c1, c2 *core.CommonAn
 			}
 			newstats, exists := newdd[newdev]
 			if !exists {
-				newstats = &DevDay{}
+				newstats = &DevDay{Languages: map[string]LineStats{}}
 				newdd[newdev] = newstats
 			}
 			newstats.Commits += stats.Commits
 			newstats.Added += stats.Added
 			newstats.Removed += stats.Removed
 			newstats.Changed += stats.Changed
+			for lang, ls := range stats.Languages {
+				prev := newstats.Languages[lang]
+				newstats.Languages[lang] = LineStats{
+					Added:   prev.Added + ls.Added,
+					Removed: prev.Removed + ls.Removed,
+					Changed: prev.Changed + ls.Changed,
+				}
+			}
 		}
 	}
 	return merged
@@ -373,8 +451,18 @@ func (devs *DevsAnalysis) serializeText(result *DevsResult, writer io.Writer) {
 			if dev == identity.AuthorMissing {
 				dev = -1
 			}
-			fmt.Fprintf(writer, "      %d: [%d, %d, %d, %d]\n",
-				dev, stats.Commits, stats.Added, stats.Removed, stats.Changed)
+			var langs []string
+			for lang, ls := range stats.Languages {
+				if lang == "" {
+					lang = "none"
+				}
+				langs = append(langs,
+					fmt.Sprintf("%s: [%d, %d, %d]", lang, ls.Added, ls.Removed, ls.Changed))
+			}
+			sort.Strings(langs)
+			fmt.Fprintf(writer, "      %d: [%d, %d, %d, %d, {%s}]\n",
+				dev, stats.Commits, stats.Added, stats.Removed, stats.Changed,
+				strings.Join(langs, ", "))
 		}
 	}
 	fmt.Fprintln(writer, "  people:")
@@ -395,11 +483,22 @@ func (devs *DevsAnalysis) serializeBinary(result *DevsResult, writer io.Writer)
 			if dev == identity.AuthorMissing {
 				dev = -1
 			}
+			languages := map[string]*pb.LineStats{}
 			dd.Devs[int32(dev)] = &pb.DevDay{
 				Commits: int32(stats.Commits),
-				Added:   int32(stats.Added),
-				Changed: int32(stats.Changed),
-				Removed: int32(stats.Removed),
+				Stats: &pb.LineStats{
+					Added:   int32(stats.Added),
+					Changed: int32(stats.Changed),
+					Removed: int32(stats.Removed),
+				},
+				Languages: languages,
+			}
+			for lang, ls := range stats.Languages {
+				languages[lang] = &pb.LineStats{
+					Added:   int32(ls.Added),
+					Changed: int32(ls.Changed),
+					Removed: int32(ls.Removed),
+				}
 			}
 		}
 	}

+ 83 - 44
leaves/devs_test.go

@@ -8,12 +8,12 @@ import (
 	"github.com/stretchr/testify/assert"
 	gitplumbing "gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/plumbing/identity"
-	"gopkg.in/src-d/hercules.v6/internal/test"
-	"gopkg.in/src-d/hercules.v6/internal/test/fixtures"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v7/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/test/fixtures"
 )
 
 func fixtureDevs() *DevsAnalysis {
@@ -28,12 +28,13 @@ func TestDevsMeta(t *testing.T) {
 	d := fixtureDevs()
 	assert.Equal(t, d.Name(), "Devs")
 	assert.Equal(t, len(d.Provides()), 0)
-	assert.Equal(t, len(d.Requires()), 5)
+	assert.Equal(t, len(d.Requires()), 6)
 	assert.Equal(t, d.Requires()[0], identity.DependencyAuthor)
 	assert.Equal(t, d.Requires()[1], plumbing.DependencyTreeChanges)
 	assert.Equal(t, d.Requires()[2], plumbing.DependencyFileDiff)
 	assert.Equal(t, d.Requires()[3], plumbing.DependencyBlobCache)
 	assert.Equal(t, d.Requires()[4], plumbing.DependencyDay)
+	assert.Equal(t, d.Requires()[5], plumbing.DependencyLanguages)
 	assert.Equal(t, d.Flag(), "devs")
 	assert.Len(t, d.ListConfigurationOptions(), 1)
 	assert.Equal(t, d.ListConfigurationOptions()[0].Name, ConfigDevsConsiderEmptyCommits)
@@ -84,6 +85,12 @@ func TestDevsConsumeFinalize(t *testing.T) {
 	AddHash(t, cache, "baa64828831d174f40140e4b3cfa77d1e917a2c1")
 	AddHash(t, cache, "dc248ba2b22048cc730c571a748e8ffcf7085ab9")
 	deps[plumbing.DependencyBlobCache] = cache
+	deps[plumbing.DependencyLanguages] = map[gitplumbing.Hash]string{
+		gitplumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"): "Go",
+		gitplumbing.NewHash("c29112dbd697ad9b401333b80c18a63951bc18d9"): "Go",
+		gitplumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1"): "Go",
+		gitplumbing.NewHash("dc248ba2b22048cc730c571a748e8ffcf7085ab9"): "Go",
+	}
 	changes := make(object.Changes, 3)
 	treeFrom, _ := test.Repository.TreeObject(gitplumbing.NewHash(
 		"a1eb2ea76eb7f9bfbde9b243861474421000eb96"))
@@ -145,6 +152,9 @@ func TestDevsConsumeFinalize(t *testing.T) {
 	assert.Equal(t, dev.Added, 847)
 	assert.Equal(t, dev.Removed, 9)
 	assert.Equal(t, dev.Changed, 67)
+	assert.Equal(t, dev.Languages["Go"].Added, 847)
+	assert.Equal(t, dev.Languages["Go"].Removed, 9)
+	assert.Equal(t, dev.Languages["Go"].Changed, 67)
 
 	deps[identity.DependencyAuthor] = 1
 	result, err = devs.Consume(deps)
@@ -159,6 +169,9 @@ func TestDevsConsumeFinalize(t *testing.T) {
 		assert.Equal(t, dev.Added, 847)
 		assert.Equal(t, dev.Removed, 9)
 		assert.Equal(t, dev.Changed, 67)
+		assert.Equal(t, dev.Languages["Go"].Added, 847)
+		assert.Equal(t, dev.Languages["Go"].Removed, 9)
+		assert.Equal(t, dev.Languages["Go"].Changed, 67)
 	}
 
 	result, err = devs.Consume(deps)
@@ -172,11 +185,17 @@ func TestDevsConsumeFinalize(t *testing.T) {
 	assert.Equal(t, dev.Added, 847)
 	assert.Equal(t, dev.Removed, 9)
 	assert.Equal(t, dev.Changed, 67)
+	assert.Equal(t, dev.Languages["Go"].Added, 847)
+	assert.Equal(t, dev.Languages["Go"].Removed, 9)
+	assert.Equal(t, dev.Languages["Go"].Changed, 67)
 	dev = day[1]
 	assert.Equal(t, dev.Commits, 2)
 	assert.Equal(t, dev.Added, 847*2)
 	assert.Equal(t, dev.Removed, 9*2)
 	assert.Equal(t, dev.Changed, 67*2)
+	assert.Equal(t, dev.Languages["Go"].Added, 847*2)
+	assert.Equal(t, dev.Languages["Go"].Removed, 9*2)
+	assert.Equal(t, dev.Languages["Go"].Changed, 67*2)
 
 	deps[plumbing.DependencyDay] = 1
 	result, err = devs.Consume(deps)
@@ -190,11 +209,17 @@ func TestDevsConsumeFinalize(t *testing.T) {
 	assert.Equal(t, dev.Added, 847)
 	assert.Equal(t, dev.Removed, 9)
 	assert.Equal(t, dev.Changed, 67)
+	assert.Equal(t, dev.Languages["Go"].Added, 847)
+	assert.Equal(t, dev.Languages["Go"].Removed, 9)
+	assert.Equal(t, dev.Languages["Go"].Changed, 67)
 	dev = day[1]
 	assert.Equal(t, dev.Commits, 2)
 	assert.Equal(t, dev.Added, 847*2)
 	assert.Equal(t, dev.Removed, 9*2)
 	assert.Equal(t, dev.Changed, 67*2)
+	assert.Equal(t, dev.Languages["Go"].Added, 847*2)
+	assert.Equal(t, dev.Languages["Go"].Removed, 9*2)
+	assert.Equal(t, dev.Languages["Go"].Changed, 67*2)
 	day = devs.days[1]
 	assert.Len(t, day, 1)
 	dev = day[1]
@@ -202,12 +227,15 @@ func TestDevsConsumeFinalize(t *testing.T) {
 	assert.Equal(t, dev.Added, 847)
 	assert.Equal(t, dev.Removed, 9)
 	assert.Equal(t, dev.Changed, 67)
+	assert.Equal(t, dev.Languages["Go"].Added, 847)
+	assert.Equal(t, dev.Languages["Go"].Removed, 9)
+	assert.Equal(t, dev.Languages["Go"].Changed, 67)
 }
 
 func TestDevsFinalize(t *testing.T) {
 	devs := fixtureDevs()
 	devs.days[1] = map[int]*DevDay{}
-	devs.days[1][1] = &DevDay{10, 20, 30, 40}
+	devs.days[1][1] = &DevDay{10, LineStats{20, 30, 40}, nil}
 	x := devs.Finalize().(DevsResult)
 	assert.Equal(t, x.Days, devs.days)
 	assert.Equal(t, x.reversedPeopleDict, devs.reversedPeopleDict)
@@ -222,22 +250,23 @@ func TestDevsFork(t *testing.T) {
 func TestDevsSerialize(t *testing.T) {
 	devs := fixtureDevs()
 	devs.days[1] = map[int]*DevDay{}
-	devs.days[1][0] = &DevDay{10, 20, 30, 40}
-	devs.days[1][1] = &DevDay{1, 2, 3, 4}
+	devs.days[1][0] = &DevDay{10, LineStats{20, 30, 40}, map[string]LineStats{"Go": {2, 3, 4}}}
+	devs.days[1][1] = &DevDay{1, LineStats{2, 3, 4}, map[string]LineStats{"Go": {25, 35, 45}}}
 	devs.days[10] = map[int]*DevDay{}
-	devs.days[10][0] = &DevDay{11, 21, 31, 41}
-	devs.days[10][identity.AuthorMissing] = &DevDay{100, 200, 300, 400}
+	devs.days[10][0] = &DevDay{11, LineStats{21, 31, 41}, map[string]LineStats{"": {12, 13, 14}}}
+	devs.days[10][identity.AuthorMissing] = &DevDay{
+		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {32, 33, 34}}}
 	res := devs.Finalize().(DevsResult)
 	buffer := &bytes.Buffer{}
 	err := devs.Serialize(res, false, buffer)
 	assert.Nil(t, err)
 	assert.Equal(t, `  days:
     1:
-      0: [10, 20, 30, 40]
-      1: [1, 2, 3, 4]
+      0: [10, 20, 30, 40, {Go: [2, 3, 4]}]
+      1: [1, 2, 3, 4, {Go: [25, 35, 45]}]
     10:
-      0: [11, 21, 31, 41]
-      -1: [100, 200, 300, 400]
+      0: [11, 21, 31, 41, {none: [12, 13, 14]}]
+      -1: [100, 200, 300, 400, {Go: [32, 33, 34]}]
   people:
   - "one@srcd"
   - "two@srcd"
@@ -252,24 +281,29 @@ func TestDevsSerialize(t *testing.T) {
 	assert.Len(t, msg.Days, 2)
 	assert.Len(t, msg.Days[1].Devs, 2)
 	assert.Equal(t, msg.Days[1].Devs[0], &pb.DevDay{
-		Commits: 10, Added: 20, Removed: 30, Changed: 40})
+		Commits: 10, Stats: &pb.LineStats{Added: 20, Removed: 30, Changed: 40},
+		Languages: map[string]*pb.LineStats{"Go": {Added: 2, Removed: 3, Changed: 4}}})
 	assert.Equal(t, msg.Days[1].Devs[1], &pb.DevDay{
-		Commits: 1, Added: 2, Removed: 3, Changed: 4})
+		Commits: 1, Stats: &pb.LineStats{Added: 2, Removed: 3, Changed: 4},
+		Languages: map[string]*pb.LineStats{"Go": {Added: 25, Removed: 35, Changed: 45}}})
 	assert.Len(t, msg.Days[10].Devs, 2)
 	assert.Equal(t, msg.Days[10].Devs[0], &pb.DevDay{
-		Commits: 11, Added: 21, Removed: 31, Changed: 41})
+		Commits: 11, Stats: &pb.LineStats{Added: 21, Removed: 31, Changed: 41},
+		Languages: map[string]*pb.LineStats{"": {Added: 12, Removed: 13, Changed: 14}}})
 	assert.Equal(t, msg.Days[10].Devs[-1], &pb.DevDay{
-		Commits: 100, Added: 200, Removed: 300, Changed: 400})
+		Commits: 100, Stats: &pb.LineStats{Added: 200, Removed: 300, Changed: 400},
+		Languages: map[string]*pb.LineStats{"Go": {Added: 32, Removed: 33, Changed: 34}}})
 }
 
 func TestDevsDeserialize(t *testing.T) {
 	devs := fixtureDevs()
 	devs.days[1] = map[int]*DevDay{}
-	devs.days[1][0] = &DevDay{10, 20, 30, 40}
-	devs.days[1][1] = &DevDay{1, 2, 3, 4}
+	devs.days[1][0] = &DevDay{10, LineStats{20, 30, 40}, map[string]LineStats{"Go": {12, 13, 14}}}
+	devs.days[1][1] = &DevDay{1, LineStats{2, 3, 4}, map[string]LineStats{"Go": {22, 23, 24}}}
 	devs.days[10] = map[int]*DevDay{}
-	devs.days[10][0] = &DevDay{11, 21, 31, 41}
-	devs.days[10][identity.AuthorMissing] = &DevDay{100, 200, 300, 400}
+	devs.days[10][0] = &DevDay{11, LineStats{21, 31, 41}, map[string]LineStats{"Go": {32, 33, 34}}}
+	devs.days[10][identity.AuthorMissing] = &DevDay{
+		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {42, 43, 44}}}
 	res := devs.Finalize().(DevsResult)
 	buffer := &bytes.Buffer{}
 	err := devs.Serialize(res, true, buffer)
@@ -288,45 +322,50 @@ func TestDevsMergeResults(t *testing.T) {
 		reversedPeopleDict: people1[:],
 	}
 	r1.Days[1] = map[int]*DevDay{}
-	r1.Days[1][0] = &DevDay{10, 20, 30, 40}
-	r1.Days[1][1] = &DevDay{1, 2, 3, 4}
+	r1.Days[1][0] = &DevDay{10, LineStats{20, 30, 40}, map[string]LineStats{"Go": {12, 13, 14}}}
+	r1.Days[1][1] = &DevDay{1, LineStats{2, 3, 4}, map[string]LineStats{"Go": {22, 23, 24}}}
 	r1.Days[10] = map[int]*DevDay{}
-	r1.Days[10][0] = &DevDay{11, 21, 31, 41}
-	r1.Days[10][identity.AuthorMissing] = &DevDay{100, 200, 300, 400}
+	r1.Days[10][0] = &DevDay{11, LineStats{21, 31, 41}, nil}
+	r1.Days[10][identity.AuthorMissing] = &DevDay{
+		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {32, 33, 34}}}
 	r1.Days[11] = map[int]*DevDay{}
-	r1.Days[11][1] = &DevDay{10, 20, 30, 40}
+	r1.Days[11][1] = &DevDay{10, LineStats{20, 30, 40}, map[string]LineStats{"Go": {42, 43, 44}}}
 	r2 := DevsResult{
 		Days:               map[int]map[int]*DevDay{},
 		reversedPeopleDict: people2[:],
 	}
 	r2.Days[1] = map[int]*DevDay{}
-	r2.Days[1][0] = &DevDay{10, 20, 30, 40}
-	r2.Days[1][1] = &DevDay{1, 2, 3, 4}
+	r2.Days[1][0] = &DevDay{10, LineStats{20, 30, 40}, map[string]LineStats{"Go": {12, 13, 14}}}
+	r2.Days[1][1] = &DevDay{1, LineStats{2, 3, 4}, map[string]LineStats{"Go": {22, 23, 24}}}
 	r2.Days[2] = map[int]*DevDay{}
-	r2.Days[2][0] = &DevDay{11, 21, 31, 41}
-	r2.Days[2][identity.AuthorMissing] = &DevDay{100, 200, 300, 400}
+	r2.Days[2][0] = &DevDay{11, LineStats{21, 31, 41}, map[string]LineStats{"Go": {32, 33, 34}}}
+	r2.Days[2][identity.AuthorMissing] = &DevDay{
+		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {42, 43, 44}}}
 	r2.Days[10] = map[int]*DevDay{}
-	r2.Days[10][0] = &DevDay{11, 21, 31, 41}
-	r2.Days[10][identity.AuthorMissing] = &DevDay{100, 200, 300, 400}
+	r2.Days[10][0] = &DevDay{11, LineStats{21, 31, 41}, map[string]LineStats{"Go": {52, 53, 54}}}
+	r2.Days[10][identity.AuthorMissing] = &DevDay{
+		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {62, 63, 64}}}
 
 	devs := fixtureDevs()
 	rm := devs.MergeResults(r1, r2, nil, nil).(DevsResult)
 	peoplerm := [...]string{"1@srcd", "2@srcd", "3@srcd"}
 	assert.Equal(t, rm.reversedPeopleDict, peoplerm[:])
 	assert.Len(t, rm.Days, 4)
-	assert.Equal(t, rm.Days[11], map[int]*DevDay{1: {10, 20, 30, 40}})
+	assert.Equal(t, rm.Days[11], map[int]*DevDay{
+		1: {10, LineStats{20, 30, 40}, map[string]LineStats{"Go": {42, 43, 44}}}})
 	assert.Equal(t, rm.Days[2], map[int]*DevDay{
-		identity.AuthorMissing: {100, 200, 300, 400},
-		2:                      {11, 21, 31, 41},
+		identity.AuthorMissing: {100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {42, 43, 44}}},
+		2:                      {11, LineStats{21, 31, 41}, map[string]LineStats{"Go": {32, 33, 34}}},
 	})
 	assert.Equal(t, rm.Days[1], map[int]*DevDay{
-		0: {11, 22, 33, 44},
-		1: {1, 2, 3, 4},
-		2: {10, 20, 30, 40},
+		0: {11, LineStats{22, 33, 44}, map[string]LineStats{"Go": {34, 36, 38}}},
+		1: {1, LineStats{2, 3, 4}, map[string]LineStats{"Go": {22, 23, 24}}},
+		2: {10, LineStats{20, 30, 40}, map[string]LineStats{"Go": {12, 13, 14}}},
 	})
 	assert.Equal(t, rm.Days[10], map[int]*DevDay{
-		0:                      {11, 21, 31, 41},
-		2:                      {11, 21, 31, 41},
-		identity.AuthorMissing: {100 * 2, 200 * 2, 300 * 2, 400 * 2},
+		0: {11, LineStats{21, 31, 41}, map[string]LineStats{}},
+		2: {11, LineStats{21, 31, 41}, map[string]LineStats{"Go": {52, 53, 54}}},
+		identity.AuthorMissing: {
+			100 * 2, LineStats{200 * 2, 300 * 2, 400 * 2}, map[string]LineStats{"Go": {94, 96, 98}}},
 	})
 }

+ 3 - 3
leaves/file_history.go

@@ -11,9 +11,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.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
 )
 
 // FileHistory contains the intermediate state which is mutated by Consume(). It should implement

+ 4 - 4
leaves/file_history_test.go

@@ -8,10 +8,10 @@ import (
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureFileHistory() *FileHistory {

+ 4 - 4
leaves/shotness.go

@@ -13,10 +13,10 @@ import (
 	"gopkg.in/bblfsh/sdk.v1/uast"
 	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	uast_items "gopkg.in/src-d/hercules.v6/internal/plumbing/uast"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	uast_items "gopkg.in/src-d/hercules.v7/internal/plumbing/uast"
 )
 
 // ShotnessAnalysis contains the intermediate state which is mutated by Consume(). It should implement

+ 5 - 5
leaves/shotness_test.go

@@ -11,11 +11,11 @@ import (
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/bblfsh/sdk.v1/uast"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
-	"gopkg.in/src-d/hercules.v6/internal/core"
-	"gopkg.in/src-d/hercules.v6/internal/pb"
-	items "gopkg.in/src-d/hercules.v6/internal/plumbing"
-	uast_items "gopkg.in/src-d/hercules.v6/internal/plumbing/uast"
-	"gopkg.in/src-d/hercules.v6/internal/test"
+	"gopkg.in/src-d/hercules.v7/internal/core"
+	"gopkg.in/src-d/hercules.v7/internal/pb"
+	items "gopkg.in/src-d/hercules.v7/internal/plumbing"
+	uast_items "gopkg.in/src-d/hercules.v7/internal/plumbing/uast"
+	"gopkg.in/src-d/hercules.v7/internal/test"
 )
 
 func fixtureShotness() *ShotnessAnalysis {

+ 1 - 1
version_test.go

@@ -7,5 +7,5 @@ import (
 )
 
 func TestVersion(t *testing.T) {
-	assert.Equal(t, BinaryVersion, 6)
+	assert.Equal(t, BinaryVersion, 7) // v7
 }