forks.go 18 KB

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