فهرست منبع

Add LanguagesDetection and language stats for --devs

Signed-off-by: Vadim Markovtsev <vadim@sourced.tech>
Vadim Markovtsev 6 سال پیش
والد
کامیت
7578277cc2
9فایلهای تغییر یافته به همراه675 افزوده شده و 192 حذف شده
  1. 130 100
      internal/pb/pb.pb.go
  2. 8 3
      internal/pb/pb.proto
  3. 126 30
      internal/pb/pb_pb2.py
  4. 109 0
      internal/plumbing/languages.go
  5. 105 0
      internal/plumbing/languages_test.go
  6. 1 1
      internal/plumbing/tree_diff.go
  7. 2 2
      labours.py
  8. 117 18
      leaves/devs.go
  9. 77 38
      leaves/devs_test.go

+ 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 {

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 126 - 30
internal/pb/pb_pb2.py


+ 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.v6/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.v6/internal/core"
+	"gopkg.in/src-d/hercules.v6/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)
+}

+ 1 - 1
internal/plumbing/tree_diff.go

@@ -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
labours.py

@@ -229,7 +229,7 @@ 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 +347,7 @@ 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

+ 117 - 18
leaves/devs.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"io"
 	"sort"
+	"strings"
 	"unicode/utf8"
 
 	"github.com/gogo/protobuf/proto"
@@ -21,6 +22,7 @@ import (
 
 // 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),
+				}
 			}
 		}
 	}

+ 77 - 38
leaves/devs_test.go

@@ -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": {2, 3, 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": {25, 35, 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{"": {12, 13, 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": {32, 33, 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}}},
 	})
 }