Преглед изворни кода

Add DevsAnalysis

Fixes #111

Signed-off-by: Vadim Markovtsev <vadim@sourced.tech>
Vadim Markovtsev пре 6 година
родитељ
комит
1147f86001
6 измењених фајлова са 1275 додато и 178 уклоњено
  1. 3 1
      .travis.yml
  2. 168 73
      internal/pb/pb.pb.go
  3. 16 0
      internal/pb/pb.proto
  4. 342 104
      internal/pb/pb_pb2.py
  5. 414 0
      leaves/devs.go
  6. 332 0
      leaves/devs_test.go

+ 3 - 1
.travis.yml

@@ -33,7 +33,9 @@ matrix:
 
 stages:
   - test
-  - deploy
+  - name: deploy
+    # require any tag name to deploy
+    if: tag =~ .*
 
 env:
   - PROTOC_VERSION=3.6.0 TENSORFLOW_VERSION=1.8.0

+ 168 - 73
internal/pb/pb.pb.go

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

+ 16 - 0
internal/pb/pb.proto

@@ -107,6 +107,22 @@ message FileHistoryResultMessage {
     map<string, FileHistory> files = 1;
 }
 
+message DevDay {
+    int32 commits = 1;
+    int32 added = 2;
+    int32 removed = 3;
+    int32 changed = 4;
+}
+
+message DayDevs {
+    map<int32, DevDay> devs = 1;
+}
+
+message DevsAnalysisResult {
+    map<int32, DayDevs> days = 1;
+    repeated string dev_index = 2;
+}
+
 message Sentiment {
     float value = 1;
     repeated string comments = 2;

Разлика између датотеке није приказан због своје велике величине
+ 342 - 104
internal/pb/pb_pb2.py


+ 414 - 0
leaves/devs.go

@@ -0,0 +1,414 @@
+package leaves
+
+import (
+	"fmt"
+	"io"
+	"sort"
+	"unicode/utf8"
+
+	"github.com/gogo/protobuf/proto"
+	"github.com/sergi/go-diff/diffmatchpatch"
+	"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/go-git.v4/utils/merkletrie"
+	"gopkg.in/src-d/hercules.v5/internal/core"
+	"gopkg.in/src-d/hercules.v5/internal/pb"
+	items "gopkg.in/src-d/hercules.v5/internal/plumbing"
+	"gopkg.in/src-d/hercules.v5/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v5/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.
+type DevsAnalysis struct {
+	core.NoopMerger
+	core.OneShotMergeProcessor
+	// ConsiderEmptyCommits indicates whether empty commits (e.g., merges) should be taken
+	// into account.
+	ConsiderEmptyCommits bool
+
+	// days maps days to developers to stats
+	days map[int]map[int]*DevDay
+	// reversedPeopleDict references IdentityDetector.ReversedPeopleDict
+	reversedPeopleDict []string
+}
+
+// DevsResult is returned by DevsAnalysis.Finalize() and carries the daily statistics
+// per developer.
+type DevsResult struct {
+	// Days is <day index> -> <developer index> -> daily stats
+	Days map[int]map[int]*DevDay
+
+	// reversedPeopleDict references IdentityDetector.ReversedPeopleDict
+	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
+	// 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.
+	Removed int
+	// Changed is the number of changed lines by a particular developer in a particular day.
+	Changed int
+}
+
+const (
+	// ConfigDevsConsiderEmptyCommits is the name of the option to set DevsAnalysis.ConsiderEmptyCommits.
+	ConfigDevsConsiderEmptyCommits = "Devs.ConsiderEmptyCommits"
+)
+
+// Name of this PipelineItem. Uniquely identifies the type, used for mapping keys, etc.
+func (devs *DevsAnalysis) Name() string {
+	return "Devs"
+}
+
+// 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 (devs *DevsAnalysis) Provides() []string {
+	return []string{}
+}
+
+// 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 (devs *DevsAnalysis) Requires() []string {
+	arr := [...]string{
+		identity.DependencyAuthor, items.DependencyTreeChanges, items.DependencyFileDiff,
+		items.DependencyBlobCache, items.DependencyDay}
+	return arr[:]
+}
+
+// ListConfigurationOptions returns the list of changeable public properties of this PipelineItem.
+func (devs *DevsAnalysis) ListConfigurationOptions() []core.ConfigurationOption {
+	options := [...]core.ConfigurationOption{{
+		Name:        ConfigDevsConsiderEmptyCommits,
+		Description: "Take into account empty commits such as trivial merges.",
+		Flag:        "--empty-commits",
+		Type:        core.BoolConfigurationOption,
+		Default:     false}}
+	return options[:]
+}
+
+// Configure sets the properties previously published by ListConfigurationOptions().
+func (devs *DevsAnalysis) Configure(facts map[string]interface{}) {
+	if val, exists := facts[ConfigDevsConsiderEmptyCommits].(bool); exists {
+		devs.ConsiderEmptyCommits = val
+	}
+	if val, exists := facts[identity.FactIdentityDetectorReversedPeopleDict].([]string); exists {
+		devs.reversedPeopleDict = val
+	}
+}
+
+// Flag for the command line switch which enables this analysis.
+func (devs *DevsAnalysis) Flag() string {
+	return "devs"
+}
+
+// Description returns the text which explains what the analysis is doing.
+func (devs *DevsAnalysis) Description() string {
+	return "Calculates the number of commits, added, removed and changed lines per developer through time."
+}
+
+// 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 (devs *DevsAnalysis) Initialize(repository *git.Repository) {
+	devs.days = map[int]map[int]*DevDay{}
+	devs.OneShotMergeProcessor.Initialize()
+}
+
+// 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 (devs *DevsAnalysis) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
+	if !devs.ShouldConsumeCommit(deps) {
+		return nil, nil
+	}
+	author := deps[identity.DependencyAuthor].(int)
+	treeDiff := deps[items.DependencyTreeChanges].(object.Changes)
+	if len(treeDiff) == 0 && !devs.ConsiderEmptyCommits {
+		return nil, nil
+	}
+	day := deps[items.DependencyDay].(int)
+	devsDay, exists := devs.days[day]
+	if !exists {
+		devsDay = map[int]*DevDay{}
+		devs.days[day] = devsDay
+	}
+	dd, exists := devsDay[author]
+	if !exists {
+		dd = &DevDay{}
+		devsDay[author] = dd
+	}
+	dd.Commits++
+	cache := deps[items.DependencyBlobCache].(map[plumbing.Hash]*items.CachedBlob)
+	fileDiffs := deps[items.DependencyFileDiff].(map[string]items.FileDiffData)
+	for _, change := range treeDiff {
+		action, err := change.Action()
+		if err != nil {
+			return nil, err
+		}
+		switch action {
+		case merkletrie.Insert:
+			blob := cache[change.To.TreeEntry.Hash]
+			lines, err := blob.CountLines()
+			if err != nil {
+				// binary
+				continue
+			}
+			dd.Added += lines
+		case merkletrie.Delete:
+			blob := cache[change.From.TreeEntry.Hash]
+			lines, err := blob.CountLines()
+			if err != nil {
+				// binary
+				continue
+			}
+			dd.Removed += lines
+		case merkletrie.Modify:
+			thisDiffs := fileDiffs[change.To.Name]
+			var removedPending int
+			for _, edit := range thisDiffs.Diffs {
+				switch edit.Type {
+				case diffmatchpatch.DiffEqual:
+					if removedPending > 0 {
+						dd.Removed += removedPending
+					}
+					removedPending = 0
+				case diffmatchpatch.DiffInsert:
+					added := utf8.RuneCountInString(edit.Text)
+					if removedPending > added {
+						dd.Changed += added
+						dd.Removed += removedPending - added
+					} else {
+						dd.Changed += removedPending
+						dd.Added += added - removedPending
+					}
+					removedPending = 0
+				case diffmatchpatch.DiffDelete:
+					removedPending = utf8.RuneCountInString(edit.Text)
+				}
+			}
+			if removedPending > 0 {
+				dd.Removed += removedPending
+			}
+		}
+	}
+	return nil, nil
+}
+
+// Finalize returns the result of the analysis. Further Consume() calls are not expected.
+func (devs *DevsAnalysis) Finalize() interface{} {
+	return DevsResult{
+		Days: devs.days,
+		reversedPeopleDict: devs.reversedPeopleDict,
+	}
+}
+
+// Fork clones this pipeline item.
+func (devs *DevsAnalysis) Fork(n int) []core.PipelineItem {
+	return core.ForkSamePipelineItem(devs, n)
+}
+
+// Serialize converts the analysis result as returned by Finalize() to text or bytes.
+// The text format is YAML and the bytes format is Protocol Buffers.
+func (devs *DevsAnalysis) Serialize(result interface{}, binary bool, writer io.Writer) error {
+	devsResult := result.(DevsResult)
+	if binary {
+		return devs.serializeBinary(&devsResult, writer)
+	}
+	devs.serializeText(&devsResult, writer)
+	return nil
+}
+
+// Deserialize converts the specified protobuf bytes to DevsResult.
+func (devs *DevsAnalysis) Deserialize(pbmessage []byte) (interface{}, error) {
+	message := pb.DevsAnalysisResult{}
+	err := proto.Unmarshal(pbmessage, &message)
+	if err != nil {
+		return nil, err
+	}
+	days := map[int]map[int]*DevDay{}
+	for day, dd := range message.Days {
+		rdd := map[int]*DevDay{}
+		days[int(day)] = rdd
+		for dev, stats := range dd.Devs {
+			if dev == -1 {
+				dev = identity.AuthorMissing
+			}
+			rdd[int(dev)] = &DevDay{
+				Commits: int(stats.Commits),
+				Added:   int(stats.Added),
+				Removed: int(stats.Removed),
+				Changed: int(stats.Changed),
+			}
+		}
+	}
+	result := DevsResult{
+		Days: days,
+		reversedPeopleDict: message.DevIndex,
+	}
+	return result, nil
+}
+
+// MergeResults combines two DevsAnalysis-es together.
+func (devs *DevsAnalysis) MergeResults(r1, r2 interface{}, c1, c2 *core.CommonAnalysisResult) interface{} {
+	cr1 := r1.(DevsResult)
+	cr2 := r2.(DevsResult)
+	merged := DevsResult{}
+	type devIndexPair struct {
+		Index1 int
+		Index2 int
+	}
+	devIndex := map[string]devIndexPair{}
+	for dev, devName := range cr1.reversedPeopleDict {
+		devIndex[devName] = devIndexPair{Index1: dev+1, Index2: devIndex[devName].Index2}
+	}
+	for dev, devName := range cr2.reversedPeopleDict {
+		devIndex[devName] = devIndexPair{Index1: devIndex[devName].Index1, Index2: dev+1}
+	}
+	jointDevSeq := make([]string, len(devIndex))
+	{
+		i := 0
+		for dev := range devIndex {
+			jointDevSeq[i] = dev
+			i++
+		}
+	}
+	sort.Strings(jointDevSeq)
+	merged.reversedPeopleDict = jointDevSeq
+	invDevIndex1 := map[int]int{}
+	invDevIndex2 := map[int]int{}
+	for i, dev := range jointDevSeq {
+		pair := devIndex[dev]
+		if pair.Index1 > 0 {
+			invDevIndex1[pair.Index1-1] = i
+		}
+		if pair.Index2 > 0 {
+			invDevIndex2[pair.Index2-1] = i
+		}
+	}
+	newDays := map[int]map[int]*DevDay{}
+	merged.Days = newDays
+	for day, dd := range cr1.Days {
+		newdd, exists := newDays[day]
+		if !exists {
+			newdd = map[int]*DevDay{}
+			newDays[day] = newdd
+		}
+		for dev, stats := range dd {
+			newdev := dev
+			if newdev != identity.AuthorMissing {
+				newdev = invDevIndex1[dev]
+			}
+			newstats, exists := newdd[newdev]
+			if !exists {
+				newstats = &DevDay{}
+				newdd[newdev] = newstats
+			}
+			newstats.Commits += stats.Commits
+			newstats.Added += stats.Added
+			newstats.Removed += stats.Removed
+			newstats.Changed += stats.Changed
+		}
+	}
+	for day, dd := range cr2.Days {
+		newdd, exists := newDays[day]
+		if !exists {
+			newdd = map[int]*DevDay{}
+			newDays[day] = newdd
+		}
+		for dev, stats := range dd {
+			newdev := dev
+			if newdev != identity.AuthorMissing {
+				newdev = invDevIndex2[dev]
+			}
+			newstats, exists := newdd[newdev]
+			if !exists {
+				newstats = &DevDay{}
+				newdd[newdev] = newstats
+			}
+			newstats.Commits += stats.Commits
+			newstats.Added += stats.Added
+			newstats.Removed += stats.Removed
+			newstats.Changed += stats.Changed
+		}
+	}
+	return merged
+}
+
+func (devs *DevsAnalysis) serializeText(result *DevsResult, writer io.Writer) {
+	fmt.Fprintln(writer, "  days:")
+	days := make([]int, len(result.Days))
+	{
+		i := 0
+		for day := range result.Days {
+			days[i] = day
+			i++
+		}
+	}
+	sort.Ints(days)
+	for _, day := range days {
+		fmt.Fprintf(writer, "    %d:\n", day)
+		rday := result.Days[day]
+		devseq := make([]int, len(rday))
+		{
+			i := 0
+			for dev := range rday {
+				devseq[i] = dev
+				i++
+			}
+		}
+		sort.Ints(devseq)
+		for _, dev := range devseq {
+			stats := rday[dev]
+			if dev == identity.AuthorMissing {
+				dev = -1
+			}
+			fmt.Fprintf(writer, "      %d: [%d, %d, %d, %d]\n",
+				dev, stats.Commits, stats.Added, stats.Removed, stats.Changed)
+		}
+	}
+	fmt.Fprintln(writer, "  people:")
+	for _, person := range result.reversedPeopleDict {
+		fmt.Fprintf(writer, "  - %s\n", yaml.SafeString(person))
+	}
+}
+
+func (devs *DevsAnalysis) serializeBinary(result *DevsResult, writer io.Writer) error {
+	message := pb.DevsAnalysisResult{}
+	message.DevIndex = result.reversedPeopleDict
+	message.Days = map[int32]*pb.DayDevs{}
+	for day, devs := range result.Days {
+		dd := &pb.DayDevs{}
+		message.Days[int32(day)] = dd
+		dd.Devs = map[int32]*pb.DevDay{}
+		for dev, stats := range devs {
+			if dev == identity.AuthorMissing {
+				dev = -1
+			}
+			dd.Devs[int32(dev)] = &pb.DevDay{
+				Commits: int32(stats.Commits),
+				Added:   int32(stats.Added),
+				Changed: int32(stats.Changed),
+				Removed: int32(stats.Removed),
+			}
+		}
+	}
+	serialized, err := proto.Marshal(&message)
+	if err != nil {
+		return err
+	}
+	_, err = writer.Write(serialized)
+	return err
+}
+
+func init() {
+	core.Registry.Register(&DevsAnalysis{})
+}

+ 332 - 0
leaves/devs_test.go

@@ -0,0 +1,332 @@
+package leaves
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/gogo/protobuf/proto"
+	"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.v5/internal/core"
+	"gopkg.in/src-d/hercules.v5/internal/pb"
+	"gopkg.in/src-d/hercules.v5/internal/plumbing"
+	"gopkg.in/src-d/hercules.v5/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v5/internal/test"
+	"gopkg.in/src-d/hercules.v5/internal/test/fixtures"
+)
+
+func fixtureDevs() *DevsAnalysis {
+	d := DevsAnalysis{}
+	d.Initialize(test.Repository)
+	people := [...]string{"one@srcd", "two@srcd"}
+	d.reversedPeopleDict = people[:]
+	return &d
+}
+
+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, 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.Flag(), "devs")
+	assert.Len(t, d.ListConfigurationOptions(), 1)
+	assert.Equal(t, d.ListConfigurationOptions()[0].Name, ConfigDevsConsiderEmptyCommits)
+	assert.Equal(t, d.ListConfigurationOptions()[0].Flag, "--empty-commits")
+	assert.Equal(t, d.ListConfigurationOptions()[0].Type, core.BoolConfigurationOption)
+	assert.Equal(t, d.ListConfigurationOptions()[0].Default, false)
+	assert.True(t, len(d.Description()) > 0)
+}
+
+func TestDevsRegistration(t *testing.T) {
+	summoned := core.Registry.Summon((&DevsAnalysis{}).Name())
+	assert.Len(t, summoned, 1)
+	assert.Equal(t, summoned[0].Name(), "Devs")
+	leaves := core.Registry.GetLeaves()
+	matched := false
+	for _, tp := range leaves {
+		if tp.Flag() == (&DevsAnalysis{}).Flag() {
+			matched = true
+			break
+		}
+	}
+	assert.True(t, matched)
+}
+
+func TestDevsConfigure(t *testing.T) {
+	devs := DevsAnalysis{}
+	facts := map[string]interface{}{}
+	facts[ConfigDevsConsiderEmptyCommits] = true
+	devs.Configure(facts)
+	assert.Equal(t, devs.ConsiderEmptyCommits, true)
+}
+
+func TestDevsInitialize(t *testing.T) {
+	d := fixtureDevs()
+	assert.NotNil(t, d.days)
+}
+
+func TestDevsConsumeFinalize(t *testing.T) {
+	devs := fixtureDevs()
+	deps := map[string]interface{}{}
+
+	// stage 1
+	deps[identity.DependencyAuthor] = 0
+	deps[plumbing.DependencyDay] = 0
+	cache := map[gitplumbing.Hash]*plumbing.CachedBlob{}
+	AddHash(t, cache, "291286b4ac41952cbd1389fda66420ec03c1a9fe")
+	AddHash(t, cache, "c29112dbd697ad9b401333b80c18a63951bc18d9")
+	AddHash(t, cache, "baa64828831d174f40140e4b3cfa77d1e917a2c1")
+	AddHash(t, cache, "dc248ba2b22048cc730c571a748e8ffcf7085ab9")
+	deps[plumbing.DependencyBlobCache] = cache
+	changes := make(object.Changes, 3)
+	treeFrom, _ := test.Repository.TreeObject(gitplumbing.NewHash(
+		"a1eb2ea76eb7f9bfbde9b243861474421000eb96"))
+	treeTo, _ := test.Repository.TreeObject(gitplumbing.NewHash(
+		"994eac1cd07235bb9815e547a75c84265dea00f5"))
+	changes[0] = &object.Change{From: object.ChangeEntry{
+		Name: "analyser.go",
+		Tree: treeFrom,
+		TreeEntry: object.TreeEntry{
+			Name: "analyser.go",
+			Mode: 0100644,
+			Hash: gitplumbing.NewHash("dc248ba2b22048cc730c571a748e8ffcf7085ab9"),
+		},
+	}, To: object.ChangeEntry{
+		Name: "analyser.go",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: "analyser.go",
+			Mode: 0100644,
+			Hash: gitplumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1"),
+		},
+	}}
+	changes[1] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
+		Name: "cmd/hercules/main.go",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: "cmd/hercules/main.go",
+			Mode: 0100644,
+			Hash: gitplumbing.NewHash("c29112dbd697ad9b401333b80c18a63951bc18d9"),
+		},
+	},
+	}
+	changes[2] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
+		Name: ".travis.yml",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: ".travis.yml",
+			Mode: 0100644,
+			Hash: gitplumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"),
+		},
+	},
+	}
+	deps[plumbing.DependencyTreeChanges] = changes
+	fd := fixtures.FileDiff()
+	result, err := fd.Consume(deps)
+	assert.Nil(t, err)
+	deps[plumbing.DependencyFileDiff] = result[plumbing.DependencyFileDiff]
+	deps[core.DependencyCommit], _ = test.Repository.CommitObject(gitplumbing.NewHash(
+		"cce947b98a050c6d356bc6ba95030254914027b1"))
+	deps[core.DependencyIsMerge] = false
+	result, err = devs.Consume(deps)
+	assert.Nil(t, result)
+	assert.Nil(t, err)
+	assert.Len(t, devs.days, 1)
+	day := devs.days[0]
+	assert.Len(t, day, 1)
+	dev := day[0]
+	assert.Equal(t, dev.Commits, 1)
+	assert.Equal(t, dev.Added, 847)
+	assert.Equal(t, dev.Removed, 9)
+	assert.Equal(t, dev.Changed, 67)
+
+	deps[identity.DependencyAuthor] = 1
+	result, err = devs.Consume(deps)
+	assert.Nil(t, result)
+	assert.Nil(t, err)
+	assert.Len(t, devs.days, 1)
+	day = devs.days[0]
+	assert.Len(t, day, 2)
+	for i := 0; i < 2; i++ {
+		dev = day[i]
+		assert.Equal(t, dev.Commits, 1)
+		assert.Equal(t, dev.Added, 847)
+		assert.Equal(t, dev.Removed, 9)
+		assert.Equal(t, dev.Changed, 67)
+	}
+
+	result, err = devs.Consume(deps)
+	assert.Nil(t, result)
+	assert.Nil(t, err)
+	assert.Len(t, devs.days, 1)
+	day = devs.days[0]
+	assert.Len(t, day, 2)
+	dev = day[0]
+	assert.Equal(t, dev.Commits, 1)
+	assert.Equal(t, dev.Added, 847)
+	assert.Equal(t, dev.Removed, 9)
+	assert.Equal(t, dev.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)
+
+	deps[plumbing.DependencyDay] = 1
+	result, err = devs.Consume(deps)
+	assert.Nil(t, result)
+	assert.Nil(t, err)
+	assert.Len(t, devs.days, 2)
+	day = devs.days[0]
+	assert.Len(t, day, 2)
+	dev = day[0]
+	assert.Equal(t, dev.Commits, 1)
+	assert.Equal(t, dev.Added, 847)
+	assert.Equal(t, dev.Removed, 9)
+	assert.Equal(t, dev.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)
+	day = devs.days[1]
+	assert.Len(t, day, 1)
+	dev = day[1]
+	assert.Equal(t, dev.Commits, 1)
+	assert.Equal(t, dev.Added, 847)
+	assert.Equal(t, dev.Removed, 9)
+	assert.Equal(t, dev.Changed, 67)
+}
+
+func TestDevsFinalize(t *testing.T) {
+	devs := fixtureDevs()
+	devs.days[1] = map[int]*DevDay{}
+	devs.days[1][1] = &DevDay{10, 20, 30, 40}
+	x := devs.Finalize().(DevsResult)
+	assert.Equal(t, x.Days, devs.days)
+	assert.Equal(t, x.reversedPeopleDict, devs.reversedPeopleDict)
+}
+
+func TestDevsFork(t *testing.T) {
+	devs := fixtureDevs()
+	clone := devs.Fork(1)[0].(*DevsAnalysis)
+	assert.True(t, devs == clone)
+}
+
+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[10] = map[int]*DevDay{}
+	devs.days[10][0] = &DevDay{11, 21, 31, 41}
+	devs.days[10][identity.AuthorMissing] = &DevDay{100, 200, 300, 400}
+	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]
+    10:
+      0: [11, 21, 31, 41]
+      -1: [100, 200, 300, 400]
+  people:
+  - "one@srcd"
+  - "two@srcd"
+`, buffer.String())
+
+	buffer = &bytes.Buffer{}
+	err = devs.Serialize(res, true, buffer)
+	assert.Nil(t, err)
+	msg := pb.DevsAnalysisResult{}
+	proto.Unmarshal(buffer.Bytes(), &msg)
+	assert.Equal(t, msg.DevIndex, devs.reversedPeopleDict)
+	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})
+	assert.Equal(t, msg.Days[1].Devs[1], &pb.DevDay{
+		Commits: 1, Added: 2, Removed: 3, Changed: 4})
+	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})
+	assert.Equal(t, msg.Days[10].Devs[-1], &pb.DevDay{
+		Commits: 100, Added: 200, Removed: 300, Changed: 400})
+}
+
+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[10] = map[int]*DevDay{}
+	devs.days[10][0] = &DevDay{11, 21, 31, 41}
+	devs.days[10][identity.AuthorMissing] = &DevDay{100, 200, 300, 400}
+	res := devs.Finalize().(DevsResult)
+	buffer := &bytes.Buffer{}
+	err := devs.Serialize(res, true, buffer)
+	assert.Nil(t, err)
+	rawres2, err := devs.Deserialize(buffer.Bytes())
+	assert.Nil(t, err)
+	res2 := rawres2.(DevsResult)
+	assert.Equal(t, res, res2)
+}
+
+func TestDevsMergeResults(t *testing.T) {
+	people1 := [...]string{"1@srcd", "2@srcd"}
+	people2 := [...]string{"3@srcd", "1@srcd"}
+	r1 := DevsResult{
+		Days: map[int]map[int]*DevDay{},
+		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[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[11] = map[int]*DevDay{}
+	r1.Days[11][1] = &DevDay{10, 20, 30, 40}
+	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[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[10] = map[int]*DevDay{}
+	r2.Days[10][0] = &DevDay{11, 21, 31, 41}
+	r2.Days[10][identity.AuthorMissing] = &DevDay{100, 200, 300, 400}
+
+	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[2], map[int]*DevDay{
+		identity.AuthorMissing: {100, 200, 300, 400},
+		2: {11, 21, 31, 41},
+	})
+	assert.Equal(t, rm.Days[1], map[int]*DevDay{
+		0: {11, 22, 33, 44},
+		1: {1, 2, 3, 4},
+		2: {10, 20, 30, 40},
+	})
+	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},
+	})
+}