浏览代码

Merge pull request #223 from vmarkovtsev/master

Extract LinesStats item and include it in --file-history
Vadim Markovtsev 6 年之前
父节点
当前提交
276e055ef6
共有 9 个文件被更改,包括 800 次插入422 次删除
  1. 97 87
      internal/pb/pb.pb.go
  2. 1 1
      internal/pb/pb.proto
  3. 88 32
      internal/pb/pb_pb2.py
  4. 161 0
      internal/plumbing/line_stats.go
  5. 130 0
      internal/plumbing/line_stats_test.go
  6. 23 117
      leaves/devs.go
  7. 70 56
      leaves/devs_test.go
  8. 114 39
      leaves/file_history.go
  9. 116 90
      leaves/file_history_test.go

+ 97 - 87
internal/pb/pb.pb.go

@@ -529,7 +529,8 @@ func (m *ShotnessAnalysisResults) GetRecords() []*ShotnessRecord {
 }
 
 type FileHistory struct {
-	Commits []string `protobuf:"bytes,1,rep,name=commits" json:"commits,omitempty"`
+	Commits            []string             `protobuf:"bytes,1,rep,name=commits" json:"commits,omitempty"`
+	ChangesByDeveloper map[int32]*LineStats `protobuf:"bytes,2,rep,name=changes_by_developer,json=changesByDeveloper" json:"changes_by_developer,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
 }
 
 func (m *FileHistory) Reset()                    { *m = FileHistory{} }
@@ -544,6 +545,13 @@ func (m *FileHistory) GetCommits() []string {
 	return nil
 }
 
+func (m *FileHistory) GetChangesByDeveloper() map[int32]*LineStats {
+	if m != nil {
+		return m.ChangesByDeveloper
+	}
+	return nil
+}
+
 type FileHistoryResultMessage struct {
 	Files map[string]*FileHistory `protobuf:"bytes,1,rep,name=files" json:"files,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
 }
@@ -765,90 +773,92 @@ func init() {
 func init() { proto.RegisterFile("pb.proto", fileDescriptorPb) }
 
 var fileDescriptorPb = []byte{
-	// 1350 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x57, 0xcd, 0x6e, 0xdb, 0xc6,
-	0x16, 0x06, 0xf5, 0xaf, 0x23, 0x5b, 0x4e, 0x26, 0xb9, 0x31, 0xa3, 0x0b, 0xe7, 0xea, 0x12, 0xb9,
-	0xb9, 0x6e, 0x93, 0x32, 0x81, 0xd2, 0x45, 0x9a, 0x6e, 0xe2, 0xd8, 0x0d, 0x62, 0x20, 0x6e, 0x8a,
-	0x51, 0x92, 0x2e, 0x89, 0xb1, 0x38, 0xb6, 0xd8, 0x8a, 0x43, 0x62, 0x86, 0x94, 0x2d, 0xa0, 0xcf,
-	0xd2, 0x5d, 0x17, 0x2d, 0xd0, 0x55, 0x5f, 0xa0, 0x8b, 0x6e, 0xfa, 0x22, 0x05, 0xfa, 0x16, 0xc5,
-	0xfc, 0x51, 0xa4, 0x4a, 0xa7, 0xed, 0x4e, 0xe7, 0x9c, 0xef, 0xcc, 0x7c, 0xf3, 0x9d, 0x33, 0x67,
-	0x28, 0xe8, 0xa5, 0xa7, 0x7e, 0xca, 0x93, 0x2c, 0xf1, 0x7e, 0x6b, 0x40, 0xef, 0x84, 0x66, 0x24,
-	0x24, 0x19, 0x41, 0x2e, 0x74, 0x97, 0x94, 0x8b, 0x28, 0x61, 0xae, 0x33, 0x76, 0xf6, 0xdb, 0xd8,
-	0x9a, 0x08, 0x41, 0x6b, 0x4e, 0xc4, 0xdc, 0x6d, 0x8c, 0x9d, 0xfd, 0x3e, 0x56, 0xbf, 0xd1, 0x1d,
-	0x00, 0x4e, 0xd3, 0x44, 0x44, 0x59, 0xc2, 0x57, 0x6e, 0x53, 0x45, 0x4a, 0x1e, 0x74, 0x0f, 0x76,
-	0x4e, 0xe9, 0x79, 0xc4, 0x82, 0x9c, 0x45, 0x97, 0x41, 0x16, 0xc5, 0xd4, 0x6d, 0x8d, 0x9d, 0xfd,
-	0x26, 0xde, 0x56, 0xee, 0xb7, 0x2c, 0xba, 0x7c, 0x13, 0xc5, 0x14, 0x79, 0xb0, 0x4d, 0x59, 0x58,
-	0x42, 0xb5, 0x15, 0x6a, 0x40, 0x59, 0x58, 0x60, 0x5c, 0xe8, 0xce, 0x92, 0x38, 0x8e, 0x32, 0xe1,
-	0x76, 0x34, 0x33, 0x63, 0xa2, 0xdb, 0xd0, 0xe3, 0x39, 0xd3, 0x89, 0x5d, 0x95, 0xd8, 0xe5, 0x39,
-	0x53, 0x49, 0x2f, 0xe1, 0xba, 0x0d, 0x05, 0x29, 0xe5, 0x41, 0x94, 0xd1, 0xd8, 0xed, 0x8d, 0x9b,
-	0xfb, 0x83, 0xc9, 0x9e, 0x6f, 0x0f, 0xed, 0x63, 0x8d, 0xfe, 0x82, 0xf2, 0xe3, 0x8c, 0xc6, 0x9f,
-	0xb1, 0x8c, 0xaf, 0xf0, 0x90, 0x57, 0x9c, 0xa3, 0x03, 0xb8, 0x51, 0x03, 0x43, 0xd7, 0xa0, 0xf9,
-	0x35, 0x5d, 0x29, 0xad, 0xfa, 0x58, 0xfe, 0x44, 0x37, 0xa1, 0xbd, 0x24, 0x8b, 0x9c, 0x2a, 0xa1,
-	0x1c, 0xac, 0x8d, 0xa7, 0x8d, 0x27, 0x8e, 0xf7, 0x18, 0x76, 0x9f, 0xe7, 0x9c, 0x85, 0xc9, 0x05,
-	0x9b, 0xa6, 0x84, 0x0b, 0x7a, 0x42, 0x32, 0x1e, 0x5d, 0xe2, 0xe4, 0x42, 0x1f, 0x6e, 0x91, 0xc7,
-	0x4c, 0xb8, 0xce, 0xb8, 0xb9, 0xbf, 0x8d, 0xad, 0xe9, 0xfd, 0xe0, 0xc0, 0xcd, 0xba, 0x2c, 0x59,
-	0x0f, 0x46, 0x62, 0x6a, 0xb6, 0x56, 0xbf, 0xd1, 0x5d, 0x18, 0xb2, 0x3c, 0x3e, 0xa5, 0x3c, 0x48,
-	0xce, 0x02, 0x9e, 0x5c, 0x08, 0x45, 0xa2, 0x8d, 0xb7, 0xb4, 0xf7, 0xf5, 0x19, 0x4e, 0x2e, 0x04,
-	0xfa, 0x10, 0xae, 0xaf, 0x51, 0x76, 0xdb, 0xa6, 0x02, 0xee, 0x58, 0xe0, 0xa1, 0x76, 0xa3, 0x07,
-	0xd0, 0x52, 0xeb, 0xb4, 0x94, 0x66, 0xae, 0x7f, 0xc5, 0x01, 0xb0, 0x42, 0x79, 0xdf, 0xc0, 0xf0,
-	0x45, 0xb4, 0xa0, 0xe2, 0xf5, 0x05, 0xa3, 0x5c, 0xcc, 0xa3, 0x14, 0x3d, 0xb2, 0x6a, 0x38, 0x6a,
-	0x81, 0x91, 0x5f, 0x8d, 0xfb, 0xef, 0x64, 0x50, 0x2b, 0xae, 0x81, 0xa3, 0x27, 0x00, 0x6b, 0x67,
-	0x59, 0xdf, 0x76, 0x8d, 0xbe, 0xed, 0xb2, 0xbe, 0xbf, 0x37, 0xd6, 0x02, 0x1f, 0x30, 0xb2, 0x58,
-	0x89, 0x48, 0x60, 0x2a, 0xf2, 0x45, 0x26, 0xd0, 0x18, 0x06, 0xe7, 0x9c, 0xb0, 0x7c, 0x41, 0x78,
-	0x94, 0xd9, 0xf5, 0xca, 0x2e, 0x34, 0x82, 0x9e, 0x20, 0x71, 0xba, 0x88, 0xd8, 0xb9, 0x59, 0xba,
-	0xb0, 0xd1, 0x43, 0xe8, 0xa6, 0x3c, 0xf9, 0x8a, 0xce, 0x32, 0xa5, 0xd3, 0x60, 0xf2, 0xaf, 0x7a,
-	0x21, 0x2c, 0x0a, 0xdd, 0x87, 0xf6, 0x99, 0x3c, 0xa8, 0xd1, 0xed, 0x0a, 0xb8, 0xc6, 0xa0, 0x8f,
-	0xa0, 0x93, 0xd2, 0x24, 0x5d, 0xc8, 0xb6, 0x7f, 0x0f, 0xda, 0x80, 0xd0, 0x31, 0x20, 0xfd, 0x2b,
-	0x88, 0x58, 0x46, 0x39, 0x99, 0x65, 0xf2, 0xb6, 0x76, 0x14, 0xaf, 0x91, 0x7f, 0x98, 0xc4, 0x29,
-	0xa7, 0x42, 0xd0, 0x50, 0x27, 0xe3, 0xe4, 0xc2, 0xe4, 0x5f, 0xd7, 0x59, 0xc7, 0xeb, 0x24, 0xf4,
-	0x04, 0x76, 0x14, 0x85, 0x20, 0xb1, 0x05, 0x71, 0xbb, 0x8a, 0xc2, 0xce, 0x46, 0x9d, 0xf0, 0xf0,
-	0xac, 0x62, 0x7b, 0x3f, 0x39, 0x70, 0xfb, 0xca, 0xad, 0x6a, 0xfa, 0xd0, 0xf9, 0xbb, 0x7d, 0xd8,
-	0xa8, 0xef, 0x43, 0x04, 0x2d, 0x79, 0x55, 0xdd, 0xe6, 0xb8, 0xb9, 0xdf, 0xc4, 0x2d, 0x3b, 0xab,
-	0x22, 0x16, 0x46, 0x33, 0x23, 0x73, 0x1b, 0x5b, 0x13, 0xdd, 0x82, 0x4e, 0xc4, 0xc2, 0x34, 0xe3,
-	0x4a, 0xd1, 0x26, 0x36, 0x96, 0x37, 0x85, 0xee, 0x61, 0x92, 0xa7, 0x52, 0xf4, 0x9b, 0xd0, 0x8e,
-	0x58, 0x48, 0x2f, 0x55, 0x63, 0xf6, 0xb1, 0x36, 0xd0, 0x04, 0x3a, 0xb1, 0x3a, 0x82, 0xe2, 0xf1,
-	0x7e, 0x3d, 0x0d, 0xd2, 0xbb, 0x0b, 0x5b, 0x6f, 0x92, 0x7c, 0x36, 0xa7, 0xa1, 0xd2, 0x4c, 0xae,
-	0xac, 0x6b, 0xef, 0x28, 0x52, 0xda, 0xf0, 0x7e, 0x75, 0xe0, 0x96, 0xd9, 0x7b, 0xb3, 0x37, 0xef,
-	0xc3, 0x96, 0xc4, 0x04, 0x33, 0x1d, 0x36, 0xa5, 0xec, 0xf9, 0x06, 0x8e, 0x07, 0x32, 0x6a, 0x79,
-	0x3f, 0x84, 0xa1, 0xa9, 0xbe, 0x85, 0x77, 0x37, 0xe0, 0xdb, 0x3a, 0x6e, 0x13, 0x1e, 0xc1, 0x96,
-	0x49, 0xd0, 0xac, 0xf4, 0xf4, 0xdb, 0xf6, 0xcb, 0x9c, 0xf1, 0x40, 0x43, 0xf4, 0x01, 0xfe, 0x03,
-	0x03, 0xdd, 0x15, 0x8b, 0x88, 0x51, 0xe1, 0xf6, 0xd5, 0x31, 0x40, 0xb9, 0x5e, 0x49, 0x8f, 0xf7,
-	0x9d, 0x03, 0xf0, 0xf6, 0x60, 0xfa, 0xe6, 0x70, 0x4e, 0xd8, 0x39, 0x45, 0xff, 0x86, 0xbe, 0xe2,
-	0x5f, 0x1a, 0x47, 0x3d, 0xe9, 0xf8, 0x5c, 0x8e, 0xa4, 0x3d, 0x00, 0xc1, 0x67, 0xc1, 0x29, 0x3d,
-	0x4b, 0x38, 0x35, 0x8f, 0x47, 0x5f, 0xf0, 0xd9, 0x73, 0xe5, 0x90, 0xb9, 0x32, 0x4c, 0xce, 0x32,
-	0xca, 0xcd, 0x03, 0xd2, 0x13, 0x7c, 0x76, 0x20, 0x6d, 0x49, 0x24, 0x27, 0x22, 0xb3, 0xc9, 0x2d,
-	0xfd, 0xbe, 0x48, 0x97, 0xc9, 0xde, 0x03, 0x65, 0x99, 0xf4, 0xb6, 0x5e, 0x5c, 0x7a, 0x54, 0xbe,
-	0xf7, 0x0c, 0x76, 0xd7, 0x34, 0xc5, 0x94, 0x2c, 0x29, 0xb7, 0x9a, 0xff, 0x0f, 0xba, 0x33, 0xed,
-	0x36, 0x93, 0x69, 0xe0, 0xaf, 0xa1, 0xd8, 0xc6, 0xbc, 0x5f, 0x1c, 0x18, 0x4e, 0xe7, 0x49, 0xc6,
-	0xa8, 0x10, 0x98, 0xce, 0x12, 0x1e, 0xca, 0x4e, 0xcc, 0x56, 0x69, 0x31, 0x77, 0xe5, 0xef, 0x62,
-	0x16, 0x37, 0x4a, 0xb3, 0x18, 0x41, 0x4b, 0x8a, 0x60, 0x0e, 0xa5, 0x7e, 0xa3, 0x4f, 0xa0, 0x37,
-	0x4b, 0x72, 0x79, 0x01, 0xed, 0x64, 0xd8, 0xf3, 0xab, 0xcb, 0xcb, 0x2a, 0xaa, 0xb8, 0x9e, 0x89,
-	0x05, 0x7c, 0xf4, 0x29, 0x6c, 0x57, 0x42, 0xff, 0x68, 0x32, 0x1e, 0xc1, 0xae, 0xdd, 0x66, 0xb3,
-	0xf9, 0x3e, 0x80, 0x2e, 0x57, 0x3b, 0x5b, 0x21, 0x76, 0x36, 0x18, 0x61, 0x1b, 0xf7, 0xfe, 0x0f,
-	0x03, 0xd9, 0x20, 0x2f, 0x23, 0xa1, 0x1e, 0xf7, 0xd2, 0x83, 0xac, 0xef, 0x90, 0x35, 0xbd, 0x6f,
-	0x1d, 0x70, 0x4b, 0x48, 0xbd, 0xd5, 0x09, 0x15, 0x82, 0x9c, 0x53, 0xf4, 0xb4, 0x7c, 0x3d, 0x06,
-	0x93, 0xbb, 0xfe, 0x55, 0x48, 0x3d, 0x82, 0xcc, 0xdb, 0xa0, 0x52, 0x46, 0x2f, 0x00, 0xd6, 0xce,
-	0x9a, 0xb7, 0xd7, 0x2b, 0x2b, 0x30, 0x98, 0x6c, 0x55, 0xd6, 0x2e, 0xe9, 0xf1, 0x16, 0xfa, 0xb2,
-	0x93, 0xa7, 0x19, 0xc9, 0xd4, 0x7d, 0x25, 0x61, 0x48, 0x43, 0x23, 0xa5, 0x36, 0xe4, 0xe9, 0x38,
-	0x8d, 0x93, 0x25, 0x0d, 0x8d, 0x9c, 0xd6, 0x54, 0xe7, 0x56, 0xed, 0x11, 0x9a, 0x47, 0xd3, 0x9a,
-	0xb2, 0x5b, 0x3a, 0x47, 0x74, 0x79, 0x44, 0x36, 0xc4, 0xa9, 0x7c, 0xad, 0x8c, 0xa1, 0x2d, 0xe4,
-	0xbe, 0x86, 0x23, 0xf8, 0x05, 0x13, 0xac, 0x03, 0xe8, 0x63, 0xe8, 0x2f, 0x08, 0x3b, 0xcf, 0x89,
-	0xec, 0xce, 0xa6, 0x52, 0xe9, 0x96, 0xaf, 0xd7, 0xf5, 0x5f, 0xd9, 0x80, 0xd6, 0x65, 0x0d, 0x1c,
-	0xbd, 0x84, 0x61, 0x35, 0x58, 0xa3, 0xcf, 0xb8, 0xaa, 0x4f, 0x65, 0xef, 0xb5, 0x3a, 0x02, 0xba,
-	0x47, 0x64, 0x75, 0x44, 0x97, 0x02, 0xdd, 0x83, 0x56, 0x48, 0x97, 0xb6, 0x56, 0xc8, 0x37, 0x7e,
-	0xc9, 0xc6, 0x30, 0x50, 0xf1, 0xd1, 0x33, 0xe8, 0x17, 0xae, 0x9a, 0xce, 0xdc, 0xab, 0xee, 0xdb,
-	0x35, 0xa7, 0x29, 0x6f, 0xfa, 0xbd, 0x03, 0x37, 0xe4, 0x12, 0x9b, 0xfd, 0x39, 0x91, 0x83, 0x7f,
-	0x65, 0x19, 0xdc, 0xf1, 0x6b, 0x30, 0x92, 0x55, 0xc1, 0x86, 0xac, 0x84, 0x1c, 0x2a, 0x21, 0x5d,
-	0x06, 0x7a, 0xbe, 0x37, 0x54, 0x6f, 0xf6, 0x42, 0xba, 0x3c, 0x96, 0xf6, 0xe8, 0x00, 0xfa, 0x05,
-	0xbe, 0x86, 0xea, 0x9d, 0x2a, 0xd5, 0x9e, 0x3d, 0x72, 0x99, 0xeb, 0x97, 0xd0, 0x9f, 0x52, 0x26,
-	0x3f, 0x2a, 0x59, 0xb6, 0xbe, 0x75, 0x72, 0x91, 0x86, 0x81, 0xc9, 0xaf, 0x09, 0x59, 0x70, 0xca,
-	0x54, 0xa1, 0x15, 0x03, 0x6b, 0x97, 0x7b, 0xa3, 0x59, 0xbd, 0x38, 0x3f, 0x3b, 0xb0, 0x7b, 0xa8,
-	0x61, 0xc5, 0x06, 0x56, 0x88, 0x77, 0x70, 0x4d, 0x58, 0x5f, 0x70, 0xba, 0x0a, 0x42, 0xb2, 0x32,
-	0xa2, 0x3c, 0xf0, 0xaf, 0xc8, 0xf1, 0x0b, 0xc7, 0xf3, 0xd5, 0x11, 0x59, 0x99, 0x0f, 0x5b, 0x51,
-	0x71, 0x8e, 0x4e, 0xe0, 0x46, 0x0d, 0xac, 0x46, 0x99, 0x3f, 0x35, 0xcf, 0x7a, 0xbb, 0x92, 0x36,
-	0x3f, 0x3a, 0xb0, 0xb3, 0x59, 0xc3, 0xff, 0x42, 0x67, 0x4e, 0x49, 0x48, 0xb9, 0x5a, 0x6e, 0x30,
-	0xe9, 0x17, 0x9f, 0xde, 0xd8, 0x04, 0xd0, 0x53, 0xa9, 0x17, 0xcb, 0x0a, 0xbd, 0x64, 0xa9, 0x37,
-	0xcb, 0x7c, 0x68, 0x00, 0xc5, 0x68, 0xd4, 0xa6, 0x1e, 0x8d, 0xa5, 0xd0, 0x5f, 0x7d, 0x94, 0x6f,
-	0x95, 0xf8, 0x9e, 0x76, 0xd4, 0x9f, 0xa0, 0xc7, 0x7f, 0x04, 0x00, 0x00, 0xff, 0xff, 0x0b, 0x16,
-	0xa3, 0xa0, 0x10, 0x0d, 0x00, 0x00,
+	// 1390 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x57, 0x4d, 0x8f, 0xdc, 0x44,
+	0x13, 0x96, 0xe7, 0x7b, 0x6a, 0x76, 0x67, 0x93, 0xce, 0xbe, 0x59, 0x67, 0x5e, 0x6d, 0x18, 0xac,
+	0x10, 0x2d, 0x24, 0x38, 0xd1, 0x86, 0x43, 0x08, 0x97, 0xec, 0x07, 0x51, 0x56, 0xca, 0x12, 0xf0,
+	0x24, 0xe1, 0x68, 0xf5, 0xda, 0xbd, 0x3b, 0x86, 0x71, 0xdb, 0xea, 0xb6, 0x67, 0x76, 0x24, 0x7e,
+	0x0b, 0x37, 0x0e, 0x20, 0x71, 0xe2, 0x0f, 0x70, 0xe0, 0xc2, 0x95, 0x1f, 0x81, 0xc4, 0xbf, 0x40,
+	0xfd, 0xe5, 0xb1, 0x07, 0x6f, 0x08, 0x37, 0x57, 0xd5, 0x53, 0xdd, 0x4f, 0x3f, 0x55, 0x5d, 0x3d,
+	0x03, 0xbd, 0xf4, 0xcc, 0x4d, 0x59, 0x92, 0x25, 0xce, 0x9f, 0x0d, 0xe8, 0x9d, 0x92, 0x0c, 0x87,
+	0x38, 0xc3, 0xc8, 0x86, 0xee, 0x9c, 0x30, 0x1e, 0x25, 0xd4, 0xb6, 0xc6, 0xd6, 0x5e, 0xdb, 0x33,
+	0x26, 0x42, 0xd0, 0x9a, 0x62, 0x3e, 0xb5, 0x1b, 0x63, 0x6b, 0xaf, 0xef, 0xc9, 0x6f, 0x74, 0x1b,
+	0x80, 0x91, 0x34, 0xe1, 0x51, 0x96, 0xb0, 0xa5, 0xdd, 0x94, 0x91, 0x92, 0x07, 0xdd, 0x85, 0xad,
+	0x33, 0x72, 0x11, 0x51, 0x3f, 0xa7, 0xd1, 0xa5, 0x9f, 0x45, 0x31, 0xb1, 0x5b, 0x63, 0x6b, 0xaf,
+	0xe9, 0x6d, 0x4a, 0xf7, 0x6b, 0x1a, 0x5d, 0xbe, 0x8a, 0x62, 0x82, 0x1c, 0xd8, 0x24, 0x34, 0x2c,
+	0xa1, 0xda, 0x12, 0x35, 0x20, 0x34, 0x2c, 0x30, 0x36, 0x74, 0x83, 0x24, 0x8e, 0xa3, 0x8c, 0xdb,
+	0x1d, 0xc5, 0x4c, 0x9b, 0xe8, 0x16, 0xf4, 0x58, 0x4e, 0x55, 0x62, 0x57, 0x26, 0x76, 0x59, 0x4e,
+	0x65, 0xd2, 0x73, 0xb8, 0x6e, 0x42, 0x7e, 0x4a, 0x98, 0x1f, 0x65, 0x24, 0xb6, 0x7b, 0xe3, 0xe6,
+	0xde, 0x60, 0x7f, 0xd7, 0x35, 0x87, 0x76, 0x3d, 0x85, 0xfe, 0x92, 0xb0, 0x93, 0x8c, 0xc4, 0x9f,
+	0xd3, 0x8c, 0x2d, 0xbd, 0x21, 0xab, 0x38, 0x47, 0x07, 0x70, 0xa3, 0x06, 0x86, 0xae, 0x41, 0xf3,
+	0x5b, 0xb2, 0x94, 0x5a, 0xf5, 0x3d, 0xf1, 0x89, 0xb6, 0xa1, 0x3d, 0xc7, 0xb3, 0x9c, 0x48, 0xa1,
+	0x2c, 0x4f, 0x19, 0x4f, 0x1a, 0x8f, 0x2d, 0xe7, 0x11, 0xec, 0x1c, 0xe6, 0x8c, 0x86, 0xc9, 0x82,
+	0x4e, 0x52, 0xcc, 0x38, 0x39, 0xc5, 0x19, 0x8b, 0x2e, 0xbd, 0x64, 0xa1, 0x0e, 0x37, 0xcb, 0x63,
+	0xca, 0x6d, 0x6b, 0xdc, 0xdc, 0xdb, 0xf4, 0x8c, 0xe9, 0xfc, 0x64, 0xc1, 0x76, 0x5d, 0x96, 0xa8,
+	0x07, 0xc5, 0x31, 0xd1, 0x5b, 0xcb, 0x6f, 0x74, 0x07, 0x86, 0x34, 0x8f, 0xcf, 0x08, 0xf3, 0x93,
+	0x73, 0x9f, 0x25, 0x0b, 0x2e, 0x49, 0xb4, 0xbd, 0x0d, 0xe5, 0x7d, 0x79, 0xee, 0x25, 0x0b, 0x8e,
+	0x3e, 0x82, 0xeb, 0x2b, 0x94, 0xd9, 0xb6, 0x29, 0x81, 0x5b, 0x06, 0x78, 0xa4, 0xdc, 0xe8, 0x3e,
+	0xb4, 0xe4, 0x3a, 0x2d, 0xa9, 0x99, 0xed, 0x5e, 0x71, 0x00, 0x4f, 0xa2, 0x9c, 0xef, 0x60, 0xf8,
+	0x2c, 0x9a, 0x11, 0xfe, 0x72, 0x41, 0x09, 0xe3, 0xd3, 0x28, 0x45, 0x0f, 0x8d, 0x1a, 0x96, 0x5c,
+	0x60, 0xe4, 0x56, 0xe3, 0xee, 0x1b, 0x11, 0x54, 0x8a, 0x2b, 0xe0, 0xe8, 0x31, 0xc0, 0xca, 0x59,
+	0xd6, 0xb7, 0x5d, 0xa3, 0x6f, 0xbb, 0xac, 0xef, 0x5f, 0x8d, 0x95, 0xc0, 0x07, 0x14, 0xcf, 0x96,
+	0x3c, 0xe2, 0x1e, 0xe1, 0xf9, 0x2c, 0xe3, 0x68, 0x0c, 0x83, 0x0b, 0x86, 0x69, 0x3e, 0xc3, 0x2c,
+	0xca, 0xcc, 0x7a, 0x65, 0x17, 0x1a, 0x41, 0x8f, 0xe3, 0x38, 0x9d, 0x45, 0xf4, 0x42, 0x2f, 0x5d,
+	0xd8, 0xe8, 0x01, 0x74, 0x53, 0x96, 0x7c, 0x43, 0x82, 0x4c, 0xea, 0x34, 0xd8, 0xff, 0x5f, 0xbd,
+	0x10, 0x06, 0x85, 0xee, 0x41, 0xfb, 0x5c, 0x1c, 0x54, 0xeb, 0x76, 0x05, 0x5c, 0x61, 0xd0, 0xc7,
+	0xd0, 0x49, 0x49, 0x92, 0xce, 0x44, 0xdb, 0xbf, 0x05, 0xad, 0x41, 0xe8, 0x04, 0x90, 0xfa, 0xf2,
+	0x23, 0x9a, 0x11, 0x86, 0x83, 0x4c, 0xdc, 0xd6, 0x8e, 0xe4, 0x35, 0x72, 0x8f, 0x92, 0x38, 0x65,
+	0x84, 0x73, 0x12, 0xaa, 0x64, 0x2f, 0x59, 0xe8, 0xfc, 0xeb, 0x2a, 0xeb, 0x64, 0x95, 0x84, 0x1e,
+	0xc3, 0x96, 0xa4, 0xe0, 0x27, 0xa6, 0x20, 0x76, 0x57, 0x52, 0xd8, 0x5a, 0xab, 0x93, 0x37, 0x3c,
+	0xaf, 0xd8, 0xce, 0x2f, 0x16, 0xdc, 0xba, 0x72, 0xab, 0x9a, 0x3e, 0xb4, 0xde, 0xb5, 0x0f, 0x1b,
+	0xf5, 0x7d, 0x88, 0xa0, 0x25, 0xae, 0xaa, 0xdd, 0x1c, 0x37, 0xf7, 0x9a, 0x5e, 0xcb, 0xcc, 0xaa,
+	0x88, 0x86, 0x51, 0xa0, 0x65, 0x6e, 0x7b, 0xc6, 0x44, 0x37, 0xa1, 0x13, 0xd1, 0x30, 0xcd, 0x98,
+	0x54, 0xb4, 0xe9, 0x69, 0xcb, 0x99, 0x40, 0xf7, 0x28, 0xc9, 0x53, 0x21, 0xfa, 0x36, 0xb4, 0x23,
+	0x1a, 0x92, 0x4b, 0xd9, 0x98, 0x7d, 0x4f, 0x19, 0x68, 0x1f, 0x3a, 0xb1, 0x3c, 0x82, 0xe4, 0xf1,
+	0x76, 0x3d, 0x35, 0xd2, 0xb9, 0x03, 0x1b, 0xaf, 0x92, 0x3c, 0x98, 0x92, 0x50, 0x6a, 0x26, 0x56,
+	0x56, 0xb5, 0xb7, 0x24, 0x29, 0x65, 0x38, 0xbf, 0x5b, 0x70, 0x53, 0xef, 0xbd, 0xde, 0x9b, 0xf7,
+	0x60, 0x43, 0x60, 0xfc, 0x40, 0x85, 0x75, 0x29, 0x7b, 0xae, 0x86, 0x7b, 0x03, 0x11, 0x35, 0xbc,
+	0x1f, 0xc0, 0x50, 0x57, 0xdf, 0xc0, 0xbb, 0x6b, 0xf0, 0x4d, 0x15, 0x37, 0x09, 0x0f, 0x61, 0x43,
+	0x27, 0x28, 0x56, 0x6a, 0xfa, 0x6d, 0xba, 0x65, 0xce, 0xde, 0x40, 0x41, 0xd4, 0x01, 0xde, 0x83,
+	0x81, 0xea, 0x8a, 0x59, 0x44, 0x09, 0xb7, 0xfb, 0xf2, 0x18, 0x20, 0x5d, 0x2f, 0x84, 0xc7, 0xf9,
+	0xc1, 0x02, 0x78, 0x7d, 0x30, 0x79, 0x75, 0x34, 0xc5, 0xf4, 0x82, 0xa0, 0xff, 0x43, 0x5f, 0xf2,
+	0x2f, 0x8d, 0xa3, 0x9e, 0x70, 0x7c, 0x21, 0x46, 0xd2, 0x2e, 0x00, 0x67, 0x81, 0x7f, 0x46, 0xce,
+	0x13, 0x46, 0xf4, 0xe3, 0xd1, 0xe7, 0x2c, 0x38, 0x94, 0x0e, 0x91, 0x2b, 0xc2, 0xf8, 0x3c, 0x23,
+	0x4c, 0x3f, 0x20, 0x3d, 0xce, 0x82, 0x03, 0x61, 0x0b, 0x22, 0x39, 0xe6, 0x99, 0x49, 0x6e, 0xa9,
+	0xf7, 0x45, 0xb8, 0x74, 0xf6, 0x2e, 0x48, 0x4b, 0xa7, 0xb7, 0xd5, 0xe2, 0xc2, 0x23, 0xf3, 0x9d,
+	0xa7, 0xb0, 0xb3, 0xa2, 0xc9, 0x27, 0x78, 0x4e, 0x98, 0xd1, 0xfc, 0x03, 0xe8, 0x06, 0xca, 0xad,
+	0x27, 0xd3, 0xc0, 0x5d, 0x41, 0x3d, 0x13, 0x73, 0x7e, 0xb3, 0x60, 0x38, 0x99, 0x26, 0x19, 0x25,
+	0x9c, 0x7b, 0x24, 0x48, 0x58, 0x28, 0x3a, 0x31, 0x5b, 0xa6, 0xc5, 0xdc, 0x15, 0xdf, 0xc5, 0x2c,
+	0x6e, 0x94, 0x66, 0x31, 0x82, 0x96, 0x10, 0x41, 0x1f, 0x4a, 0x7e, 0xa3, 0x4f, 0xa1, 0x17, 0x24,
+	0xb9, 0xb8, 0x80, 0x66, 0x32, 0xec, 0xba, 0xd5, 0xe5, 0x45, 0x15, 0x65, 0x5c, 0xcd, 0xc4, 0x02,
+	0x3e, 0xfa, 0x0c, 0x36, 0x2b, 0xa1, 0xff, 0x34, 0x19, 0x8f, 0x61, 0xc7, 0x6c, 0xb3, 0xde, 0x7c,
+	0x1f, 0x42, 0x97, 0xc9, 0x9d, 0x8d, 0x10, 0x5b, 0x6b, 0x8c, 0x3c, 0x13, 0x77, 0xfe, 0xb0, 0x60,
+	0x20, 0x3a, 0xe4, 0x79, 0xc4, 0xe5, 0xeb, 0x5e, 0x7a, 0x91, 0xd5, 0x25, 0x2a, 0x5e, 0xe4, 0x37,
+	0xb0, 0xad, 0x15, 0xf4, 0xcf, 0x96, 0x7e, 0x48, 0xe6, 0x64, 0x96, 0xa4, 0x84, 0xd9, 0x0d, 0xb9,
+	0xc3, 0x1d, 0xb7, 0xb4, 0x8a, 0xab, 0xab, 0x73, 0xb8, 0x3c, 0x36, 0x30, 0x75, 0x74, 0x14, 0xfc,
+	0x23, 0x30, 0xfa, 0x0a, 0x76, 0xae, 0x80, 0xd7, 0xc8, 0x31, 0x2e, 0xcb, 0x31, 0xd8, 0x07, 0x57,
+	0x34, 0xef, 0x24, 0xc3, 0x19, 0x2f, 0x4b, 0xf3, 0xbd, 0x05, 0x76, 0x89, 0x8e, 0x92, 0xe5, 0x94,
+	0x70, 0x8e, 0x2f, 0x08, 0x7a, 0x52, 0xbe, 0xca, 0x6b, 0xc4, 0x2b, 0x48, 0x35, 0x2e, 0xf5, 0x3b,
+	0x26, 0x53, 0x46, 0xcf, 0x00, 0x56, 0xce, 0x9a, 0xdf, 0x09, 0x4e, 0x95, 0xde, 0x46, 0x65, 0xed,
+	0x12, 0xc1, 0xd7, 0xd0, 0x2f, 0x88, 0x8b, 0x12, 0xe3, 0x30, 0x24, 0xa1, 0x3e, 0xa7, 0x32, 0x44,
+	0x21, 0x18, 0x89, 0x93, 0x39, 0x09, 0x75, 0xe9, 0x8d, 0x29, 0x4b, 0x24, 0x05, 0x0b, 0xf5, 0x03,
+	0x6f, 0x4c, 0xd1, 0xd9, 0x9d, 0x63, 0x32, 0x3f, 0xc6, 0x6b, 0x75, 0xac, 0xfc, 0xb2, 0x1a, 0x43,
+	0x9b, 0x8b, 0x7d, 0xeb, 0x24, 0x94, 0x01, 0xf4, 0x09, 0xf4, 0x67, 0x98, 0x5e, 0xe4, 0x58, 0xdc,
+	0xa4, 0xa6, 0x54, 0xe9, 0xa6, 0xab, 0xd6, 0x75, 0x5f, 0x98, 0x80, 0xd2, 0x65, 0x05, 0x1c, 0x3d,
+	0x87, 0x61, 0x35, 0x58, 0xa3, 0xcf, 0xbb, 0x95, 0x8f, 0x43, 0xf7, 0x18, 0x8b, 0x5e, 0xe0, 0xe8,
+	0x2e, 0xb4, 0x42, 0x32, 0x37, 0xb5, 0x42, 0xae, 0xf6, 0x0b, 0x36, 0x9a, 0x81, 0x8c, 0x8f, 0x9e,
+	0x42, 0xbf, 0x70, 0xd5, 0xb4, 0xcd, 0x6e, 0x75, 0xdf, 0xae, 0x3e, 0x4d, 0x79, 0xd3, 0x1f, 0x2d,
+	0xb8, 0x21, 0x96, 0x58, 0xbf, 0x4b, 0xfb, 0xe2, 0x91, 0x5a, 0x1a, 0x06, 0xb7, 0xdd, 0x1a, 0x8c,
+	0x60, 0x55, 0xb0, 0xc1, 0x4b, 0x2e, 0x06, 0x60, 0x48, 0xe6, 0xbe, 0x7a, 0x8b, 0x1a, 0xf2, 0x1a,
+	0xf5, 0x42, 0x32, 0x3f, 0x11, 0xf6, 0xe8, 0x00, 0xfa, 0x05, 0xbe, 0x86, 0xea, 0xed, 0x2a, 0xd5,
+	0x9e, 0x39, 0x72, 0x99, 0xeb, 0xd7, 0xd0, 0x9f, 0x10, 0x2a, 0x7e, 0x00, 0xd3, 0x6c, 0x35, 0x21,
+	0xc4, 0x22, 0x0d, 0x0d, 0x13, 0xbf, 0x7c, 0x44, 0xc1, 0x09, 0x95, 0x85, 0x96, 0x0c, 0x8c, 0x5d,
+	0xee, 0x8d, 0x66, 0xe5, 0x8e, 0x3b, 0xbf, 0x5a, 0xb0, 0x73, 0xa4, 0x60, 0xc5, 0x06, 0x46, 0x88,
+	0x37, 0x70, 0x8d, 0x1b, 0x9f, 0x9c, 0x00, 0x78, 0xa9, 0x45, 0xb9, 0xef, 0x5e, 0x91, 0xe3, 0x16,
+	0x8e, 0xc3, 0xe5, 0x31, 0x5e, 0xea, 0x1f, 0xe1, 0xbc, 0xe2, 0x1c, 0x9d, 0xc2, 0x8d, 0x1a, 0xd8,
+	0xbb, 0xdc, 0xfd, 0xd5, 0x76, 0x25, 0x6d, 0x7e, 0xb6, 0x60, 0x6b, 0xbd, 0x86, 0xef, 0x43, 0x67,
+	0x4a, 0x70, 0x48, 0x98, 0x5c, 0x6e, 0xb0, 0xdf, 0x2f, 0xfe, 0x26, 0x78, 0x3a, 0x80, 0x9e, 0x08,
+	0xbd, 0x68, 0x56, 0xe8, 0x25, 0x4a, 0xbd, 0x5e, 0xe6, 0x23, 0x0d, 0x28, 0xc6, 0xb8, 0x32, 0xd5,
+	0x18, 0x2f, 0x85, 0xfe, 0xed, 0x0f, 0xc4, 0x46, 0x89, 0xef, 0x59, 0x47, 0xfe, 0x61, 0x7b, 0xf4,
+	0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0x48, 0x21, 0x38, 0x53, 0xbc, 0x0d, 0x00, 0x00,
 }

+ 1 - 1
internal/pb/pb.proto

@@ -109,7 +109,7 @@ message ShotnessAnalysisResults {
 
 message FileHistory {
     repeated string commits = 1;
-    //map<int32, int32> changes_by_developer = 2;
+    map<int32, LineStats> changes_by_developer = 2;
 }
 
 message FileHistoryResultMessage {

文件差异内容过多而无法显示
+ 88 - 32
internal/pb/pb_pb2.py


+ 161 - 0
internal/plumbing/line_stats.go

@@ -0,0 +1,161 @@
+package plumbing
+
+import (
+	"unicode/utf8"
+
+	"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.v8/internal/core"
+)
+
+// LinesStatsCalculator measures line statistics for each text file in the commit.
+type LinesStatsCalculator struct {
+	core.NoopMerger
+}
+
+// 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.
+	Removed int
+	// Changed is the number of changed lines by a particular developer in a particular day.
+	Changed int
+}
+
+const (
+	// DependencyLineStats is the identifier of the data provided by LinesStatsCalculator - line
+	// statistics for each file in the commit.
+	DependencyLineStats = "line_stats"
+)
+
+// Name of this PipelineItem. Uniquely identifies the type, used for mapping keys, etc.
+func (lsc *LinesStatsCalculator) Name() string {
+	return "LinesStats"
+}
+
+// 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 (lsc *LinesStatsCalculator) Provides() []string {
+	arr := [...]string{DependencyLineStats}
+	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 (lsc *LinesStatsCalculator) Requires() []string {
+	arr := [...]string{DependencyTreeChanges, DependencyBlobCache, DependencyFileDiff}
+	return arr[:]
+}
+
+// ListConfigurationOptions returns the list of changeable public properties of this PipelineItem.
+func (lsc *LinesStatsCalculator) ListConfigurationOptions() []core.ConfigurationOption {
+	return nil
+}
+
+// Configure sets the properties previously published by ListConfigurationOptions().
+func (lsc *LinesStatsCalculator) 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 (lsc *LinesStatsCalculator) 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 (lsc *LinesStatsCalculator) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
+	result := map[object.ChangeEntry]LineStats{}
+	if deps[core.DependencyIsMerge].(bool) {
+		// we ignore merge commit diffs
+		// TODO(vmarkovtsev): handle them better
+		return map[string]interface{}{DependencyLineStats: result}, nil
+	}
+	treeDiff := deps[DependencyTreeChanges].(object.Changes)
+	cache := deps[DependencyBlobCache].(map[plumbing.Hash]*CachedBlob)
+	fileDiffs := deps[DependencyFileDiff].(map[string]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
+			}
+			result[change.To] = LineStats{
+				Added:   lines,
+				Removed: 0,
+				Changed: 0,
+			}
+		case merkletrie.Delete:
+			blob := cache[change.From.TreeEntry.Hash]
+			lines, err := blob.CountLines()
+			if err != nil {
+				// binary
+				continue
+			}
+			result[change.From] = LineStats{
+				Added:   0,
+				Removed: lines,
+				Changed: 0,
+			}
+		case merkletrie.Modify:
+			thisDiffs := fileDiffs[change.To.Name]
+			var added, removed, changed, removedPending int
+			for _, edit := range thisDiffs.Diffs {
+				switch edit.Type {
+				case diffmatchpatch.DiffEqual:
+					if removedPending > 0 {
+						removed += removedPending
+					}
+					removedPending = 0
+				case diffmatchpatch.DiffInsert:
+					delta := utf8.RuneCountInString(edit.Text)
+					if removedPending > delta {
+						changed += delta
+						removed += removedPending - delta
+					} else {
+						changed += removedPending
+						added += delta - removedPending
+					}
+					removedPending = 0
+				case diffmatchpatch.DiffDelete:
+					removedPending = utf8.RuneCountInString(edit.Text)
+				}
+			}
+			if removedPending > 0 {
+				removed += removedPending
+			}
+			result[change.To] = LineStats{
+				Added:   added,
+				Removed: removed,
+				Changed: changed,
+			}
+		}
+	}
+	return map[string]interface{}{DependencyLineStats: result}, nil
+}
+
+// Fork clones this PipelineItem.
+func (lsc *LinesStatsCalculator) Fork(n int) []core.PipelineItem {
+	return core.ForkSamePipelineItem(lsc, n)
+}
+
+func init() {
+	core.Registry.Register(&LinesStatsCalculator{})
+}

+ 130 - 0
internal/plumbing/line_stats_test.go

@@ -0,0 +1,130 @@
+package plumbing_test
+
+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.v8/internal/core"
+	items "gopkg.in/src-d/hercules.v8/internal/plumbing"
+	"gopkg.in/src-d/hercules.v8/internal/plumbing/identity"
+	"gopkg.in/src-d/hercules.v8/internal/test"
+	"gopkg.in/src-d/hercules.v8/internal/test/fixtures"
+)
+
+func TestLinesStatsMeta(t *testing.T) {
+	ra := &items.LinesStatsCalculator{}
+	assert.Equal(t, ra.Name(), "LinesStats")
+	assert.Equal(t, len(ra.Provides()), 1)
+	assert.Equal(t, ra.Provides()[0], items.DependencyLineStats)
+	assert.Equal(t, len(ra.Requires()), 3)
+	assert.Equal(t, ra.Requires()[0], items.DependencyTreeChanges)
+	assert.Equal(t, ra.Requires()[1], items.DependencyBlobCache)
+	assert.Equal(t, ra.Requires()[2], items.DependencyFileDiff)
+	assert.Nil(t, ra.ListConfigurationOptions())
+	assert.Nil(t, ra.Configure(nil))
+	for _, f := range ra.Fork(10) {
+		assert.Equal(t, f, ra)
+	}
+}
+
+func TestLinesStatsRegistration(t *testing.T) {
+	summoned := core.Registry.Summon((&items.LinesStatsCalculator{}).Name())
+	assert.Len(t, summoned, 1)
+	assert.Equal(t, summoned[0].Name(), "LinesStats")
+	summoned = core.Registry.Summon((&items.LinesStatsCalculator{}).Provides()[0])
+	assert.True(t, len(summoned) >= 1)
+	matched := false
+	for _, tp := range summoned {
+		matched = matched || tp.Name() == "LinesStats"
+	}
+	assert.True(t, matched)
+}
+
+func TestLinesStatsConsume(t *testing.T) {
+	deps := map[string]interface{}{}
+
+	// stage 1
+	deps[identity.DependencyAuthor] = 0
+	cache := map[plumbing.Hash]*items.CachedBlob{}
+	items.AddHash(t, cache, "291286b4ac41952cbd1389fda66420ec03c1a9fe")
+	items.AddHash(t, cache, "c29112dbd697ad9b401333b80c18a63951bc18d9")
+	items.AddHash(t, cache, "baa64828831d174f40140e4b3cfa77d1e917a2c1")
+	items.AddHash(t, cache, "dc248ba2b22048cc730c571a748e8ffcf7085ab9")
+	deps[items.DependencyBlobCache] = cache
+	changes := make(object.Changes, 3)
+	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
+		"a1eb2ea76eb7f9bfbde9b243861474421000eb96"))
+	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
+		"994eac1cd07235bb9815e547a75c84265dea00f5"))
+	changes[0] = &object.Change{From: object.ChangeEntry{
+		Name: "analyser.go",
+		Tree: treeFrom,
+		TreeEntry: object.TreeEntry{
+			Name: "analyser.go",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("dc248ba2b22048cc730c571a748e8ffcf7085ab9"),
+		},
+	}, To: object.ChangeEntry{
+		Name: "analyser2.go",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: "analyser2.go",
+			Mode: 0100644,
+			Hash: plumbing.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: plumbing.NewHash("c29112dbd697ad9b401333b80c18a63951bc18d9"),
+		},
+	},
+	}
+	changes[2] = &object.Change{From: object.ChangeEntry{
+		Name: ".travis.yml",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: ".travis.yml",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"),
+		},
+	}, To: object.ChangeEntry{},
+	}
+	deps[items.DependencyTreeChanges] = changes
+	fd := fixtures.FileDiff()
+	result, err := fd.Consume(deps)
+	assert.Nil(t, err)
+	deps[items.DependencyFileDiff] = result[items.DependencyFileDiff]
+	deps[core.DependencyCommit], _ = test.Repository.CommitObject(plumbing.NewHash(
+		"cce947b98a050c6d356bc6ba95030254914027b1"))
+	deps[core.DependencyIsMerge] = false
+	lsc := &items.LinesStatsCalculator{}
+	result, err = lsc.Consume(deps)
+	assert.Nil(t, err)
+	stats := result[items.DependencyLineStats].(map[object.ChangeEntry]items.LineStats)
+	assert.Len(t, stats, 3)
+	nameMap := map[string]items.LineStats{}
+	for ch, val := range stats {
+		nameMap[ch.Name] = val
+	}
+	assert.Equal(t, nameMap["analyser2.go"], items.LineStats{
+		Added:   628,
+		Removed: 9,
+		Changed: 67,
+	})
+	assert.Equal(t, nameMap[".travis.yml"], items.LineStats{
+		Added:   0,
+		Removed: 12,
+		Changed: 0,
+	})
+	assert.Equal(t, nameMap["cmd/hercules/main.go"], items.LineStats{
+		Added:   207,
+		Removed: 0,
+		Changed: 0,
+	})
+}

+ 23 - 117
leaves/devs.go

@@ -5,14 +5,11 @@ import (
 	"io"
 	"sort"
 	"strings"
-	"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.v8/internal/core"
 	"gopkg.in/src-d/hercules.v8/internal/pb"
 	items "gopkg.in/src-d/hercules.v8/internal/plumbing"
@@ -46,23 +43,13 @@ type DevsResult struct {
 	reversedPeopleDict []string
 }
 
-// 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.
-	Removed int
-	// Changed is the number of changed lines by a particular developer in a particular day.
-	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
+	items.LineStats
 	// LanguagesDetection carries fine-grained line stats per programming language.
-	Languages map[string]LineStats
+	Languages map[string]items.LineStats
 }
 
 const (
@@ -87,8 +74,8 @@ func (devs *DevsAnalysis) Provides() []string {
 // entities are Provides() upstream.
 func (devs *DevsAnalysis) Requires() []string {
 	arr := [...]string{
-		identity.DependencyAuthor, items.DependencyTreeChanges, items.DependencyFileDiff,
-		items.DependencyBlobCache, items.DependencyDay, items.DependencyLanguages}
+		identity.DependencyAuthor, items.DependencyTreeChanges, items.DependencyDay,
+		items.DependencyLanguages, items.DependencyLineStats}
 	return arr[:]
 }
 
@@ -154,7 +141,7 @@ func (devs *DevsAnalysis) Consume(deps map[string]interface{}) (map[string]inter
 	}
 	dd, exists := devsDay[author]
 	if !exists {
-		dd = &DevDay{Languages: map[string]LineStats{}}
+		dd = &DevDay{Languages: map[string]items.LineStats{}}
 		devsDay[author] = dd
 	}
 	dd.Commits++
@@ -163,99 +150,18 @@ func (devs *DevsAnalysis) Consume(deps map[string]interface{}) (map[string]inter
 		// TODO(vmarkovtsev): handle them
 		return nil, nil
 	}
-	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 {
-			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
-			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()
-			if err != nil {
-				// binary
-				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 {
-				switch edit.Type {
-				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 += 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
-						langStats := dd.Languages[lang]
-						dd.Languages[lang] = LineStats{
-							Added:   langStats.Added + added,
-							Removed: langStats.Removed,
-							Changed: langStats.Changed + removedPending,
-						}
-					}
-					removedPending = 0
-				case diffmatchpatch.DiffDelete:
-					removedPending = utf8.RuneCountInString(edit.Text)
-				}
-			}
-			if removedPending > 0 {
-				dd.Removed += removedPending
-				langStats := dd.Languages[lang]
-				dd.Languages[lang] = LineStats{
-					Added:   langStats.Added,
-					Removed: langStats.Removed + removedPending,
-					Changed: langStats.Changed,
-				}
-			}
+	lineStats := deps[items.DependencyLineStats].(map[object.ChangeEntry]items.LineStats)
+	for changeEntry, stats := range lineStats {
+		dd.Added += stats.Added
+		dd.Removed += stats.Removed
+		dd.Changed += stats.Changed
+		lang := langs[changeEntry.TreeEntry.Hash]
+		langStats := dd.Languages[lang]
+		dd.Languages[lang] = items.LineStats{
+			Added:   langStats.Added + stats.Added,
+			Removed: langStats.Removed + stats.Removed,
+			Changed: langStats.Changed + stats.Changed,
 		}
 	}
 	return nil, nil
@@ -300,10 +206,10 @@ func (devs *DevsAnalysis) Deserialize(pbmessage []byte) (interface{}, error) {
 			if dev == -1 {
 				dev = identity.AuthorMissing
 			}
-			languages := map[string]LineStats{}
+			languages := map[string]items.LineStats{}
 			rdd[int(dev)] = &DevDay{
 				Commits: int(stats.Commits),
-				LineStats: LineStats{
+				LineStats: items.LineStats{
 					Added:   int(stats.Stats.Added),
 					Removed: int(stats.Stats.Removed),
 					Changed: int(stats.Stats.Changed),
@@ -311,7 +217,7 @@ func (devs *DevsAnalysis) Deserialize(pbmessage []byte) (interface{}, error) {
 				Languages: languages,
 			}
 			for lang, ls := range stats.Languages {
-				languages[lang] = LineStats{
+				languages[lang] = items.LineStats{
 					Added:   int(ls.Added),
 					Removed: int(ls.Removed),
 					Changed: int(ls.Changed),
@@ -378,7 +284,7 @@ func (devs *DevsAnalysis) MergeResults(r1, r2 interface{}, c1, c2 *core.CommonAn
 			}
 			newstats, exists := newdd[newdev]
 			if !exists {
-				newstats = &DevDay{Languages: map[string]LineStats{}}
+				newstats = &DevDay{Languages: map[string]items.LineStats{}}
 				newdd[newdev] = newstats
 			}
 			newstats.Commits += stats.Commits
@@ -387,7 +293,7 @@ func (devs *DevsAnalysis) MergeResults(r1, r2 interface{}, c1, c2 *core.CommonAn
 			newstats.Changed += stats.Changed
 			for lang, ls := range stats.Languages {
 				prev := newstats.Languages[lang]
-				newstats.Languages[lang] = LineStats{
+				newstats.Languages[lang] = items.LineStats{
 					Added:   prev.Added + ls.Added,
 					Removed: prev.Removed + ls.Removed,
 					Changed: prev.Changed + ls.Changed,
@@ -408,7 +314,7 @@ func (devs *DevsAnalysis) MergeResults(r1, r2 interface{}, c1, c2 *core.CommonAn
 			}
 			newstats, exists := newdd[newdev]
 			if !exists {
-				newstats = &DevDay{Languages: map[string]LineStats{}}
+				newstats = &DevDay{Languages: map[string]items.LineStats{}}
 				newdd[newdev] = newstats
 			}
 			newstats.Commits += stats.Commits
@@ -417,7 +323,7 @@ func (devs *DevsAnalysis) MergeResults(r1, r2 interface{}, c1, c2 *core.CommonAn
 			newstats.Changed += stats.Changed
 			for lang, ls := range stats.Languages {
 				prev := newstats.Languages[lang]
-				newstats.Languages[lang] = LineStats{
+				newstats.Languages[lang] = items.LineStats{
 					Added:   prev.Added + ls.Added,
 					Removed: prev.Removed + ls.Removed,
 					Changed: prev.Changed + ls.Changed,

+ 70 - 56
leaves/devs_test.go

@@ -6,11 +6,11 @@ import (
 
 	"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"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
 	"gopkg.in/src-d/hercules.v8/internal/core"
 	"gopkg.in/src-d/hercules.v8/internal/pb"
-	"gopkg.in/src-d/hercules.v8/internal/plumbing"
+	items "gopkg.in/src-d/hercules.v8/internal/plumbing"
 	"gopkg.in/src-d/hercules.v8/internal/plumbing/identity"
 	"gopkg.in/src-d/hercules.v8/internal/test"
 	"gopkg.in/src-d/hercules.v8/internal/test/fixtures"
@@ -28,13 +28,12 @@ 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()), 6)
+	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.Requires()[5], plumbing.DependencyLanguages)
+	assert.Equal(t, d.Requires()[1], items.DependencyTreeChanges)
+	assert.Equal(t, d.Requires()[2], items.DependencyDay)
+	assert.Equal(t, d.Requires()[3], items.DependencyLanguages)
+	assert.Equal(t, d.Requires()[4], items.DependencyLineStats)
 	assert.Equal(t, d.Flag(), "devs")
 	assert.Len(t, d.ListConfigurationOptions(), 1)
 	assert.Equal(t, d.ListConfigurationOptions()[0].Name, ConfigDevsConsiderEmptyCommits)
@@ -78,23 +77,23 @@ func TestDevsConsumeFinalize(t *testing.T) {
 
 	// stage 1
 	deps[identity.DependencyAuthor] = 0
-	deps[plumbing.DependencyDay] = 0
-	cache := map[gitplumbing.Hash]*plumbing.CachedBlob{}
+	deps[items.DependencyDay] = 0
+	cache := map[plumbing.Hash]*items.CachedBlob{}
 	AddHash(t, cache, "291286b4ac41952cbd1389fda66420ec03c1a9fe")
 	AddHash(t, cache, "c29112dbd697ad9b401333b80c18a63951bc18d9")
 	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",
+	deps[items.DependencyBlobCache] = cache
+	deps[items.DependencyLanguages] = map[plumbing.Hash]string{
+		plumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"): "Go",
+		plumbing.NewHash("c29112dbd697ad9b401333b80c18a63951bc18d9"): "Go",
+		plumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1"): "Go",
+		plumbing.NewHash("dc248ba2b22048cc730c571a748e8ffcf7085ab9"): "Go",
 	}
 	changes := make(object.Changes, 3)
-	treeFrom, _ := test.Repository.TreeObject(gitplumbing.NewHash(
+	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
 		"a1eb2ea76eb7f9bfbde9b243861474421000eb96"))
-	treeTo, _ := test.Repository.TreeObject(gitplumbing.NewHash(
+	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
 		"994eac1cd07235bb9815e547a75c84265dea00f5"))
 	changes[0] = &object.Change{From: object.ChangeEntry{
 		Name: "analyser.go",
@@ -102,7 +101,7 @@ func TestDevsConsumeFinalize(t *testing.T) {
 		TreeEntry: object.TreeEntry{
 			Name: "analyser.go",
 			Mode: 0100644,
-			Hash: gitplumbing.NewHash("dc248ba2b22048cc730c571a748e8ffcf7085ab9"),
+			Hash: plumbing.NewHash("dc248ba2b22048cc730c571a748e8ffcf7085ab9"),
 		},
 	}, To: object.ChangeEntry{
 		Name: "analyser.go",
@@ -110,7 +109,7 @@ func TestDevsConsumeFinalize(t *testing.T) {
 		TreeEntry: object.TreeEntry{
 			Name: "analyser.go",
 			Mode: 0100644,
-			Hash: gitplumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1"),
+			Hash: plumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1"),
 		},
 	}}
 	changes[1] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
@@ -119,7 +118,7 @@ func TestDevsConsumeFinalize(t *testing.T) {
 		TreeEntry: object.TreeEntry{
 			Name: "cmd/hercules/main.go",
 			Mode: 0100644,
-			Hash: gitplumbing.NewHash("c29112dbd697ad9b401333b80c18a63951bc18d9"),
+			Hash: plumbing.NewHash("c29112dbd697ad9b401333b80c18a63951bc18d9"),
 		},
 	},
 	}
@@ -129,18 +128,23 @@ func TestDevsConsumeFinalize(t *testing.T) {
 		TreeEntry: object.TreeEntry{
 			Name: ".travis.yml",
 			Mode: 0100644,
-			Hash: gitplumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"),
+			Hash: plumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"),
 		},
 	},
 	}
-	deps[plumbing.DependencyTreeChanges] = changes
+	deps[items.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(
+	deps[items.DependencyFileDiff] = result[items.DependencyFileDiff]
+	deps[core.DependencyCommit], _ = test.Repository.CommitObject(plumbing.NewHash(
 		"cce947b98a050c6d356bc6ba95030254914027b1"))
 	deps[core.DependencyIsMerge] = false
+	lsc := &items.LinesStatsCalculator{}
+	lscres, err := lsc.Consume(deps)
+	assert.Nil(t, err)
+	deps[items.DependencyLineStats] = lscres[items.DependencyLineStats]
+
 	result, err = devs.Consume(deps)
 	assert.Nil(t, result)
 	assert.Nil(t, err)
@@ -157,6 +161,9 @@ func TestDevsConsumeFinalize(t *testing.T) {
 	assert.Equal(t, dev.Languages["Go"].Changed, 67)
 
 	deps[core.DependencyIsMerge] = true
+	lscres, err = lsc.Consume(deps)
+	assert.Nil(t, err)
+	deps[items.DependencyLineStats] = lscres[items.DependencyLineStats]
 	result, err = devs.Consume(deps)
 	assert.Nil(t, result)
 	assert.Nil(t, err)
@@ -174,6 +181,9 @@ func TestDevsConsumeFinalize(t *testing.T) {
 
 	deps[core.DependencyIsMerge] = false
 	deps[identity.DependencyAuthor] = 1
+	lscres, err = lsc.Consume(deps)
+	assert.Nil(t, err)
+	deps[items.DependencyLineStats] = lscres[items.DependencyLineStats]
 	result, err = devs.Consume(deps)
 	assert.Nil(t, result)
 	assert.Nil(t, err)
@@ -218,7 +228,7 @@ func TestDevsConsumeFinalize(t *testing.T) {
 	assert.Equal(t, dev.Languages["Go"].Removed, 9*2)
 	assert.Equal(t, dev.Languages["Go"].Changed, 67*2)
 
-	deps[plumbing.DependencyDay] = 1
+	deps[items.DependencyDay] = 1
 	result, err = devs.Consume(deps)
 	assert.Nil(t, result)
 	assert.Nil(t, err)
@@ -253,10 +263,14 @@ func TestDevsConsumeFinalize(t *testing.T) {
 	assert.Equal(t, dev.Languages["Go"].Changed, 67)
 }
 
+func ls(added, removed, changed int) items.LineStats {
+	return items.LineStats{Added: added, Removed: removed, Changed: changed}
+}
+
 func TestDevsFinalize(t *testing.T) {
 	devs := fixtureDevs()
 	devs.days[1] = map[int]*DevDay{}
-	devs.days[1][1] = &DevDay{10, LineStats{20, 30, 40}, nil}
+	devs.days[1][1] = &DevDay{10, ls(20, 30, 40), nil}
 	x := devs.Finalize().(DevsResult)
 	assert.Equal(t, x.Days, devs.days)
 	assert.Equal(t, x.reversedPeopleDict, devs.reversedPeopleDict)
@@ -271,12 +285,12 @@ func TestDevsFork(t *testing.T) {
 func TestDevsSerialize(t *testing.T) {
 	devs := fixtureDevs()
 	devs.days[1] = map[int]*DevDay{}
-	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[1][0] = &DevDay{10, ls(20, 30, 40), map[string]items.LineStats{"Go": ls(2, 3, 4)}}
+	devs.days[1][1] = &DevDay{1, ls(2, 3, 4), map[string]items.LineStats{"Go": ls(25, 35, 45)}}
 	devs.days[10] = map[int]*DevDay{}
-	devs.days[10][0] = &DevDay{11, LineStats{21, 31, 41}, map[string]LineStats{"": {12, 13, 14}}}
+	devs.days[10][0] = &DevDay{11, ls(21, 31, 41), map[string]items.LineStats{"": ls(12, 13, 14)}}
 	devs.days[10][identity.AuthorMissing] = &DevDay{
-		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {32, 33, 34}}}
+		100, ls(200, 300, 400), map[string]items.LineStats{"Go": ls(32, 33, 34)}}
 	res := devs.Finalize().(DevsResult)
 	buffer := &bytes.Buffer{}
 	err := devs.Serialize(res, false, buffer)
@@ -297,7 +311,7 @@ func TestDevsSerialize(t *testing.T) {
 	err = devs.Serialize(res, true, buffer)
 	assert.Nil(t, err)
 	msg := pb.DevsAnalysisResults{}
-	proto.Unmarshal(buffer.Bytes(), &msg)
+	assert.Nil(t, 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)
@@ -319,12 +333,12 @@ func TestDevsSerialize(t *testing.T) {
 func TestDevsDeserialize(t *testing.T) {
 	devs := fixtureDevs()
 	devs.days[1] = map[int]*DevDay{}
-	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[1][0] = &DevDay{10, ls(20, 30, 40), map[string]items.LineStats{"Go": ls(12, 13, 14)}}
+	devs.days[1][1] = &DevDay{1, ls(2, 3, 4), map[string]items.LineStats{"Go": ls(22, 23, 24)}}
 	devs.days[10] = map[int]*DevDay{}
-	devs.days[10][0] = &DevDay{11, LineStats{21, 31, 41}, map[string]LineStats{"Go": {32, 33, 34}}}
+	devs.days[10][0] = &DevDay{11, ls(21, 31, 41), map[string]items.LineStats{"Go": ls(32, 33, 34)}}
 	devs.days[10][identity.AuthorMissing] = &DevDay{
-		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {42, 43, 44}}}
+		100, ls(200, 300, 400), map[string]items.LineStats{"Go": ls(42, 43, 44)}}
 	res := devs.Finalize().(DevsResult)
 	buffer := &bytes.Buffer{}
 	err := devs.Serialize(res, true, buffer)
@@ -343,29 +357,29 @@ func TestDevsMergeResults(t *testing.T) {
 		reversedPeopleDict: people1[:],
 	}
 	r1.Days[1] = map[int]*DevDay{}
-	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[1][0] = &DevDay{10, ls(20, 30, 40), map[string]items.LineStats{"Go": ls(12, 13, 14)}}
+	r1.Days[1][1] = &DevDay{1, ls(2, 3, 4), map[string]items.LineStats{"Go": ls(22, 23, 24)}}
 	r1.Days[10] = map[int]*DevDay{}
-	r1.Days[10][0] = &DevDay{11, LineStats{21, 31, 41}, nil}
+	r1.Days[10][0] = &DevDay{11, ls(21, 31, 41), nil}
 	r1.Days[10][identity.AuthorMissing] = &DevDay{
-		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {32, 33, 34}}}
+		100, ls(200, 300, 400), map[string]items.LineStats{"Go": ls(32, 33, 34)}}
 	r1.Days[11] = map[int]*DevDay{}
-	r1.Days[11][1] = &DevDay{10, LineStats{20, 30, 40}, map[string]LineStats{"Go": {42, 43, 44}}}
+	r1.Days[11][1] = &DevDay{10, ls(20, 30, 40), map[string]items.LineStats{"Go": ls(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, 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[1][0] = &DevDay{10, ls(20, 30, 40), map[string]items.LineStats{"Go": ls(12, 13, 14)}}
+	r2.Days[1][1] = &DevDay{1, ls(2, 3, 4), map[string]items.LineStats{"Go": ls(22, 23, 24)}}
 	r2.Days[2] = map[int]*DevDay{}
-	r2.Days[2][0] = &DevDay{11, LineStats{21, 31, 41}, map[string]LineStats{"Go": {32, 33, 34}}}
+	r2.Days[2][0] = &DevDay{11, ls(21, 31, 41), map[string]items.LineStats{"Go": ls(32, 33, 34)}}
 	r2.Days[2][identity.AuthorMissing] = &DevDay{
-		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {42, 43, 44}}}
+		100, ls(200, 300, 400), map[string]items.LineStats{"Go": ls(42, 43, 44)}}
 	r2.Days[10] = map[int]*DevDay{}
-	r2.Days[10][0] = &DevDay{11, LineStats{21, 31, 41}, map[string]LineStats{"Go": {52, 53, 54}}}
+	r2.Days[10][0] = &DevDay{11, ls(21, 31, 41), map[string]items.LineStats{"Go": ls(52, 53, 54)}}
 	r2.Days[10][identity.AuthorMissing] = &DevDay{
-		100, LineStats{200, 300, 400}, map[string]LineStats{"Go": {62, 63, 64}}}
+		100, ls(200, 300, 400), map[string]items.LineStats{"Go": ls(62, 63, 64)}}
 
 	devs := fixtureDevs()
 	rm := devs.MergeResults(r1, r2, nil, nil).(DevsResult)
@@ -373,20 +387,20 @@ func TestDevsMergeResults(t *testing.T) {
 	assert.Equal(t, rm.reversedPeopleDict, peoplerm[:])
 	assert.Len(t, rm.Days, 4)
 	assert.Equal(t, rm.Days[11], map[int]*DevDay{
-		1: {10, LineStats{20, 30, 40}, map[string]LineStats{"Go": {42, 43, 44}}}})
+		1: {10, ls(20, 30, 40), map[string]items.LineStats{"Go": ls(42, 43, 44)}}})
 	assert.Equal(t, rm.Days[2], map[int]*DevDay{
-		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}}},
+		identity.AuthorMissing: {100, ls(200, 300, 400), map[string]items.LineStats{"Go": ls(42, 43, 44)}},
+		2:                      {11, ls(21, 31, 41), map[string]items.LineStats{"Go": ls(32, 33, 34)}},
 	})
 	assert.Equal(t, rm.Days[1], map[int]*DevDay{
-		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}}},
+		0: {11, ls(22, 33, 44), map[string]items.LineStats{"Go": ls(34, 36, 38)}},
+		1: {1, ls(2, 3, 4), map[string]items.LineStats{"Go": ls(22, 23, 24)}},
+		2: {10, ls(20, 30, 40), map[string]items.LineStats{"Go": ls(12, 13, 14)}},
 	})
 	assert.Equal(t, rm.Days[10], map[int]*DevDay{
-		0: {11, LineStats{21, 31, 41}, map[string]LineStats{}},
-		2: {11, LineStats{21, 31, 41}, map[string]LineStats{"Go": {52, 53, 54}}},
+		0: {11, ls(21, 31, 41), map[string]items.LineStats{}},
+		2: {11, ls(21, 31, 41), map[string]items.LineStats{"Go": ls(52, 53, 54)}},
 		identity.AuthorMissing: {
-			100 * 2, LineStats{200 * 2, 300 * 2, 400 * 2}, map[string]LineStats{"Go": {94, 96, 98}}},
+			100 * 2, ls(200*2, 300*2, 400*2), map[string]items.LineStats{"Go": ls(94, 96, 98)}},
 	})
 }

+ 114 - 39
leaves/file_history.go

@@ -3,6 +3,7 @@ package leaves
 import (
 	"fmt"
 	"io"
+	"log"
 	"sort"
 	"strings"
 
@@ -14,65 +15,77 @@ import (
 	"gopkg.in/src-d/hercules.v8/internal/core"
 	"gopkg.in/src-d/hercules.v8/internal/pb"
 	items "gopkg.in/src-d/hercules.v8/internal/plumbing"
+	"gopkg.in/src-d/hercules.v8/internal/plumbing/identity"
 )
 
-// FileHistory contains the intermediate state which is mutated by Consume(). It should implement
+// FileHistoryAnalysis contains the intermediate state which is mutated by Consume(). It should implement
 // LeafPipelineItem.
-type FileHistory struct {
+type FileHistoryAnalysis struct {
 	core.NoopMerger
 	core.OneShotMergeProcessor
-	files map[string][]plumbing.Hash
+	files      map[string]*FileHistory
+	lastCommit *object.Commit
 }
 
 // FileHistoryResult is returned by Finalize() and represents the analysis result.
 type FileHistoryResult struct {
-	Files map[string][]plumbing.Hash
+	Files map[string]FileHistory
+}
+
+// FileHistory is the gathered stats about a particular file.
+type FileHistory struct {
+	// Hashes is the list of commit hashes which changed this file.
+	Hashes []plumbing.Hash
+	// People is the mapping from developers to the number of lines they altered.
+	People map[int]items.LineStats
 }
 
 // Name of this PipelineItem. Uniquely identifies the type, used for mapping keys, etc.
-func (history *FileHistory) Name() string {
-	return "FileHistory"
+func (history *FileHistoryAnalysis) Name() string {
+	return "FileHistoryAnalysis"
 }
 
 // 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 (history *FileHistory) Provides() []string {
+func (history *FileHistoryAnalysis) 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 (history *FileHistory) Requires() []string {
-	arr := [...]string{items.DependencyTreeChanges}
+func (history *FileHistoryAnalysis) Requires() []string {
+	arr := [...]string{items.DependencyTreeChanges, items.DependencyLineStats, identity.DependencyAuthor}
 	return arr[:]
 }
 
 // ListConfigurationOptions returns the list of changeable public properties of this PipelineItem.
-func (history *FileHistory) ListConfigurationOptions() []core.ConfigurationOption {
+func (history *FileHistoryAnalysis) ListConfigurationOptions() []core.ConfigurationOption {
 	return []core.ConfigurationOption{}
 }
 
 // Flag for the command line switch which enables this analysis.
-func (history *FileHistory) Flag() string {
+func (history *FileHistoryAnalysis) Flag() string {
 	return "file-history"
 }
 
 // Description returns the text which explains what the analysis is doing.
-func (history *FileHistory) Description() string {
-	return "Each file path is mapped to the list of commits which involve that file."
+func (history *FileHistoryAnalysis) Description() string {
+	return "Each file path is mapped to the list of commits which touch that file and the mapping " +
+		"from involved developers to the corresponding line statistics: how many lines were added, " +
+		"removed and changed throughout the whole history."
 }
 
 // Configure sets the properties previously published by ListConfigurationOptions().
-func (history *FileHistory) Configure(facts map[string]interface{}) error {
+func (history *FileHistoryAnalysis) 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 (history *FileHistory) Initialize(repository *git.Repository) error {
-	history.files = map[string][]plumbing.Hash{}
+func (history *FileHistoryAnalysis) Initialize(repository *git.Repository) error {
+	history.files = map[string]*FileHistory{}
 	history.OneShotMergeProcessor.Initialize()
 	return nil
 }
@@ -82,46 +95,91 @@ func (history *FileHistory) Initialize(repository *git.Repository) error {
 // 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 (history *FileHistory) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
-	if !history.ShouldConsumeCommit(deps) {
+func (history *FileHistoryAnalysis) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
+	if deps[core.DependencyIsMerge].(bool) {
+		// we ignore merge commits
+		// TODO(vmarkovtsev): handle them better
 		return nil, nil
 	}
-	commit := deps[core.DependencyCommit].(*object.Commit).Hash
+	history.lastCommit = deps[core.DependencyCommit].(*object.Commit)
+	commit := history.lastCommit.Hash
 	changes := deps[items.DependencyTreeChanges].(object.Changes)
 	for _, change := range changes {
 		action, _ := change.Action()
+		var fh *FileHistory
+		if action != merkletrie.Delete {
+			fh = history.files[change.To.Name]
+		} else {
+			fh = history.files[change.From.Name]
+		}
+		if fh == nil {
+			fh = &FileHistory{}
+			history.files[change.To.Name] = fh
+		}
 		switch action {
 		case merkletrie.Insert:
-			hashes := make([]plumbing.Hash, 1)
-			hashes[0] = commit
-			history.files[change.To.Name] = hashes
+			fh.Hashes = []plumbing.Hash{commit}
 		case merkletrie.Delete:
-			delete(history.files, change.From.Name)
+			fh.Hashes = append(fh.Hashes, commit)
 		case merkletrie.Modify:
-			hashes := history.files[change.From.Name]
+			hashes := history.files[change.From.Name].Hashes
 			if change.From.Name != change.To.Name {
 				delete(history.files, change.From.Name)
 			}
 			hashes = append(hashes, commit)
-			history.files[change.To.Name] = hashes
+			fh.Hashes = hashes
+		}
+	}
+	lineStats := deps[items.DependencyLineStats].(map[object.ChangeEntry]items.LineStats)
+	author := deps[identity.DependencyAuthor].(int)
+	for changeEntry, stats := range lineStats {
+		file := history.files[changeEntry.Name]
+		if file == nil {
+			file = &FileHistory{}
+			history.files[changeEntry.Name] = file
+		}
+		people := file.People
+		if people == nil {
+			people = map[int]items.LineStats{}
+			file.People = people
+		}
+		oldStats := people[author]
+		people[author] = items.LineStats{
+			Added:   oldStats.Added + stats.Added,
+			Removed: oldStats.Removed + stats.Removed,
+			Changed: oldStats.Changed + stats.Changed,
 		}
 	}
 	return nil, nil
 }
 
 // Finalize returns the result of the analysis. Further Consume() calls are not expected.
-func (history *FileHistory) Finalize() interface{} {
-	return FileHistoryResult{Files: history.files}
+func (history *FileHistoryAnalysis) Finalize() interface{} {
+	files := map[string]FileHistory{}
+	fileIter, err := history.lastCommit.Files()
+	if err != nil {
+		log.Panicf("Failed to iterate files of %s", history.lastCommit.Hash.String())
+	}
+	err = fileIter.ForEach(func(file *object.File) error {
+		if fh := history.files[file.Name]; fh != nil {
+			files[file.Name] = *fh
+		}
+		return nil
+	})
+	if err != nil {
+		log.Panicf("Failed to iterate files of %s", history.lastCommit.Hash.String())
+	}
+	return FileHistoryResult{Files: files}
 }
 
 // Fork clones this PipelineItem.
-func (history *FileHistory) Fork(n int) []core.PipelineItem {
+func (history *FileHistoryAnalysis) Fork(n int) []core.PipelineItem {
 	return core.ForkSamePipelineItem(history, 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 (history *FileHistory) Serialize(result interface{}, binary bool, writer io.Writer) error {
+func (history *FileHistoryAnalysis) Serialize(result interface{}, binary bool, writer io.Writer) error {
 	historyResult := result.(FileHistoryResult)
 	if binary {
 		return history.serializeBinary(&historyResult, writer)
@@ -130,7 +188,7 @@ func (history *FileHistory) Serialize(result interface{}, binary bool, writer io
 	return nil
 }
 
-func (history *FileHistory) serializeText(result *FileHistoryResult, writer io.Writer) {
+func (history *FileHistoryAnalysis) serializeText(result *FileHistoryResult, writer io.Writer) {
 	keys := make([]string, len(result.Files))
 	i := 0
 	for key := range result.Files {
@@ -139,27 +197,44 @@ func (history *FileHistory) serializeText(result *FileHistoryResult, writer io.W
 	}
 	sort.Strings(keys)
 	for _, key := range keys {
-		hashes := result.Files[key]
+		fmt.Fprintf(writer, "  - %s:\n", key)
+		file := result.Files[key]
+		hashes := file.Hashes
 		strhashes := make([]string, len(hashes))
 		for i, hash := range hashes {
 			strhashes[i] = "\"" + hash.String() + "\""
 		}
-		fmt.Fprintf(writer, "  - %s: [%s]\n", key, strings.Join(strhashes, ","))
+		sort.Strings(strhashes)
+		fmt.Fprintf(writer, "    commits: [%s]\n", strings.Join(strhashes, ","))
+		strpeople := make([]string, 0, len(file.People))
+		for key, val := range file.People {
+			strpeople = append(strpeople, fmt.Sprintf("%d:[%d,%d,%d]", key, val.Added, val.Removed, val.Changed))
+		}
+		sort.Strings(strpeople)
+		fmt.Fprintf(writer, "    people: {%s}\n", strings.Join(strpeople, ","))
 	}
 }
 
-func (history *FileHistory) serializeBinary(result *FileHistoryResult, writer io.Writer) error {
+func (history *FileHistoryAnalysis) serializeBinary(result *FileHistoryResult, writer io.Writer) error {
 	message := pb.FileHistoryResultMessage{
 		Files: map[string]*pb.FileHistory{},
 	}
 	for key, vals := range result.Files {
-		hashes := &pb.FileHistory{
-			Commits: make([]string, len(vals)),
+		fh := &pb.FileHistory{
+			Commits:            make([]string, len(vals.Hashes)),
+			ChangesByDeveloper: map[int32]*pb.LineStats{},
 		}
-		for i, hash := range vals {
-			hashes.Commits[i] = hash.String()
+		for i, hash := range vals.Hashes {
+			fh.Commits[i] = hash.String()
+		}
+		for key, val := range vals.People {
+			fh.ChangesByDeveloper[int32(key)] = &pb.LineStats{
+				Added:   int32(val.Added),
+				Removed: int32(val.Removed),
+				Changed: int32(val.Changed),
+			}
 		}
-		message.Files[key] = hashes
+		message.Files[key] = fh
 	}
 	serialized, err := proto.Marshal(&message)
 	if err != nil {
@@ -170,5 +245,5 @@ func (history *FileHistory) serializeBinary(result *FileHistoryResult, writer io
 }
 
 func init() {
-	core.Registry.Register(&FileHistory{})
+	core.Registry.Register(&FileHistoryAnalysis{})
 }

+ 116 - 90
leaves/file_history_test.go

@@ -11,33 +11,37 @@ import (
 	"gopkg.in/src-d/hercules.v8/internal/core"
 	"gopkg.in/src-d/hercules.v8/internal/pb"
 	items "gopkg.in/src-d/hercules.v8/internal/plumbing"
+	"gopkg.in/src-d/hercules.v8/internal/plumbing/identity"
 	"gopkg.in/src-d/hercules.v8/internal/test"
+	"gopkg.in/src-d/hercules.v8/internal/test/fixtures"
 )
 
-func fixtureFileHistory() *FileHistory {
-	fh := FileHistory{}
+func fixtureFileHistory() *FileHistoryAnalysis {
+	fh := FileHistoryAnalysis{}
 	fh.Initialize(test.Repository)
 	return &fh
 }
 
 func TestFileHistoryMeta(t *testing.T) {
 	fh := fixtureFileHistory()
-	assert.Equal(t, fh.Name(), "FileHistory")
+	assert.Equal(t, fh.Name(), "FileHistoryAnalysis")
 	assert.Equal(t, len(fh.Provides()), 0)
-	assert.Equal(t, len(fh.Requires()), 1)
+	assert.Equal(t, len(fh.Requires()), 3)
 	assert.Equal(t, fh.Requires()[0], items.DependencyTreeChanges)
+	assert.Equal(t, fh.Requires()[1], items.DependencyLineStats)
+	assert.Equal(t, fh.Requires()[2], identity.DependencyAuthor)
 	assert.Len(t, fh.ListConfigurationOptions(), 0)
-	fh.Configure(nil)
+	assert.Nil(t, fh.Configure(nil))
 }
 
 func TestFileHistoryRegistration(t *testing.T) {
-	summoned := core.Registry.Summon((&FileHistory{}).Name())
+	summoned := core.Registry.Summon((&FileHistoryAnalysis{}).Name())
 	assert.Len(t, summoned, 1)
-	assert.Equal(t, summoned[0].Name(), "FileHistory")
+	assert.Equal(t, summoned[0].Name(), "FileHistoryAnalysis")
 	leaves := core.Registry.GetLeaves()
 	matched := false
 	for _, tp := range leaves {
-		if tp.Flag() == (&FileHistory{}).Flag() {
+		if tp.Flag() == (&FileHistoryAnalysis{}).Flag() {
 			matched = true
 			break
 		}
@@ -46,8 +50,95 @@ func TestFileHistoryRegistration(t *testing.T) {
 }
 
 func TestFileHistoryConsume(t *testing.T) {
+	fh, deps := bakeFileHistoryForSerialization(t)
+	validate := func() {
+		assert.Len(t, fh.files, 3)
+		assert.Equal(t, fh.files["cmd/hercules/main.go"].People,
+			map[int]items.LineStats{1: ls(0, 207, 0)})
+		assert.Equal(t, fh.files[".travis.yml"].People, map[int]items.LineStats{1: ls(12, 0, 0)})
+		assert.Equal(t, fh.files["analyser.go"].People, map[int]items.LineStats{1: ls(628, 9, 67)})
+		assert.Len(t, fh.files["analyser.go"].Hashes, 2)
+		assert.Equal(t, fh.files["analyser.go"].Hashes[0], plumbing.NewHash(
+			"ffffffffffffffffffffffffffffffffffffffff"))
+		assert.Equal(t, fh.files["analyser.go"].Hashes[1], plumbing.NewHash(
+			"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
+		assert.Len(t, fh.files[".travis.yml"].Hashes, 1)
+		assert.Equal(t, fh.files[".travis.yml"].Hashes[0], plumbing.NewHash(
+			"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
+		assert.Len(t, fh.files["cmd/hercules/main.go"].Hashes, 2)
+		assert.Equal(t, fh.files["cmd/hercules/main.go"].Hashes[0], plumbing.NewHash(
+			"0000000000000000000000000000000000000000"))
+		assert.Equal(t, fh.files["cmd/hercules/main.go"].Hashes[1], plumbing.NewHash(
+			"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
+	}
+	validate()
+	res := fh.Finalize().(FileHistoryResult)
+	assert.Equal(t, 2, len(res.Files))
+	for key, val := range res.Files {
+		assert.Equal(t, val, *fh.files[key])
+	}
+	deps[core.DependencyIsMerge] = true
+	cres, err := fh.Consume(deps)
+	assert.Nil(t, cres)
+	assert.Nil(t, err)
+	validate()
+	fh.lastCommit = &object.Commit{}
+	assert.Panics(t, func() { fh.Finalize() })
+}
+
+func TestFileHistoryFork(t *testing.T) {
+	fh1 := fixtureFileHistory()
+	clones := fh1.Fork(1)
+	assert.Len(t, clones, 1)
+	fh2 := clones[0].(*FileHistoryAnalysis)
+	assert.True(t, fh1 == fh2)
+	fh1.Merge([]core.PipelineItem{fh2})
+}
+
+func TestFileHistorySerializeText(t *testing.T) {
+	fh, _ := bakeFileHistoryForSerialization(t)
+	res := fh.Finalize().(FileHistoryResult)
+	buffer := &bytes.Buffer{}
+	assert.Nil(t, fh.Serialize(res, false, buffer))
+	assert.Equal(t, buffer.String(), `  - .travis.yml:
+    commits: ["2b1ed978194a94edeabbca6de7ff3b5771d4d665"]
+    people: {1:[12,0,0]}
+  - cmd/hercules/main.go:
+    commits: ["0000000000000000000000000000000000000000","2b1ed978194a94edeabbca6de7ff3b5771d4d665"]
+    people: {1:[0,207,0]}
+`)
+}
+
+func TestFileHistorySerializeBinary(t *testing.T) {
+	fh, _ := bakeFileHistoryForSerialization(t)
+	res := fh.Finalize().(FileHistoryResult)
+	buffer := &bytes.Buffer{}
+	assert.Nil(t, fh.Serialize(res, true, buffer))
+	msg := pb.FileHistoryResultMessage{}
+	assert.Nil(t, proto.Unmarshal(buffer.Bytes(), &msg))
+	assert.Len(t, msg.Files, 2)
+	assert.Len(t, msg.Files[".travis.yml"].Commits, 1)
+	assert.Equal(t, msg.Files[".travis.yml"].Commits[0], "2b1ed978194a94edeabbca6de7ff3b5771d4d665")
+	assert.Len(t, msg.Files["cmd/hercules/main.go"].Commits, 2)
+	assert.Equal(t, msg.Files["cmd/hercules/main.go"].Commits[0],
+		"0000000000000000000000000000000000000000")
+	assert.Equal(t, msg.Files["cmd/hercules/main.go"].Commits[1],
+		"2b1ed978194a94edeabbca6de7ff3b5771d4d665")
+	assert.Equal(t, msg.Files[".travis.yml"].ChangesByDeveloper,
+		map[int32]*pb.LineStats{1: {Added: 12, Removed: 0, Changed: 0}})
+	assert.Equal(t, msg.Files["cmd/hercules/main.go"].ChangesByDeveloper,
+		map[int32]*pb.LineStats{1: {Added: 0, Removed: 207, Changed: 0}})
+}
+
+func bakeFileHistoryForSerialization(t *testing.T) (*FileHistoryAnalysis, map[string]interface{}) {
 	fh := fixtureFileHistory()
 	deps := map[string]interface{}{}
+	cache := map[plumbing.Hash]*items.CachedBlob{}
+	AddHash(t, cache, "291286b4ac41952cbd1389fda66420ec03c1a9fe")
+	AddHash(t, cache, "c29112dbd697ad9b401333b80c18a63951bc18d9")
+	AddHash(t, cache, "baa64828831d174f40140e4b3cfa77d1e917a2c1")
+	AddHash(t, cache, "dc248ba2b22048cc730c571a748e8ffcf7085ab9")
+	deps[items.DependencyBlobCache] = cache
 	changes := make(object.Changes, 3)
 	treeFrom, _ := test.Repository.TreeObject(plumbing.NewHash(
 		"a1eb2ea76eb7f9bfbde9b243861474421000eb96"))
@@ -94,88 +185,23 @@ func TestFileHistoryConsume(t *testing.T) {
 	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
 		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
 	deps[core.DependencyCommit] = commit
-	fh.files["cmd/hercules/main.go"] = []plumbing.Hash{plumbing.NewHash(
-		"0000000000000000000000000000000000000000")}
-	fh.files["analyser.go"] = []plumbing.Hash{plumbing.NewHash(
-		"ffffffffffffffffffffffffffffffffffffffff")}
-	fh.Consume(deps)
-	assert.Len(t, fh.files, 2)
-	assert.Nil(t, fh.files["cmd/hercules/main.go"])
-	assert.Len(t, fh.files["analyser.go"], 2)
-	assert.Equal(t, fh.files["analyser.go"][0], plumbing.NewHash(
-		"ffffffffffffffffffffffffffffffffffffffff"))
-	assert.Equal(t, fh.files["analyser.go"][1], plumbing.NewHash(
-		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
-	assert.Len(t, fh.files[".travis.yml"], 1)
-	assert.Equal(t, fh.files[".travis.yml"][0], plumbing.NewHash(
-		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
-	res := fh.Finalize().(FileHistoryResult)
-	assert.Equal(t, fh.files, res.Files)
-}
-
-func TestFileHistoryFork(t *testing.T) {
-	fh1 := fixtureFileHistory()
-	clones := fh1.Fork(1)
-	assert.Len(t, clones, 1)
-	fh2 := clones[0].(*FileHistory)
-	assert.True(t, fh1 == fh2)
-	fh1.Merge([]core.PipelineItem{fh2})
-}
+	deps[core.DependencyIsMerge] = false
+	deps[identity.DependencyAuthor] = 1
+	fd := fixtures.FileDiff()
+	result, err := fd.Consume(deps)
+	assert.Nil(t, err)
+	deps[items.DependencyFileDiff] = result[items.DependencyFileDiff]
 
-func TestFileHistorySerializeText(t *testing.T) {
-	fh := fixtureFileHistory()
-	deps := map[string]interface{}{}
-	changes := make(object.Changes, 1)
-	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
-		"994eac1cd07235bb9815e547a75c84265dea00f5"))
-	changes[0] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
-		Name: ".travis.yml",
-		Tree: treeTo,
-		TreeEntry: object.TreeEntry{
-			Name: ".travis.yml",
-			Mode: 0100644,
-			Hash: plumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"),
-		},
-	},
-	}
-	deps[items.DependencyTreeChanges] = changes
-	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
-		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
-	deps[core.DependencyCommit] = commit
-	fh.Consume(deps)
-	res := fh.Finalize().(FileHistoryResult)
-	buffer := &bytes.Buffer{}
-	fh.Serialize(res, false, buffer)
-	assert.Equal(t, buffer.String(), "  - .travis.yml: [\"2b1ed978194a94edeabbca6de7ff3b5771d4d665\"]\n")
-}
+	lineStats, err := (&items.LinesStatsCalculator{}).Consume(deps)
+	assert.Nil(t, err)
+	deps[items.DependencyLineStats] = lineStats[items.DependencyLineStats]
 
-func TestFileHistorySerializeBinary(t *testing.T) {
-	fh := fixtureFileHistory()
-	deps := map[string]interface{}{}
-	changes := make(object.Changes, 1)
-	treeTo, _ := test.Repository.TreeObject(plumbing.NewHash(
-		"994eac1cd07235bb9815e547a75c84265dea00f5"))
-	changes[0] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
-		Name: ".travis.yml",
-		Tree: treeTo,
-		TreeEntry: object.TreeEntry{
-			Name: ".travis.yml",
-			Mode: 0100644,
-			Hash: plumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"),
-		},
-	},
-	}
-	deps[items.DependencyTreeChanges] = changes
-	commit, _ := test.Repository.CommitObject(plumbing.NewHash(
-		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
-	deps[core.DependencyCommit] = commit
-	fh.Consume(deps)
-	res := fh.Finalize().(FileHistoryResult)
-	buffer := &bytes.Buffer{}
-	fh.Serialize(res, true, buffer)
-	msg := pb.FileHistoryResultMessage{}
-	proto.Unmarshal(buffer.Bytes(), &msg)
-	assert.Len(t, msg.Files, 1)
-	assert.Len(t, msg.Files[".travis.yml"].Commits, 1)
-	assert.Equal(t, msg.Files[".travis.yml"].Commits[0], "2b1ed978194a94edeabbca6de7ff3b5771d4d665")
+	fh.files["cmd/hercules/main.go"] = &FileHistory{Hashes: []plumbing.Hash{plumbing.NewHash(
+		"0000000000000000000000000000000000000000")}}
+	fh.files["analyser.go"] = &FileHistory{Hashes: []plumbing.Hash{plumbing.NewHash(
+		"ffffffffffffffffffffffffffffffffffffffff")}}
+	cres, err := fh.Consume(deps)
+	assert.Nil(t, cres)
+	assert.Nil(t, err)
+	return fh, deps
 }