analyser.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. package hercules
  2. import (
  3. "bufio"
  4. "bytes"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "os"
  9. "time"
  10. "unicode/utf8"
  11. "github.com/sergi/go-diff/diffmatchpatch"
  12. "gopkg.in/src-d/go-git.v4"
  13. "gopkg.in/src-d/go-git.v4/plumbing/difftree"
  14. "gopkg.in/src-d/go-git.v4/plumbing/object"
  15. )
  16. type Analyser struct {
  17. Repository *git.Repository
  18. Granularity int
  19. Sampling int
  20. OnProgress func(int, int)
  21. }
  22. func checkClose(c io.Closer) {
  23. if err := c.Close(); err != nil {
  24. panic(err)
  25. }
  26. }
  27. func loc(file *object.Blob) (int, error) {
  28. reader, err := file.Reader()
  29. if err != nil {
  30. panic(err)
  31. }
  32. defer checkClose(reader)
  33. scanner := bufio.NewScanner(reader)
  34. counter := 0
  35. for scanner.Scan() {
  36. if !utf8.Valid(scanner.Bytes()) {
  37. return -1, errors.New("binary")
  38. }
  39. counter++
  40. }
  41. return counter, nil
  42. }
  43. func str(file *object.Blob) string {
  44. reader, err := file.Reader()
  45. if err != nil {
  46. panic(err)
  47. }
  48. defer checkClose(reader)
  49. buf := new(bytes.Buffer)
  50. buf.ReadFrom(reader)
  51. return buf.String()
  52. }
  53. func (analyser *Analyser) handleInsertion(
  54. change *difftree.Change, day int, status map[int]int64, files map[string]*File) {
  55. blob, err := analyser.Repository.Blob(change.To.TreeEntry.Hash)
  56. if err != nil {
  57. panic(err)
  58. }
  59. lines, err := loc(blob)
  60. if err != nil {
  61. return
  62. }
  63. name := change.To.Name
  64. file, exists := files[name]
  65. if exists {
  66. panic(fmt.Sprintf("file %s already exists", name))
  67. }
  68. file = NewFile(day, lines, status)
  69. files[name] = file
  70. }
  71. func (analyser *Analyser) handleDeletion(
  72. change *difftree.Change, day int, status map[int]int64, files map[string]*File) {
  73. blob, err := analyser.Repository.Blob(change.From.TreeEntry.Hash)
  74. if err != nil {
  75. panic(err)
  76. }
  77. lines, err := loc(blob)
  78. if err != nil {
  79. return
  80. }
  81. name := change.From.Name
  82. file := files[name]
  83. file.Update(day, 0, 0, lines)
  84. delete(files, name)
  85. }
  86. func (analyser *Analyser) handleModification(
  87. change *difftree.Change, day int, status map[int]int64, files map[string]*File) {
  88. blob_from, err := analyser.Repository.Blob(change.From.TreeEntry.Hash)
  89. if err != nil {
  90. panic(err)
  91. }
  92. blob_to, err := analyser.Repository.Blob(change.To.TreeEntry.Hash)
  93. if err != nil {
  94. panic(err)
  95. }
  96. // we are not validating UTF-8 here because for example
  97. // git/git 4f7770c87ce3c302e1639a7737a6d2531fe4b160 fetch-pack.c is invalid UTF-8
  98. str_from := str(blob_from)
  99. str_to := str(blob_to)
  100. file, exists := files[change.From.Name]
  101. if !exists {
  102. analyser.handleInsertion(change, day, status, files)
  103. return
  104. }
  105. // possible rename
  106. if change.To.Name != change.From.Name {
  107. analyser.handleRename(change.From.Name, change.To.Name, files)
  108. }
  109. dmp := diffmatchpatch.New()
  110. src, dst, _ := dmp.DiffLinesToRunes(str_from, str_to)
  111. if file.Len() != len(src) {
  112. panic(fmt.Sprintf("%s: internal integrity error src %d != %d",
  113. change.To.Name, len(src), file.Len()))
  114. }
  115. diffs := dmp.DiffMainRunes(src, dst, false)
  116. // we do not call RunesToDiffLines so the number of lines equals
  117. // to the rune count
  118. position := 0
  119. pending := diffmatchpatch.Diff{Text: ""}
  120. apply := func(edit diffmatchpatch.Diff) {
  121. length := utf8.RuneCountInString(edit.Text)
  122. if edit.Type == diffmatchpatch.DiffInsert {
  123. file.Update(day, position, length, 0)
  124. position += length
  125. } else {
  126. file.Update(day, position, 0, length)
  127. }
  128. }
  129. for _, edit := range diffs {
  130. length := utf8.RuneCountInString(edit.Text)
  131. func() {
  132. defer func() {
  133. r := recover()
  134. if r != nil {
  135. fmt.Fprintf(os.Stderr, "%s: internal diff error\n", change.To.Name)
  136. fmt.Fprint(os.Stderr, "====BEFORE====\n")
  137. fmt.Fprint(os.Stderr, str_from)
  138. fmt.Fprint(os.Stderr, "====AFTER====\n")
  139. fmt.Fprint(os.Stderr, str_to)
  140. fmt.Fprint(os.Stderr, "====END====\n")
  141. panic(r)
  142. }
  143. }()
  144. switch edit.Type {
  145. case diffmatchpatch.DiffEqual:
  146. if pending.Text != "" {
  147. apply(pending)
  148. pending.Text = ""
  149. }
  150. position += length
  151. case diffmatchpatch.DiffInsert:
  152. if pending.Text != "" {
  153. if pending.Type == diffmatchpatch.DiffInsert {
  154. panic("DiffInsert may not appear after DiffInsert")
  155. }
  156. file.Update(day, position, length, utf8.RuneCountInString(pending.Text))
  157. position += length
  158. pending.Text = ""
  159. } else {
  160. pending = edit
  161. }
  162. case diffmatchpatch.DiffDelete:
  163. if pending.Text != "" {
  164. panic("DiffDelete may not appear after DiffInsert/DiffDelete")
  165. }
  166. pending = edit
  167. default:
  168. panic(fmt.Sprintf("diff operation is not supported: %d", edit.Type))
  169. }
  170. }()
  171. }
  172. if pending.Text != "" {
  173. apply(pending)
  174. pending.Text = ""
  175. }
  176. if file.Len() != len(dst) {
  177. panic(fmt.Sprintf("%s: internal integrity error dst %d != %d",
  178. change.To.Name, len(dst), file.Len()))
  179. }
  180. }
  181. func (analyser *Analyser) handleRename(from, to string, files map[string]*File) {
  182. file, exists := files[from]
  183. if !exists {
  184. panic(fmt.Sprintf("file %s does not exist", from))
  185. }
  186. files[to] = file
  187. delete(files, from)
  188. }
  189. func (analyser *Analyser) Commits() []*object.Commit {
  190. result := []*object.Commit{}
  191. repository := analyser.Repository
  192. head, err := repository.Head()
  193. if err != nil {
  194. panic(err)
  195. }
  196. commit, err := repository.Commit(head.Hash())
  197. if err != nil {
  198. panic(err)
  199. }
  200. result = append(result, commit)
  201. for ; err != io.EOF; commit, err = commit.Parents().Next() {
  202. if err != nil {
  203. panic(err)
  204. }
  205. result = append(result, commit)
  206. }
  207. // reverse the order
  208. for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
  209. result[i], result[j] = result[j], result[i]
  210. }
  211. return result
  212. }
  213. func (analyser *Analyser) groupStatus(status map[int]int64, day int) []int64 {
  214. granularity := analyser.Granularity
  215. if granularity == 0 {
  216. granularity = 1
  217. }
  218. day++
  219. adjust := 0
  220. if day%granularity < granularity-1 {
  221. adjust = 1
  222. }
  223. result := make([]int64, day/granularity+adjust)
  224. var group int64
  225. for i := 0; i < day; i++ {
  226. group += status[i]
  227. if i%granularity == (granularity - 1) {
  228. result[i/granularity] = group
  229. group = 0
  230. }
  231. }
  232. if day%granularity < granularity-1 {
  233. result[len(result)-1] = group
  234. }
  235. return result
  236. }
  237. func (analyser *Analyser) Analyse(commits []*object.Commit) [][]int64 {
  238. sampling := analyser.Sampling
  239. if sampling == 0 {
  240. sampling = 1
  241. }
  242. onProgress := analyser.OnProgress
  243. if onProgress == nil {
  244. onProgress = func(int, int) {}
  245. }
  246. // current daily alive number of lines; key is the number of days from the
  247. // beginning of the history
  248. status := map[int]int64{}
  249. // weekly snapshots of status
  250. statuses := [][]int64{}
  251. // mapping <file path> -> hercules.File
  252. files := map[string]*File{}
  253. var day0 time.Time // will be initialized in the first iteration
  254. var prev_tree *object.Tree = nil
  255. prev_day := 0
  256. for index, commit := range commits {
  257. onProgress(index, len(commits))
  258. tree, err := commit.Tree()
  259. if err != nil {
  260. panic(err)
  261. }
  262. if index == 0 {
  263. // first iteration - initialize the file objects from the tree
  264. day0 = commit.Author.When
  265. func() {
  266. file_iter := tree.Files()
  267. defer file_iter.Close()
  268. for {
  269. file, err := file_iter.Next()
  270. if err != nil {
  271. if err == io.EOF {
  272. break
  273. }
  274. panic(err)
  275. }
  276. lines, err := loc(&file.Blob)
  277. if err == nil {
  278. files[file.Name] = NewFile(0, lines, status)
  279. }
  280. }
  281. }()
  282. } else {
  283. day := int(commit.Author.When.Sub(day0).Hours() / 24)
  284. delta := (day / sampling) - (prev_day / sampling)
  285. if delta > 0 {
  286. prev_day = day
  287. gs := analyser.groupStatus(status, day)
  288. for i := 0; i < delta; i++ {
  289. statuses = append(statuses, gs)
  290. }
  291. }
  292. tree_diff, err := difftree.DiffTree(prev_tree, tree)
  293. if err != nil {
  294. panic(err)
  295. }
  296. for _, change := range tree_diff {
  297. switch change.Action {
  298. case difftree.Insert:
  299. analyser.handleInsertion(change, day, status, files)
  300. case difftree.Delete:
  301. analyser.handleDeletion(change, day, status, files)
  302. case difftree.Modify:
  303. func() {
  304. defer func() {
  305. r := recover()
  306. if r != nil {
  307. fmt.Fprintf(os.Stderr, "%s: modification error\n", commit.Hash.String())
  308. panic(r)
  309. }
  310. }()
  311. analyser.handleModification(change, day, status, files)
  312. }()
  313. default:
  314. panic(fmt.Sprintf("unsupported action: %d", change.Action))
  315. }
  316. }
  317. }
  318. prev_tree = tree
  319. }
  320. return statuses
  321. }