analyser.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. package hercules
  2. import (
  3. "bufio"
  4. "bytes"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "os"
  9. "sort"
  10. "time"
  11. "unicode/utf8"
  12. "github.com/sergi/go-diff/diffmatchpatch"
  13. "gopkg.in/src-d/go-git.v4"
  14. "gopkg.in/src-d/go-git.v4/plumbing"
  15. "gopkg.in/src-d/go-git.v4/plumbing/object"
  16. "gopkg.in/src-d/go-git.v4/utils/merkletrie"
  17. )
  18. type Analyser struct {
  19. Repository *git.Repository
  20. Granularity int
  21. Sampling int
  22. SimilarityThreshold int
  23. Debug bool
  24. OnProgress func(int, int)
  25. }
  26. func checkClose(c io.Closer) {
  27. if err := c.Close(); err != nil {
  28. panic(err)
  29. }
  30. }
  31. func loc(file *object.Blob) (int, error) {
  32. reader, err := file.Reader()
  33. if err != nil {
  34. panic(err)
  35. }
  36. defer checkClose(reader)
  37. scanner := bufio.NewScanner(reader)
  38. counter := 0
  39. for scanner.Scan() {
  40. if !utf8.Valid(scanner.Bytes()) {
  41. return -1, errors.New("binary")
  42. }
  43. counter++
  44. }
  45. return counter, nil
  46. }
  47. func str(file *object.Blob) string {
  48. reader, err := file.Reader()
  49. if err != nil {
  50. panic(err)
  51. }
  52. defer checkClose(reader)
  53. buf := new(bytes.Buffer)
  54. buf.ReadFrom(reader)
  55. return buf.String()
  56. }
  57. func (analyser *Analyser) handleInsertion(
  58. change *object.Change, day int, status map[int]int64, files map[string]*File,
  59. cache *map[plumbing.Hash]*object.Blob) {
  60. blob := (*cache)[change.To.TreeEntry.Hash]
  61. lines, err := loc(blob)
  62. if err != nil {
  63. return
  64. }
  65. name := change.To.Name
  66. file, exists := files[name]
  67. if exists {
  68. panic(fmt.Sprintf("file %s already exists", name))
  69. }
  70. file = NewFile(day, lines, status)
  71. files[name] = file
  72. }
  73. func (analyser *Analyser) handleDeletion(
  74. change *object.Change, day int, status map[int]int64, files map[string]*File,
  75. cache *map[plumbing.Hash]*object.Blob) {
  76. blob := (*cache)[change.From.TreeEntry.Hash]
  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 *object.Change, day int, status map[int]int64, files map[string]*File,
  88. cache *map[plumbing.Hash]*object.Blob) {
  89. blob_from := (*cache)[change.From.TreeEntry.Hash]
  90. blob_to := (*cache)[change.To.TreeEntry.Hash]
  91. // we are not validating UTF-8 here because for example
  92. // git/git 4f7770c87ce3c302e1639a7737a6d2531fe4b160 fetch-pack.c is invalid UTF-8
  93. str_from := str(blob_from)
  94. str_to := str(blob_to)
  95. file, exists := files[change.From.Name]
  96. if !exists {
  97. analyser.handleInsertion(change, day, status, files, cache)
  98. return
  99. }
  100. // possible rename
  101. if change.To.Name != change.From.Name {
  102. analyser.handleRename(change.From.Name, change.To.Name, files)
  103. }
  104. dmp := diffmatchpatch.New()
  105. src, dst, _ := dmp.DiffLinesToRunes(str_from, str_to)
  106. if file.Len() != len(src) {
  107. panic(fmt.Sprintf("%s: internal integrity error src %d != %d",
  108. change.To.Name, len(src), file.Len()))
  109. }
  110. diffs := dmp.DiffMainRunes(src, dst, false)
  111. // we do not call RunesToDiffLines so the number of lines equals
  112. // to the rune count
  113. position := 0
  114. pending := diffmatchpatch.Diff{Text: ""}
  115. apply := func(edit diffmatchpatch.Diff) {
  116. length := utf8.RuneCountInString(edit.Text)
  117. if edit.Type == diffmatchpatch.DiffInsert {
  118. file.Update(day, position, length, 0)
  119. position += length
  120. } else {
  121. file.Update(day, position, 0, length)
  122. }
  123. if analyser.Debug {
  124. file.Validate()
  125. }
  126. }
  127. dump_before := ""
  128. if analyser.Debug {
  129. dump_before = file.Dump()
  130. }
  131. for _, edit := range diffs {
  132. length := utf8.RuneCountInString(edit.Text)
  133. func() {
  134. defer func() {
  135. r := recover()
  136. if r != nil {
  137. fmt.Fprintf(os.Stderr, "%s: internal diff error\n", change.To.Name)
  138. fmt.Fprintf(os.Stderr, "Update(%d, %d, %d, %d)\n", day, position,
  139. length, utf8.RuneCountInString(pending.Text))
  140. if dump_before != "" {
  141. fmt.Fprintf(os.Stderr, "====TREE BEFORE====\n%s====END====\n", dump_before)
  142. }
  143. fmt.Fprintf(os.Stderr, "====TREE AFTER====\n%s====END====\n", file.Dump())
  144. panic(r)
  145. }
  146. }()
  147. switch edit.Type {
  148. case diffmatchpatch.DiffEqual:
  149. if pending.Text != "" {
  150. apply(pending)
  151. pending.Text = ""
  152. }
  153. position += length
  154. case diffmatchpatch.DiffInsert:
  155. if pending.Text != "" {
  156. if pending.Type == diffmatchpatch.DiffInsert {
  157. panic("DiffInsert may not appear after DiffInsert")
  158. }
  159. file.Update(day, position, length, utf8.RuneCountInString(pending.Text))
  160. if analyser.Debug {
  161. file.Validate()
  162. }
  163. position += length
  164. pending.Text = ""
  165. } else {
  166. pending = edit
  167. }
  168. case diffmatchpatch.DiffDelete:
  169. if pending.Text != "" {
  170. panic("DiffDelete may not appear after DiffInsert/DiffDelete")
  171. }
  172. pending = edit
  173. default:
  174. panic(fmt.Sprintf("diff operation is not supported: %d", edit.Type))
  175. }
  176. }()
  177. }
  178. if pending.Text != "" {
  179. apply(pending)
  180. pending.Text = ""
  181. }
  182. if file.Len() != len(dst) {
  183. panic(fmt.Sprintf("%s: internal integrity error dst %d != %d",
  184. change.To.Name, len(dst), file.Len()))
  185. }
  186. }
  187. func (analyser *Analyser) handleRename(from, to string, files map[string]*File) {
  188. file, exists := files[from]
  189. if !exists {
  190. panic(fmt.Sprintf("file %s does not exist", from))
  191. }
  192. files[to] = file
  193. delete(files, from)
  194. }
  195. func (analyser *Analyser) Commits() []*object.Commit {
  196. result := []*object.Commit{}
  197. repository := analyser.Repository
  198. head, err := repository.Head()
  199. if err != nil {
  200. panic(err)
  201. }
  202. commit, err := repository.CommitObject(head.Hash())
  203. if err != nil {
  204. panic(err)
  205. }
  206. result = append(result, commit)
  207. for ; err != io.EOF; commit, err = commit.Parents().Next() {
  208. if err != nil {
  209. panic(err)
  210. }
  211. result = append(result, commit)
  212. }
  213. // reverse the order
  214. for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
  215. result[i], result[j] = result[j], result[i]
  216. }
  217. return result
  218. }
  219. func (analyser *Analyser) groupStatus(status map[int]int64, day int) []int64 {
  220. granularity := analyser.Granularity
  221. if granularity == 0 {
  222. granularity = 1
  223. }
  224. day++
  225. adjust := 0
  226. if day%granularity < granularity-1 {
  227. adjust = 1
  228. }
  229. result := make([]int64, day/granularity+adjust)
  230. var group int64
  231. for i := 0; i < day; i++ {
  232. group += status[i]
  233. if i%granularity == (granularity - 1) {
  234. result[i/granularity] = group
  235. group = 0
  236. }
  237. }
  238. if day%granularity < granularity-1 {
  239. result[len(result)-1] = group
  240. }
  241. return result
  242. }
  243. type sortableChange struct {
  244. change *object.Change
  245. hash plumbing.Hash
  246. }
  247. type sortableChanges []sortableChange
  248. func (change *sortableChange) Less(other *sortableChange) bool {
  249. for x := 0; x < 20; x++ {
  250. if change.hash[x] < other.hash[x] {
  251. return true
  252. }
  253. }
  254. return false
  255. }
  256. func (slice sortableChanges) Len() int {
  257. return len(slice)
  258. }
  259. func (slice sortableChanges) Less(i, j int) bool {
  260. return slice[i].Less(&slice[j])
  261. }
  262. func (slice sortableChanges) Swap(i, j int) {
  263. slice[i], slice[j] = slice[j], slice[i]
  264. }
  265. type sortableBlob struct {
  266. change *object.Change
  267. size int64
  268. }
  269. type sortableBlobs []sortableBlob
  270. func (change *sortableBlob) Less(other *sortableBlob) bool {
  271. return change.size < other.size
  272. }
  273. func (slice sortableBlobs) Len() int {
  274. return len(slice)
  275. }
  276. func (slice sortableBlobs) Less(i, j int) bool {
  277. return slice[i].Less(&slice[j])
  278. }
  279. func (slice sortableBlobs) Swap(i, j int) {
  280. slice[i], slice[j] = slice[j], slice[i]
  281. }
  282. func (analyser *Analyser) sizesAreClose(size1 int64, size2 int64) bool {
  283. return abs64(size1-size2)*100/max64(1, min64(size1, size2)) <=
  284. int64(100-analyser.SimilarityThreshold)
  285. }
  286. func (analyser *Analyser) blobsAreClose(
  287. blob1 *object.Blob, blob2 *object.Blob) bool {
  288. str_from := str(blob1)
  289. str_to := str(blob2)
  290. dmp := diffmatchpatch.New()
  291. src, dst, _ := dmp.DiffLinesToRunes(str_from, str_to)
  292. diffs := dmp.DiffMainRunes(src, dst, false)
  293. common := 0
  294. for _, edit := range diffs {
  295. if edit.Type == diffmatchpatch.DiffEqual {
  296. common += utf8.RuneCountInString(edit.Text)
  297. }
  298. }
  299. return common*100/min(len(src), len(dst)) >=
  300. analyser.SimilarityThreshold
  301. }
  302. func (analyser *Analyser) getBlob(hash plumbing.Hash) *object.Blob {
  303. blob, err := analyser.Repository.BlobObject(hash)
  304. if err != nil {
  305. panic(err)
  306. }
  307. return blob
  308. }
  309. func (analyser *Analyser) cacheBlobs(changes object.Changes) *map[plumbing.Hash]*object.Blob {
  310. cache := make(map[plumbing.Hash]*object.Blob)
  311. for _, change := range changes {
  312. action, err := change.Action()
  313. if err != nil {
  314. panic(err)
  315. }
  316. switch action {
  317. case merkletrie.Insert:
  318. cache[change.To.TreeEntry.Hash] = analyser.getBlob(change.To.TreeEntry.Hash)
  319. case merkletrie.Delete:
  320. cache[change.From.TreeEntry.Hash] = analyser.getBlob(change.From.TreeEntry.Hash)
  321. case merkletrie.Modify:
  322. cache[change.To.TreeEntry.Hash] = analyser.getBlob(change.To.TreeEntry.Hash)
  323. cache[change.From.TreeEntry.Hash] = analyser.getBlob(change.From.TreeEntry.Hash)
  324. default:
  325. panic(fmt.Sprintf("unsupported action: %d", change.Action))
  326. }
  327. }
  328. return &cache
  329. }
  330. func (analyser *Analyser) detectRenames(
  331. changes object.Changes, cache *map[plumbing.Hash]*object.Blob) object.Changes {
  332. reduced_changes := make(object.Changes, 0, changes.Len())
  333. // Stage 1 - find renames by matching the hashes
  334. // n log(n)
  335. // We sort additions and deletions by hash and then do the single scan along
  336. // both slices.
  337. deleted := make(sortableChanges, 0, changes.Len())
  338. added := make(sortableChanges, 0, changes.Len())
  339. for _, change := range changes {
  340. action, err := change.Action()
  341. if err != nil {
  342. panic(err)
  343. }
  344. switch action {
  345. case merkletrie.Insert:
  346. added = append(added, sortableChange{change, change.To.TreeEntry.Hash})
  347. case merkletrie.Delete:
  348. deleted = append(deleted, sortableChange{change, change.From.TreeEntry.Hash})
  349. case merkletrie.Modify:
  350. reduced_changes = append(reduced_changes, change)
  351. default:
  352. panic(fmt.Sprintf("unsupported action: %d", change.Action))
  353. }
  354. }
  355. sort.Sort(deleted)
  356. sort.Sort(added)
  357. a := 0
  358. d := 0
  359. still_deleted := make(object.Changes, 0, deleted.Len())
  360. still_added := make(object.Changes, 0, added.Len())
  361. for a < added.Len() && d < deleted.Len() {
  362. if added[a].hash == deleted[d].hash {
  363. reduced_changes = append(
  364. reduced_changes,
  365. &object.Change{From: deleted[d].change.From, To: added[a].change.To})
  366. a++
  367. d++
  368. } else if added[a].Less(&deleted[d]) {
  369. still_added = append(still_added, added[a].change)
  370. a++
  371. } else {
  372. still_deleted = append(still_deleted, deleted[d].change)
  373. d++
  374. }
  375. }
  376. for ; a < added.Len(); a++ {
  377. still_added = append(still_added, added[a].change)
  378. }
  379. for ; d < deleted.Len(); d++ {
  380. still_deleted = append(still_deleted, deleted[d].change)
  381. }
  382. // Stage 2 - apply the similarity threshold
  383. // n^2 but actually linear
  384. // We sort the blobs by size and do the single linear scan.
  385. added_blobs := make(sortableBlobs, 0, still_added.Len())
  386. deleted_blobs := make(sortableBlobs, 0, still_deleted.Len())
  387. for _, change := range still_added {
  388. blob := (*cache)[change.To.TreeEntry.Hash]
  389. added_blobs = append(
  390. added_blobs, sortableBlob{change: change, size: blob.Size})
  391. }
  392. for _, change := range still_deleted {
  393. blob := (*cache)[change.From.TreeEntry.Hash]
  394. deleted_blobs = append(
  395. deleted_blobs, sortableBlob{change: change, size: blob.Size})
  396. }
  397. sort.Sort(added_blobs)
  398. sort.Sort(deleted_blobs)
  399. d_start := 0
  400. for a = 0; a < added_blobs.Len(); a++ {
  401. my_blob := (*cache)[added_blobs[a].change.To.TreeEntry.Hash]
  402. my_size := added_blobs[a].size
  403. for d = d_start; d < deleted_blobs.Len() && !analyser.sizesAreClose(my_size, deleted_blobs[d].size); d++ {
  404. }
  405. d_start = d
  406. found_match := false
  407. for d = d_start; d < deleted_blobs.Len() && analyser.sizesAreClose(my_size, deleted_blobs[d].size); d++ {
  408. if analyser.blobsAreClose(
  409. my_blob, (*cache)[deleted_blobs[d].change.From.TreeEntry.Hash]) {
  410. found_match = true
  411. reduced_changes = append(
  412. reduced_changes,
  413. &object.Change{From: deleted_blobs[d].change.From,
  414. To: added_blobs[a].change.To})
  415. break
  416. }
  417. }
  418. if found_match {
  419. added_blobs = append(added_blobs[:a], added_blobs[a+1:]...)
  420. a--
  421. deleted_blobs = append(deleted_blobs[:d], deleted_blobs[d+1:]...)
  422. }
  423. }
  424. // Stage 3 - we give up, everything left are independent additions and deletions
  425. for _, blob := range added_blobs {
  426. reduced_changes = append(reduced_changes, blob.change)
  427. }
  428. for _, blob := range deleted_blobs {
  429. reduced_changes = append(reduced_changes, blob.change)
  430. }
  431. return reduced_changes
  432. }
  433. func (analyser *Analyser) Analyse(commits []*object.Commit) [][]int64 {
  434. sampling := analyser.Sampling
  435. if sampling == 0 {
  436. sampling = 1
  437. }
  438. onProgress := analyser.OnProgress
  439. if onProgress == nil {
  440. onProgress = func(int, int) {}
  441. }
  442. if analyser.SimilarityThreshold < 0 || analyser.SimilarityThreshold > 100 {
  443. panic("hercules.Analyser: an invalid SimilarityThreshold was specified")
  444. }
  445. // current daily alive number of lines; key is the number of days from the
  446. // beginning of the history
  447. status := map[int]int64{}
  448. // weekly snapshots of status
  449. statuses := [][]int64{}
  450. // mapping <file path> -> hercules.File
  451. files := map[string]*File{}
  452. var day0 time.Time // will be initialized in the first iteration
  453. var prev_tree *object.Tree = nil
  454. prev_day := 0
  455. for index, commit := range commits {
  456. onProgress(index, len(commits))
  457. tree, err := commit.Tree()
  458. if err != nil {
  459. panic(err)
  460. }
  461. if index == 0 {
  462. // first iteration - initialize the file objects from the tree
  463. day0 = commit.Author.When
  464. func() {
  465. file_iter := tree.Files()
  466. defer file_iter.Close()
  467. for {
  468. file, err := file_iter.Next()
  469. if err != nil {
  470. if err == io.EOF {
  471. break
  472. }
  473. panic(err)
  474. }
  475. lines, err := loc(&file.Blob)
  476. if err == nil {
  477. files[file.Name] = NewFile(0, lines, status)
  478. }
  479. }
  480. }()
  481. } else {
  482. day := int(commit.Author.When.Sub(day0).Hours() / 24)
  483. delta := (day / sampling) - (prev_day / sampling)
  484. if delta > 0 {
  485. prev_day = day
  486. gs := analyser.groupStatus(status, day)
  487. for i := 0; i < delta; i++ {
  488. statuses = append(statuses, gs)
  489. }
  490. }
  491. tree_diff, err := object.DiffTree(prev_tree, tree)
  492. if err != nil {
  493. panic(err)
  494. }
  495. cache := analyser.cacheBlobs(tree_diff)
  496. tree_diff = analyser.detectRenames(tree_diff, cache)
  497. for _, change := range tree_diff {
  498. action, err := change.Action()
  499. if err != nil {
  500. panic(err)
  501. }
  502. switch action {
  503. case merkletrie.Insert:
  504. analyser.handleInsertion(change, day, status, files, cache)
  505. case merkletrie.Delete:
  506. analyser.handleDeletion(change, day, status, files, cache)
  507. case merkletrie.Modify:
  508. func() {
  509. defer func() {
  510. r := recover()
  511. if r != nil {
  512. fmt.Fprintf(os.Stderr, "#%d - %s: modification error\n",
  513. index, commit.Hash.String())
  514. panic(r)
  515. }
  516. }()
  517. analyser.handleModification(change, day, status, files, cache)
  518. }()
  519. }
  520. }
  521. }
  522. prev_tree = tree
  523. }
  524. return statuses
  525. }