本站源代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

211 lines
5.4KB

  1. package object
  2. import (
  3. "fmt"
  4. "sort"
  5. "gopkg.in/src-d/go-git.v4/plumbing"
  6. "gopkg.in/src-d/go-git.v4/plumbing/storer"
  7. )
  8. // errIsReachable is thrown when first commit is an ancestor of the second
  9. var errIsReachable = fmt.Errorf("first is reachable from second")
  10. // MergeBase mimics the behavior of `git merge-base actual other`, returning the
  11. // best common ancestor between the actual and the passed one.
  12. // The best common ancestors can not be reached from other common ancestors.
  13. func (c *Commit) MergeBase(other *Commit) ([]*Commit, error) {
  14. // use sortedByCommitDateDesc strategy
  15. sorted := sortByCommitDateDesc(c, other)
  16. newer := sorted[0]
  17. older := sorted[1]
  18. newerHistory, err := ancestorsIndex(older, newer)
  19. if err == errIsReachable {
  20. return []*Commit{older}, nil
  21. }
  22. if err != nil {
  23. return nil, err
  24. }
  25. var res []*Commit
  26. inNewerHistory := isInIndexCommitFilter(newerHistory)
  27. resIter := NewFilterCommitIter(older, &inNewerHistory, &inNewerHistory)
  28. _ = resIter.ForEach(func(commit *Commit) error {
  29. res = append(res, commit)
  30. return nil
  31. })
  32. return Independents(res)
  33. }
  34. // IsAncestor returns true if the actual commit is ancestor of the passed one.
  35. // It returns an error if the history is not transversable
  36. // It mimics the behavior of `git merge --is-ancestor actual other`
  37. func (c *Commit) IsAncestor(other *Commit) (bool, error) {
  38. found := false
  39. iter := NewCommitPreorderIter(other, nil, nil)
  40. err := iter.ForEach(func(comm *Commit) error {
  41. if comm.Hash != c.Hash {
  42. return nil
  43. }
  44. found = true
  45. return storer.ErrStop
  46. })
  47. return found, err
  48. }
  49. // ancestorsIndex returns a map with the ancestors of the starting commit if the
  50. // excluded one is not one of them. It returns errIsReachable if the excluded commit
  51. // is ancestor of the starting, or another error if the history is not traversable.
  52. func ancestorsIndex(excluded, starting *Commit) (map[plumbing.Hash]struct{}, error) {
  53. if excluded.Hash.String() == starting.Hash.String() {
  54. return nil, errIsReachable
  55. }
  56. startingHistory := map[plumbing.Hash]struct{}{}
  57. startingIter := NewCommitIterBSF(starting, nil, nil)
  58. err := startingIter.ForEach(func(commit *Commit) error {
  59. if commit.Hash == excluded.Hash {
  60. return errIsReachable
  61. }
  62. startingHistory[commit.Hash] = struct{}{}
  63. return nil
  64. })
  65. if err != nil {
  66. return nil, err
  67. }
  68. return startingHistory, nil
  69. }
  70. // Independents returns a subset of the passed commits, that are not reachable the others
  71. // It mimics the behavior of `git merge-base --independent commit...`.
  72. func Independents(commits []*Commit) ([]*Commit, error) {
  73. // use sortedByCommitDateDesc strategy
  74. candidates := sortByCommitDateDesc(commits...)
  75. candidates = removeDuplicated(candidates)
  76. seen := map[plumbing.Hash]struct{}{}
  77. var isLimit CommitFilter = func(commit *Commit) bool {
  78. _, ok := seen[commit.Hash]
  79. return ok
  80. }
  81. if len(candidates) < 2 {
  82. return candidates, nil
  83. }
  84. pos := 0
  85. for {
  86. from := candidates[pos]
  87. others := remove(candidates, from)
  88. fromHistoryIter := NewFilterCommitIter(from, nil, &isLimit)
  89. err := fromHistoryIter.ForEach(func(fromAncestor *Commit) error {
  90. for _, other := range others {
  91. if fromAncestor.Hash == other.Hash {
  92. candidates = remove(candidates, other)
  93. others = remove(others, other)
  94. }
  95. }
  96. if len(candidates) == 1 {
  97. return storer.ErrStop
  98. }
  99. seen[fromAncestor.Hash] = struct{}{}
  100. return nil
  101. })
  102. if err != nil {
  103. return nil, err
  104. }
  105. nextPos := indexOf(candidates, from) + 1
  106. if nextPos >= len(candidates) {
  107. break
  108. }
  109. pos = nextPos
  110. }
  111. return candidates, nil
  112. }
  113. // sortByCommitDateDesc returns the passed commits, sorted by `committer.When desc`
  114. //
  115. // Following this strategy, it is tried to reduce the time needed when walking
  116. // the history from one commit to reach the others. It is assumed that ancestors
  117. // use to be committed before its descendant;
  118. // That way `Independents(A^, A)` will be processed as being `Independents(A, A^)`;
  119. // so starting by `A` it will be reached `A^` way sooner than walking from `A^`
  120. // to the initial commit, and then from `A` to `A^`.
  121. func sortByCommitDateDesc(commits ...*Commit) []*Commit {
  122. sorted := make([]*Commit, len(commits))
  123. copy(sorted, commits)
  124. sort.Slice(sorted, func(i, j int) bool {
  125. return sorted[i].Committer.When.After(sorted[j].Committer.When)
  126. })
  127. return sorted
  128. }
  129. // indexOf returns the first position where target was found in the passed commits
  130. func indexOf(commits []*Commit, target *Commit) int {
  131. for i, commit := range commits {
  132. if target.Hash == commit.Hash {
  133. return i
  134. }
  135. }
  136. return -1
  137. }
  138. // remove returns the passed commits excluding the commit toDelete
  139. func remove(commits []*Commit, toDelete *Commit) []*Commit {
  140. res := make([]*Commit, len(commits))
  141. j := 0
  142. for _, commit := range commits {
  143. if commit.Hash == toDelete.Hash {
  144. continue
  145. }
  146. res[j] = commit
  147. j++
  148. }
  149. return res[:j]
  150. }
  151. // removeDuplicated removes duplicated commits from the passed slice of commits
  152. func removeDuplicated(commits []*Commit) []*Commit {
  153. seen := make(map[plumbing.Hash]struct{}, len(commits))
  154. res := make([]*Commit, len(commits))
  155. j := 0
  156. for _, commit := range commits {
  157. if _, ok := seen[commit.Hash]; ok {
  158. continue
  159. }
  160. seen[commit.Hash] = struct{}{}
  161. res[j] = commit
  162. j++
  163. }
  164. return res[:j]
  165. }
  166. // isInIndexCommitFilter returns a commitFilter that returns true
  167. // if the commit is in the passed index.
  168. func isInIndexCommitFilter(index map[plumbing.Hash]struct{}) CommitFilter {
  169. return func(c *Commit) bool {
  170. _, ok := index[c.Hash]
  171. return ok
  172. }
  173. }
上海开阖软件有限公司 沪ICP备12045867号-1