forks.go 18 KB

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