forks.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. package core
  2. import (
  3. "log"
  4. "reflect"
  5. "sort"
  6. "gopkg.in/src-d/go-git.v4/plumbing/object"
  7. "gopkg.in/src-d/go-git.v4/plumbing"
  8. "gopkg.in/src-d/hercules.v4/internal/toposort"
  9. )
  10. // OneShotMergeProcessor provides the convenience method to consume merges only once.
  11. type OneShotMergeProcessor struct {
  12. merges map[plumbing.Hash]bool
  13. }
  14. // Initialize resets OneShotMergeProcessor.
  15. func (proc *OneShotMergeProcessor) Initialize() {
  16. proc.merges = map[plumbing.Hash]bool{}
  17. }
  18. // ShouldConsumeCommit returns true on regular commits. It also returns true upon
  19. // the first occurrence of a particular merge commit.
  20. func (proc *OneShotMergeProcessor) ShouldConsumeCommit(deps map[string]interface{}) bool {
  21. commit := deps[DependencyCommit].(*object.Commit)
  22. if commit.NumParents() <= 1 {
  23. return true
  24. }
  25. if !proc.merges[commit.Hash] {
  26. proc.merges[commit.Hash] = true
  27. return true
  28. }
  29. return false
  30. }
  31. // NoopMerger provides an empty Merge() method suitable for PipelineItem.
  32. type NoopMerger struct {
  33. }
  34. // Merge does nothing.
  35. func (merger *NoopMerger) Merge(branches []PipelineItem) {
  36. // no-op
  37. }
  38. // ForkSamePipelineItem clones items by referencing the same origin.
  39. func ForkSamePipelineItem(origin PipelineItem, n int) []PipelineItem {
  40. clones := make([]PipelineItem, n)
  41. for i := 0; i < n; i++ {
  42. clones[i] = origin
  43. }
  44. return clones
  45. }
  46. // ForkCopyPipelineItem clones items by copying them by value from the origin.
  47. func ForkCopyPipelineItem(origin PipelineItem, n int) []PipelineItem {
  48. originValue := reflect.Indirect(reflect.ValueOf(origin))
  49. originType := originValue.Type()
  50. clones := make([]PipelineItem, n)
  51. for i := 0; i < n; i++ {
  52. cloneValue := reflect.New(originType).Elem()
  53. cloneValue.Set(originValue)
  54. clones[i] = cloneValue.Addr().Interface().(PipelineItem)
  55. }
  56. return clones
  57. }
  58. const (
  59. // runActionCommit corresponds to a regular commit
  60. runActionCommit = 0
  61. // runActionFork splits a branch into several parts
  62. runActionFork = iota
  63. // runActionMerge merges several branches together
  64. runActionMerge = iota
  65. // runActionEmerge starts a root branch
  66. runActionEmerge = iota
  67. // runActionDelete removes the branch as it is no longer needed
  68. runActionDelete = iota
  69. // rootBranchIndex is the minimum branch index in the plan
  70. rootBranchIndex = 1
  71. )
  72. type runAction struct {
  73. Action int
  74. Commit *object.Commit
  75. Items []int
  76. }
  77. type orderer = func(reverse, direction bool) []string
  78. func cloneItems(origin []PipelineItem, n int) [][]PipelineItem {
  79. clones := make([][]PipelineItem, n)
  80. for j := 0; j < n; j++ {
  81. clones[j] = make([]PipelineItem, len(origin))
  82. }
  83. for i, item := range origin {
  84. itemClones := item.Fork(n)
  85. for j := 0; j < n; j++ {
  86. clones[j][i] = itemClones[j]
  87. }
  88. }
  89. return clones
  90. }
  91. func mergeItems(branches [][]PipelineItem) {
  92. buffer := make([]PipelineItem, len(branches) - 1)
  93. for i, item := range branches[0] {
  94. for j := 0; j < len(branches)-1; j++ {
  95. buffer[j] = branches[j+1][i]
  96. }
  97. item.Merge(buffer)
  98. }
  99. }
  100. // getMasterBranch returns the branch with the smallest index.
  101. func getMasterBranch(branches map[int][]PipelineItem) []PipelineItem {
  102. minKey := 1 << 31
  103. var minVal []PipelineItem
  104. for key, val := range branches {
  105. if key < minKey {
  106. minKey = key
  107. minVal = val
  108. }
  109. }
  110. return minVal
  111. }
  112. // prepareRunPlan schedules the actions for Pipeline.Run().
  113. func prepareRunPlan(commits []*object.Commit) []runAction {
  114. hashes, dag := buildDag(commits)
  115. leaveRootComponent(hashes, dag)
  116. mergedDag, mergedSeq := mergeDag(hashes, dag)
  117. orderNodes := bindOrderNodes(mergedDag)
  118. collapseFastForwards(orderNodes, hashes, mergedDag, dag, mergedSeq)
  119. /*fmt.Printf("digraph Hercules {\n")
  120. for i, c := range orderNodes(false, false) {
  121. commit := hashes[c]
  122. fmt.Printf(" \"%s\"[label=\"[%d] %s\"]\n", commit.Hash.String(), i, commit.Hash.String()[:6])
  123. for _, child := range mergedDag[commit.Hash] {
  124. fmt.Printf(" \"%s\" -> \"%s\"\n", commit.Hash.String(), child.Hash.String())
  125. }
  126. }
  127. fmt.Printf("}\n")*/
  128. plan := generatePlan(orderNodes, hashes, mergedDag, dag, mergedSeq)
  129. plan = optimizePlan(plan)
  130. /*for _, p := range plan {
  131. firstItem := p.Items[0]
  132. switch p.Action {
  133. case runActionCommit:
  134. fmt.Fprintln(os.Stderr, "C", firstItem, p.Commit.Hash.String())
  135. case runActionFork:
  136. fmt.Fprintln(os.Stderr, "F", p.Items)
  137. case runActionMerge:
  138. fmt.Fprintln(os.Stderr, "M", p.Items)
  139. }
  140. }*/
  141. return plan
  142. }
  143. // buildDag generates the raw commit DAG and the commit hash map.
  144. func buildDag(commits []*object.Commit) (
  145. map[string]*object.Commit, map[plumbing.Hash][]*object.Commit) {
  146. hashes := map[string]*object.Commit{}
  147. for _, commit := range commits {
  148. hashes[commit.Hash.String()] = commit
  149. }
  150. dag := map[plumbing.Hash][]*object.Commit{}
  151. for _, commit := range commits {
  152. if _, exists := dag[commit.Hash]; !exists {
  153. dag[commit.Hash] = make([]*object.Commit, 0, 1)
  154. }
  155. for _, parent := range commit.ParentHashes {
  156. if _, exists := hashes[parent.String()]; !exists {
  157. continue
  158. }
  159. children := dag[parent]
  160. if children == nil {
  161. children = make([]*object.Commit, 0, 1)
  162. }
  163. dag[parent] = append(children, commit)
  164. }
  165. }
  166. return hashes, dag
  167. }
  168. // leaveRootComponent runs connected components analysis and throws away everything
  169. // but the part which grows from the root.
  170. func leaveRootComponent(
  171. hashes map[string]*object.Commit,
  172. dag map[plumbing.Hash][]*object.Commit) {
  173. visited := map[plumbing.Hash]bool{}
  174. var sets [][]plumbing.Hash
  175. for key := range dag {
  176. if visited[key] {
  177. continue
  178. }
  179. var set []plumbing.Hash
  180. for queue := []plumbing.Hash{key}; len(queue) > 0; {
  181. head := queue[len(queue)-1]
  182. queue = queue[:len(queue)-1]
  183. if visited[head] {
  184. continue
  185. }
  186. set = append(set, head)
  187. visited[head] = true
  188. for _, c := range dag[head] {
  189. if !visited[c.Hash] {
  190. queue = append(queue, c.Hash)
  191. }
  192. }
  193. if commit, exists := hashes[head.String()]; exists {
  194. for _, p := range commit.ParentHashes {
  195. if !visited[p] {
  196. if _, exists := hashes[p.String()]; exists {
  197. queue = append(queue, p)
  198. }
  199. }
  200. }
  201. }
  202. }
  203. sets = append(sets, set)
  204. }
  205. if len(sets) > 1 {
  206. maxlen := 0
  207. maxind := -1
  208. for i, set := range sets {
  209. if len(set) > maxlen {
  210. maxlen = len(set)
  211. maxind = i
  212. }
  213. }
  214. for i, set := range sets {
  215. if i == maxind {
  216. continue
  217. }
  218. for _, h := range set {
  219. log.Printf("warning: dropped %s from the analysis - disjoint", h.String())
  220. delete(dag, h)
  221. delete(hashes, h.String())
  222. }
  223. }
  224. }
  225. }
  226. // bindOrderNodes returns curried "orderNodes" function.
  227. func bindOrderNodes(mergedDag map[plumbing.Hash][]*object.Commit) orderer {
  228. return func(reverse, direction bool) []string {
  229. graph := toposort.NewGraph()
  230. keys := make([]plumbing.Hash, 0, len(mergedDag))
  231. for key := range mergedDag {
  232. keys = append(keys, key)
  233. }
  234. sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
  235. for _, key := range keys {
  236. graph.AddNode(key.String())
  237. }
  238. for _, key := range keys {
  239. children := mergedDag[key]
  240. sort.Slice(children, func(i, j int) bool {
  241. return children[i].Hash.String() < children[j].Hash.String()
  242. })
  243. for _, c := range children {
  244. if !direction {
  245. graph.AddEdge(key.String(), c.Hash.String())
  246. } else {
  247. graph.AddEdge(c.Hash.String(), key.String())
  248. }
  249. }
  250. }
  251. order, ok := graph.Toposort()
  252. if !ok {
  253. // should never happen
  254. panic("Could not topologically sort the DAG of commits")
  255. }
  256. if reverse != direction {
  257. // one day this must appear in the standard library...
  258. for i, j := 0, len(order)-1; i < len(order)/2; i, j = i+1, j-1 {
  259. order[i], order[j] = order[j], order[i]
  260. }
  261. }
  262. return order
  263. }
  264. }
  265. // inverts `dag`
  266. func buildParents(dag map[plumbing.Hash][]*object.Commit) map[plumbing.Hash]map[plumbing.Hash]bool {
  267. parents := map[plumbing.Hash]map[plumbing.Hash]bool{}
  268. for key, vals := range dag {
  269. for _, val := range vals {
  270. myps := parents[val.Hash]
  271. if myps == nil {
  272. myps = map[plumbing.Hash]bool{}
  273. parents[val.Hash] = myps
  274. }
  275. myps[key] = true
  276. }
  277. }
  278. return parents
  279. }
  280. // mergeDag turns sequences of consecutive commits into single nodes.
  281. func mergeDag(
  282. hashes map[string]*object.Commit,
  283. dag map[plumbing.Hash][]*object.Commit) (
  284. mergedDag, mergedSeq map[plumbing.Hash][]*object.Commit) {
  285. parents := buildParents(dag)
  286. mergedDag = map[plumbing.Hash][]*object.Commit{}
  287. mergedSeq = map[plumbing.Hash][]*object.Commit{}
  288. visited := map[plumbing.Hash]bool{}
  289. for head := range dag {
  290. if visited[head] {
  291. continue
  292. }
  293. c := head
  294. for true {
  295. nextParents := parents[c]
  296. var next plumbing.Hash
  297. for p := range nextParents {
  298. next = p
  299. break
  300. }
  301. if len(nextParents) != 1 || len(dag[next]) != 1 {
  302. break
  303. }
  304. c = next
  305. }
  306. head = c
  307. var seq []*object.Commit
  308. for true {
  309. visited[c] = true
  310. seq = append(seq, hashes[c.String()])
  311. if len(dag[c]) != 1 {
  312. break
  313. }
  314. c = dag[c][0].Hash
  315. if len(parents[c]) != 1 {
  316. break
  317. }
  318. }
  319. mergedSeq[head] = seq
  320. mergedDag[head] = dag[seq[len(seq)-1].Hash]
  321. }
  322. return
  323. }
  324. // collapseFastForwards removes the fast forward merges.
  325. func collapseFastForwards(
  326. orderNodes orderer, hashes map[string]*object.Commit,
  327. mergedDag, dag, mergedSeq map[plumbing.Hash][]*object.Commit) {
  328. parents := buildParents(mergedDag)
  329. processed := map[plumbing.Hash]bool{}
  330. for _, strkey := range orderNodes(false, true) {
  331. key := hashes[strkey].Hash
  332. processed[key] = true
  333. repeat:
  334. vals, exists := mergedDag[key]
  335. if !exists {
  336. continue
  337. }
  338. if len(vals) < 2 {
  339. continue
  340. }
  341. toRemove := map[plumbing.Hash]bool{}
  342. for _, child := range vals {
  343. var queue []plumbing.Hash
  344. visited := map[plumbing.Hash]bool{child.Hash: true}
  345. childParents := parents[child.Hash]
  346. childNumOtherParents := 0
  347. for parent := range childParents {
  348. if parent != key {
  349. visited[parent] = true
  350. childNumOtherParents++
  351. queue = append(queue, parent)
  352. }
  353. }
  354. var immediateParent plumbing.Hash
  355. if childNumOtherParents == 1 {
  356. immediateParent = queue[0]
  357. }
  358. for len(queue) > 0 {
  359. head := queue[len(queue)-1]
  360. queue = queue[:len(queue)-1]
  361. if processed[head] {
  362. if head == key {
  363. toRemove[child.Hash] = true
  364. if childNumOtherParents == 1 && len(mergedDag[immediateParent]) == 1 {
  365. mergedSeq[immediateParent] = append(
  366. mergedSeq[immediateParent], mergedSeq[child.Hash]...)
  367. delete(mergedSeq, child.Hash)
  368. mergedDag[immediateParent] = mergedDag[child.Hash]
  369. delete(mergedDag, child.Hash)
  370. parents[child.Hash] = parents[immediateParent]
  371. for _, vals := range parents {
  372. for v := range vals {
  373. if v == child.Hash {
  374. delete(vals, v)
  375. vals[immediateParent] = true
  376. break
  377. }
  378. }
  379. }
  380. }
  381. }
  382. break
  383. }
  384. for parent := range parents[head] {
  385. if !visited[parent] {
  386. visited[head] = true
  387. queue = append(queue, parent)
  388. }
  389. }
  390. }
  391. }
  392. if len(toRemove) == 0 {
  393. continue
  394. }
  395. // update dag
  396. var newVals []*object.Commit
  397. node := mergedSeq[key][len(mergedSeq[key])-1].Hash
  398. for _, child := range dag[node] {
  399. if !toRemove[child.Hash] {
  400. newVals = append(newVals, child)
  401. }
  402. }
  403. dag[node] = newVals
  404. // update mergedDag
  405. newVals = []*object.Commit{}
  406. for _, child := range vals {
  407. if !toRemove[child.Hash] {
  408. newVals = append(newVals, child)
  409. }
  410. }
  411. merged := false
  412. if len(newVals) == 1 {
  413. onlyChild := newVals[0].Hash
  414. if len(parents[onlyChild]) == 1 {
  415. merged = true
  416. mergedSeq[key] = append(mergedSeq[key], mergedSeq[onlyChild]...)
  417. delete(mergedSeq, onlyChild)
  418. mergedDag[key] = mergedDag[onlyChild]
  419. delete(mergedDag, onlyChild)
  420. parents[onlyChild] = parents[key]
  421. for _, vals := range parents {
  422. for v := range vals {
  423. if v == onlyChild {
  424. delete(vals, v)
  425. vals[key] = true
  426. break
  427. }
  428. }
  429. }
  430. }
  431. }
  432. if !merged {
  433. mergedDag[key] = newVals
  434. } else {
  435. goto repeat
  436. }
  437. }
  438. }
  439. // generatePlan creates the list of actions from the commit DAG.
  440. func generatePlan(
  441. orderNodes orderer, hashes map[string]*object.Commit,
  442. mergedDag, dag, mergedSeq map[plumbing.Hash][]*object.Commit) []runAction {
  443. parents := buildParents(dag)
  444. var plan []runAction
  445. branches := map[plumbing.Hash]int{}
  446. branchers := map[plumbing.Hash]map[plumbing.Hash]int{}
  447. counter := rootBranchIndex
  448. for _, name := range orderNodes(false, true) {
  449. commit := hashes[name]
  450. if len(parents[commit.Hash]) == 0 {
  451. branches[commit.Hash] = counter
  452. plan = append(plan, runAction{
  453. Action: runActionEmerge,
  454. Commit: commit,
  455. Items: []int{counter},
  456. })
  457. counter++
  458. }
  459. var branch int
  460. {
  461. var exists bool
  462. branch, exists = branches[commit.Hash]
  463. if !exists {
  464. branch = -1
  465. }
  466. }
  467. branchExists := func() bool { return branch >= 0 }
  468. appendCommit := func(c *object.Commit, branch int) {
  469. if branch == 0 {
  470. log.Panicf("setting a zero branch for %s", c.Hash.String())
  471. }
  472. plan = append(plan, runAction{
  473. Action: runActionCommit,
  474. Commit: c,
  475. Items: []int{branch},
  476. })
  477. }
  478. appendMergeIfNeeded := func() {
  479. if len(parents[commit.Hash]) < 2 {
  480. return
  481. }
  482. // merge after the merge commit (the first in the sequence)
  483. var items []int
  484. minBranch := 1 << 31
  485. for parent := range parents[commit.Hash] {
  486. parentBranch := -1
  487. if parents, exists := branchers[commit.Hash]; exists {
  488. if inheritedBranch, exists := parents[parent]; exists {
  489. parentBranch = inheritedBranch
  490. }
  491. }
  492. if parentBranch == -1 {
  493. parentBranch = branches[parent]
  494. if parentBranch < rootBranchIndex {
  495. log.Panicf("parent %s > %s does not have a branch assigned",
  496. parent.String(), commit.Hash.String())
  497. }
  498. }
  499. if len(dag[parent]) == 1 && minBranch > parentBranch {
  500. minBranch = parentBranch
  501. }
  502. items = append(items, parentBranch)
  503. if parentBranch != branch {
  504. appendCommit(commit, parentBranch)
  505. }
  506. }
  507. // there should be no duplicates in items
  508. if minBranch < 1 << 31 {
  509. branch = minBranch
  510. branches[commit.Hash] = minBranch
  511. } else if !branchExists() {
  512. log.Panicf("!branchExists(%s)", commit.Hash.String())
  513. }
  514. plan = append(plan, runAction{
  515. Action: runActionMerge,
  516. Commit: nil,
  517. Items: items,
  518. })
  519. }
  520. var head plumbing.Hash
  521. if subseq, exists := mergedSeq[commit.Hash]; exists {
  522. for subseqIndex, offspring := range subseq {
  523. if branchExists() {
  524. appendCommit(offspring, branch)
  525. }
  526. if subseqIndex == 0 {
  527. appendMergeIfNeeded()
  528. }
  529. }
  530. head = subseq[len(subseq)-1].Hash
  531. branches[head] = branch
  532. } else {
  533. head = commit.Hash
  534. }
  535. if len(mergedDag[commit.Hash]) > 1 {
  536. children := []int{branch}
  537. for i, child := range mergedDag[commit.Hash] {
  538. if i == 0 {
  539. branches[child.Hash] = branch
  540. continue
  541. }
  542. if _, exists := branches[child.Hash]; !exists {
  543. branches[child.Hash] = counter
  544. }
  545. parents := branchers[child.Hash]
  546. if parents == nil {
  547. parents = map[plumbing.Hash]int{}
  548. branchers[child.Hash] = parents
  549. }
  550. parents[head] = counter
  551. children = append(children, counter)
  552. counter++
  553. }
  554. plan = append(plan, runAction{
  555. Action: runActionFork,
  556. Commit: hashes[head.String()],
  557. Items: children,
  558. })
  559. }
  560. }
  561. return plan
  562. }
  563. // optimizePlan removes "dead" nodes and inserts `runActionDelete` disposal steps.
  564. //
  565. // | *
  566. // * /
  567. // |\/
  568. // |/
  569. // *
  570. //
  571. func optimizePlan(plan []runAction) []runAction {
  572. // lives maps branch index to the number of commits in that branch
  573. lives := map[int]int{}
  574. // lastMentioned maps branch index to the index inside `plan` when that branch was last used
  575. lastMentioned := map[int]int{}
  576. for i, p := range plan {
  577. firstItem := p.Items[0]
  578. switch p.Action {
  579. case runActionCommit:
  580. lives[firstItem]++
  581. lastMentioned[firstItem] = i
  582. if firstItem == -1 {
  583. log.Panicf("commit %s does not have an assigned branch",
  584. p.Commit.Hash.String())
  585. }
  586. case runActionFork:
  587. lastMentioned[firstItem] = i
  588. case runActionMerge:
  589. for _, item := range p.Items {
  590. lastMentioned[item] = i
  591. }
  592. case runActionEmerge:
  593. lives[firstItem]++
  594. lastMentioned[firstItem] = i
  595. }
  596. }
  597. branchesToDelete := map[int]bool{}
  598. for key, life := range lives {
  599. if life == 1 {
  600. branchesToDelete[key] = true
  601. delete(lastMentioned, key)
  602. }
  603. }
  604. var optimizedPlan []runAction
  605. lastMentionedArr := make([][2]int, 0, len(lastMentioned) + 1)
  606. for key, val := range lastMentioned {
  607. if val != len(plan) - 1 {
  608. lastMentionedArr = append(lastMentionedArr, [2]int{val, key})
  609. }
  610. }
  611. if len(lastMentionedArr) == 0 && len(branchesToDelete) == 0 {
  612. // early return - we have nothing to optimize
  613. return plan
  614. }
  615. sort.Slice(lastMentionedArr, func(i, j int) bool {
  616. return lastMentionedArr[i][0] < lastMentionedArr[j][0]
  617. })
  618. lastMentionedArr = append(lastMentionedArr, [2]int{len(plan)-1, -1})
  619. prevpi := -1
  620. for _, pair := range lastMentionedArr {
  621. for pi := prevpi + 1; pi <= pair[0]; pi++ {
  622. p := plan[pi]
  623. switch p.Action {
  624. case runActionCommit:
  625. if !branchesToDelete[p.Items[0]] {
  626. optimizedPlan = append(optimizedPlan, p)
  627. }
  628. case runActionFork:
  629. var newBranches []int
  630. for _, b := range p.Items {
  631. if !branchesToDelete[b] {
  632. newBranches = append(newBranches, b)
  633. }
  634. }
  635. if len(newBranches) > 1 {
  636. optimizedPlan = append(optimizedPlan, runAction{
  637. Action: runActionFork,
  638. Commit: p.Commit,
  639. Items: newBranches,
  640. })
  641. }
  642. case runActionMerge:
  643. var newBranches []int
  644. for _, b := range p.Items {
  645. if !branchesToDelete[b] {
  646. newBranches = append(newBranches, b)
  647. }
  648. }
  649. if len(newBranches) > 1 {
  650. optimizedPlan = append(optimizedPlan, runAction{
  651. Action: runActionMerge,
  652. Commit: p.Commit,
  653. Items: newBranches,
  654. })
  655. }
  656. case runActionEmerge:
  657. optimizedPlan = append(optimizedPlan, p)
  658. }
  659. }
  660. if pair[1] >= 0 {
  661. prevpi = pair[0]
  662. optimizedPlan = append(optimizedPlan, runAction{
  663. Action: runActionDelete,
  664. Commit: nil,
  665. Items: []int{pair[1]},
  666. })
  667. }
  668. }
  669. // single commit can be detected as redundant
  670. if len(optimizedPlan) > 0 {
  671. return optimizedPlan
  672. }
  673. return plan
  674. }