|
@@ -99,9 +99,18 @@ type PipelineItem interface {
|
|
|
Initialize(*git.Repository)
|
|
|
// Consume processes the next commit.
|
|
|
// deps contains the required entities which match Depends(). Besides, it always includes
|
|
|
- // "commit" and "index".
|
|
|
+ // DependencyCommit and DependencyIndex.
|
|
|
// Returns the calculated entities which match Provides().
|
|
|
Consume(deps map[string]interface{}) (map[string]interface{}, error)
|
|
|
+ // Fork clones the item the requested number of times. The data links between the clones
|
|
|
+ // are up to the implementation. Needed to handle Git branches. See also Merge().
|
|
|
+ // Returns a slice with `n` fresh clones. In other words, it does not include the original item.
|
|
|
+ Fork(n int) []PipelineItem
|
|
|
+ // Merge combines several branches together. Each is supposed to have been created with Fork().
|
|
|
+ // The result is stored in the called item, thus this function returns nothing.
|
|
|
+ // Merge() must update all the branches, not only self. When several branches merge, some of
|
|
|
+ // them may continue to live, hence this requirement.
|
|
|
+ Merge(branches []PipelineItem)
|
|
|
}
|
|
|
|
|
|
// FeaturedPipelineItem enables switching the automatic insertion of pipeline items on or off.
|
|
@@ -197,8 +206,8 @@ func MetadataToCommonAnalysisResult(meta *Metadata) *CommonAnalysisResult {
|
|
|
// See the extended example of how a Pipeline works in doc.go
|
|
|
type Pipeline struct {
|
|
|
// OnProgress is the callback which is invoked in Analyse() to output it's
|
|
|
- // progress. The first argument is the number of processed commits and the
|
|
|
- // second is the total number of commits.
|
|
|
+ // progress. The first argument is the number of complete steps and the
|
|
|
+ // second is the total number of steps.
|
|
|
OnProgress func(int, int)
|
|
|
|
|
|
// Repository points to the analysed Git repository struct from go-git.
|
|
@@ -227,6 +236,12 @@ const (
|
|
|
// ConfigPipelineCommits is the name of the Pipeline configuration option (Pipeline.Initialize())
|
|
|
// which allows to specify the custom commit sequence. By default, Pipeline.Commits() is used.
|
|
|
ConfigPipelineCommits = "commits"
|
|
|
+ // DependencyCommit is the name of one of the two items in `deps` supplied to PipelineItem.Consume()
|
|
|
+ // which always exists. It corresponds to the currently analyzed commit.
|
|
|
+ DependencyCommit = "commit"
|
|
|
+ // DependencyIndex is the name of one of the two items in `deps` supplied to PipelineItem.Consume()
|
|
|
+ // which always exists. It corresponds to the currently analyzed commit's index.
|
|
|
+ DependencyIndex = "index"
|
|
|
)
|
|
|
|
|
|
// NewPipeline initializes a new instance of Pipeline struct.
|
|
@@ -549,33 +564,56 @@ func (pipeline *Pipeline) Run(commits []*object.Commit) (map[LeafPipelineItem]in
|
|
|
if onProgress == nil {
|
|
|
onProgress = func(int, int) {}
|
|
|
}
|
|
|
+ plan := prepareRunPlan(commits)
|
|
|
+ progressSteps := len(plan) + 2
|
|
|
+ branches := map[int][]PipelineItem{0: pipeline.items}
|
|
|
|
|
|
- for index, commit := range commits {
|
|
|
- onProgress(index, len(commits))
|
|
|
- state := map[string]interface{}{"commit": commit, "index": index}
|
|
|
- for _, item := range pipeline.items {
|
|
|
- update, err := item.Consume(state)
|
|
|
- if err != nil {
|
|
|
- log.Printf("%s failed on commit #%d %s\n",
|
|
|
- item.Name(), index, commit.Hash.String())
|
|
|
- return nil, err
|
|
|
- }
|
|
|
- for _, key := range item.Provides() {
|
|
|
- val, ok := update[key]
|
|
|
- if !ok {
|
|
|
- panic(fmt.Sprintf("%s: Consume() did not return %s", item.Name(), key))
|
|
|
+ for index, step := range plan {
|
|
|
+ onProgress(index + 1, progressSteps)
|
|
|
+ firstItem := step.Items[0]
|
|
|
+ switch step.Action {
|
|
|
+ case runActionCommit:
|
|
|
+ state := map[string]interface{}{
|
|
|
+ DependencyCommit: step.Commit,
|
|
|
+ DependencyIndex: index,
|
|
|
+ }
|
|
|
+ for _, item := range branches[firstItem] {
|
|
|
+ update, err := item.Consume(state)
|
|
|
+ if err != nil {
|
|
|
+ log.Printf("%s failed on commit #%d %s\n",
|
|
|
+ item.Name(), index + 1, step.Commit.Hash.String())
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ for _, key := range item.Provides() {
|
|
|
+ val, ok := update[key]
|
|
|
+ if !ok {
|
|
|
+ panic(fmt.Sprintf("%s: Consume() did not return %s", item.Name(), key))
|
|
|
+ }
|
|
|
+ state[key] = val
|
|
|
}
|
|
|
- state[key] = val
|
|
|
}
|
|
|
+ case runActionFork:
|
|
|
+ for i, clone := range cloneItems(branches[firstItem], len(step.Items)-1) {
|
|
|
+ branches[step.Items[i+1]] = clone
|
|
|
+ }
|
|
|
+ case runActionMerge:
|
|
|
+ merged := make([][]PipelineItem, len(step.Items))
|
|
|
+ for i, b := range step.Items {
|
|
|
+ merged[i] = branches[b]
|
|
|
+ }
|
|
|
+ mergeItems(merged)
|
|
|
+ case runActionDelete:
|
|
|
+ delete(branches, firstItem)
|
|
|
}
|
|
|
}
|
|
|
- onProgress(len(commits), len(commits))
|
|
|
+ onProgress(len(plan) + 1, progressSteps)
|
|
|
result := map[LeafPipelineItem]interface{}{}
|
|
|
- for _, item := range pipeline.items {
|
|
|
+ for _, item := range getMasterBranch(branches) {
|
|
|
if casted, ok := item.(LeafPipelineItem); ok {
|
|
|
result[casted] = casted.Finalize()
|
|
|
}
|
|
|
}
|
|
|
+ onProgress(progressSteps, progressSteps)
|
|
|
result[nil] = &CommonAnalysisResult{
|
|
|
BeginTime: commits[0].Author.When.Unix(),
|
|
|
EndTime: commits[len(commits)-1].Author.When.Unix(),
|
|
@@ -600,7 +638,7 @@ func LoadCommitsFromFile(path string, repository *git.Repository) ([]*object.Com
|
|
|
file = os.Stdin
|
|
|
}
|
|
|
scanner := bufio.NewScanner(file)
|
|
|
- commits := []*object.Commit{}
|
|
|
+ var commits []*object.Commit
|
|
|
for scanner.Scan() {
|
|
|
hash := plumbing.NewHash(scanner.Text())
|
|
|
if len(hash) != 20 {
|
|
@@ -614,485 +652,3 @@ func LoadCommitsFromFile(path string, repository *git.Repository) ([]*object.Com
|
|
|
}
|
|
|
return commits, nil
|
|
|
}
|
|
|
-
|
|
|
-const (
|
|
|
- runActionCommit = 0
|
|
|
- runActionFork = iota
|
|
|
- runActionMerge = iota
|
|
|
- runActionDelete = iota
|
|
|
-)
|
|
|
-
|
|
|
-type runAction struct {
|
|
|
- Action int
|
|
|
- Commit *object.Commit
|
|
|
- Items []int
|
|
|
-}
|
|
|
-
|
|
|
-func prepareRunPlan(commits []*object.Commit) []runAction {
|
|
|
- hashes, dag := buildDag(commits)
|
|
|
- leaveRootComponent(hashes, dag)
|
|
|
- numParents := bindNumParents(hashes, dag)
|
|
|
- mergedDag, mergedSeq := mergeDag(numParents, hashes, dag)
|
|
|
- orderNodes := bindOrderNodes(mergedDag)
|
|
|
- collapseFastForwards(orderNodes, hashes, mergedDag, dag, mergedSeq)
|
|
|
-
|
|
|
- /*fmt.Printf("digraph Hercules {\n")
|
|
|
- for i, c := range order {
|
|
|
- commit := hashes[c]
|
|
|
- fmt.Printf(" \"%s\"[label=\"[%d] %s\"]\n", commit.Hash.String(), i, commit.Hash.String()[:6])
|
|
|
- for _, child := range mergedDag[commit.Hash] {
|
|
|
- fmt.Printf(" \"%s\" -> \"%s\"\n", commit.Hash.String(), child.Hash.String())
|
|
|
- }
|
|
|
- }
|
|
|
- fmt.Printf("}\n")*/
|
|
|
-
|
|
|
- plan := generatePlan(orderNodes, numParents, hashes, mergedDag, dag, mergedSeq)
|
|
|
- plan = optimizePlan(plan)
|
|
|
- return plan
|
|
|
-}
|
|
|
-
|
|
|
-// buildDag generates the raw commit DAG and the commit hash map.
|
|
|
-func buildDag(commits []*object.Commit) (
|
|
|
- map[string]*object.Commit, map[plumbing.Hash][]*object.Commit) {
|
|
|
-
|
|
|
- hashes := map[string]*object.Commit{}
|
|
|
- for _, commit := range commits {
|
|
|
- hashes[commit.Hash.String()] = commit
|
|
|
- }
|
|
|
- dag := map[plumbing.Hash][]*object.Commit{}
|
|
|
- for _, commit := range commits {
|
|
|
- if _, exists := dag[commit.Hash]; !exists {
|
|
|
- dag[commit.Hash] = make([]*object.Commit, 0, 1)
|
|
|
- }
|
|
|
- for _, parent := range commit.ParentHashes {
|
|
|
- if _, exists := hashes[parent.String()]; !exists {
|
|
|
- continue
|
|
|
- }
|
|
|
- children := dag[parent]
|
|
|
- if children == nil {
|
|
|
- children = make([]*object.Commit, 0, 1)
|
|
|
- }
|
|
|
- dag[parent] = append(children, commit)
|
|
|
- }
|
|
|
- }
|
|
|
- return hashes, dag
|
|
|
-}
|
|
|
-
|
|
|
-// bindNumParents returns curried "numParents" function.
|
|
|
-func bindNumParents(
|
|
|
- hashes map[string]*object.Commit,
|
|
|
- dag map[plumbing.Hash][]*object.Commit) func(c *object.Commit) int {
|
|
|
- return func(c *object.Commit) int {
|
|
|
- r := 0
|
|
|
- for _, parent := range c.ParentHashes {
|
|
|
- if p, exists := hashes[parent.String()]; exists {
|
|
|
- for _, pc := range dag[p.Hash] {
|
|
|
- if pc.Hash == c.Hash {
|
|
|
- r++
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return r
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// leaveRootComponent runs connected components analysis and throws away everything
|
|
|
-// but the part which grows from the root.
|
|
|
-func leaveRootComponent(
|
|
|
- hashes map[string]*object.Commit,
|
|
|
- dag map[plumbing.Hash][]*object.Commit) {
|
|
|
-
|
|
|
- visited := map[plumbing.Hash]bool{}
|
|
|
- var sets [][]plumbing.Hash
|
|
|
- for key := range dag {
|
|
|
- if visited[key] {
|
|
|
- continue
|
|
|
- }
|
|
|
- var set []plumbing.Hash
|
|
|
- for queue := []plumbing.Hash{key}; len(queue) > 0; {
|
|
|
- head := queue[len(queue)-1]
|
|
|
- queue = queue[:len(queue)-1]
|
|
|
- if visited[head] {
|
|
|
- continue
|
|
|
- }
|
|
|
- set = append(set, head)
|
|
|
- visited[head] = true
|
|
|
- for _, c := range dag[head] {
|
|
|
- if !visited[c.Hash] {
|
|
|
- queue = append(queue, c.Hash)
|
|
|
- }
|
|
|
- }
|
|
|
- if commit, exists := hashes[head.String()]; exists {
|
|
|
- for _, p := range commit.ParentHashes {
|
|
|
- if !visited[p] {
|
|
|
- if _, exists := hashes[p.String()]; exists {
|
|
|
- queue = append(queue, p)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- sets = append(sets, set)
|
|
|
- }
|
|
|
- if len(sets) > 1 {
|
|
|
- maxlen := 0
|
|
|
- maxind := -1
|
|
|
- for i, set := range sets {
|
|
|
- if len(set) > maxlen {
|
|
|
- maxlen = len(set)
|
|
|
- maxind = i
|
|
|
- }
|
|
|
- }
|
|
|
- for i, set := range sets {
|
|
|
- if i == maxind {
|
|
|
- continue
|
|
|
- }
|
|
|
- for _, h := range set {
|
|
|
- log.Printf("warning: dropped %s from the analysis - disjoint", h.String())
|
|
|
- delete(dag, h)
|
|
|
- delete(hashes, h.String())
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// bindOrderNodes returns curried "orderNodes" function.
|
|
|
-func bindOrderNodes(mergedDag map[plumbing.Hash][]*object.Commit) func(reverse bool) []string {
|
|
|
- return func(reverse bool) []string {
|
|
|
- graph := toposort.NewGraph()
|
|
|
- keys := make([]plumbing.Hash, 0, len(mergedDag))
|
|
|
- for key := range mergedDag {
|
|
|
- keys = append(keys, key)
|
|
|
- }
|
|
|
- sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
|
|
|
- for _, key := range keys {
|
|
|
- graph.AddNode(key.String())
|
|
|
- }
|
|
|
- for _, key := range keys {
|
|
|
- children := mergedDag[key]
|
|
|
- sort.Slice(children, func(i, j int) bool {
|
|
|
- return children[i].Hash.String() < children[j].Hash.String()
|
|
|
- })
|
|
|
- for _, c := range children {
|
|
|
- graph.AddEdge(key.String(), c.Hash.String())
|
|
|
- }
|
|
|
- }
|
|
|
- order, ok := graph.Toposort()
|
|
|
- if !ok {
|
|
|
- // should never happen
|
|
|
- panic("Could not topologically sort the DAG of commits")
|
|
|
- }
|
|
|
- if reverse {
|
|
|
- // one day this must appear in the standard library...
|
|
|
- for i, j := 0, len(order)-1; i < len(order)/2; i, j = i+1, j-1 {
|
|
|
- order[i], order[j] = order[j], order[i]
|
|
|
- }
|
|
|
- }
|
|
|
- return order
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// mergeDag turns sequences of consecutive commits into single nodes.
|
|
|
-func mergeDag(
|
|
|
- numParents func(c *object.Commit) int,
|
|
|
- hashes map[string]*object.Commit,
|
|
|
- dag map[plumbing.Hash][]*object.Commit) (
|
|
|
- mergedDag, mergedSeq map[plumbing.Hash][]*object.Commit) {
|
|
|
-
|
|
|
- parentOf := func(c *object.Commit) plumbing.Hash {
|
|
|
- var parent plumbing.Hash
|
|
|
- for _, p := range c.ParentHashes {
|
|
|
- if _, exists := hashes[p.String()]; exists {
|
|
|
- if parent != plumbing.ZeroHash {
|
|
|
- // more than one parent
|
|
|
- return plumbing.ZeroHash
|
|
|
- }
|
|
|
- parent = p
|
|
|
- }
|
|
|
- }
|
|
|
- return parent
|
|
|
- }
|
|
|
- mergedDag = map[plumbing.Hash][]*object.Commit{}
|
|
|
- mergedSeq = map[plumbing.Hash][]*object.Commit{}
|
|
|
- visited := map[plumbing.Hash]bool{}
|
|
|
- for ch := range dag {
|
|
|
- c := hashes[ch.String()]
|
|
|
- if visited[c.Hash] {
|
|
|
- continue
|
|
|
- }
|
|
|
- for true {
|
|
|
- parent := parentOf(c)
|
|
|
- if parent == plumbing.ZeroHash || len(dag[parent]) != 1 {
|
|
|
- break
|
|
|
- }
|
|
|
- c = hashes[parent.String()]
|
|
|
- }
|
|
|
- head := c
|
|
|
- var seq []*object.Commit
|
|
|
- children := dag[c.Hash]
|
|
|
- for true {
|
|
|
- visited[c.Hash] = true
|
|
|
- seq = append(seq, c)
|
|
|
- if len(children) != 1 {
|
|
|
- break
|
|
|
- }
|
|
|
- c = children[0]
|
|
|
- children = dag[c.Hash]
|
|
|
- if numParents(c) != 1 {
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
- mergedSeq[head.Hash] = seq
|
|
|
- mergedDag[head.Hash] = dag[seq[len(seq)-1].Hash]
|
|
|
- }
|
|
|
- return
|
|
|
-}
|
|
|
-
|
|
|
-// collapseFastForwards removes the fast forward merges.
|
|
|
-func collapseFastForwards(
|
|
|
- orderNodes func(reverse bool) []string,
|
|
|
- hashes map[string]*object.Commit,
|
|
|
- mergedDag, dag, mergedSeq map[plumbing.Hash][]*object.Commit) {
|
|
|
-
|
|
|
- for _, strkey := range orderNodes(true) {
|
|
|
- key := hashes[strkey].Hash
|
|
|
- vals, exists := mergedDag[key]
|
|
|
- if !exists {
|
|
|
- continue
|
|
|
- }
|
|
|
- if len(vals) == 2 {
|
|
|
- grand1 := mergedDag[vals[0].Hash]
|
|
|
- grand2 := mergedDag[vals[1].Hash]
|
|
|
- if len(grand2) == 1 && vals[0].Hash == grand2[0].Hash {
|
|
|
- mergedDag[key] = mergedDag[vals[0].Hash]
|
|
|
- dag[key] = vals[1:]
|
|
|
- delete(mergedDag, vals[0].Hash)
|
|
|
- delete(mergedDag, vals[1].Hash)
|
|
|
- mergedSeq[key] = append(mergedSeq[key], mergedSeq[vals[1].Hash]...)
|
|
|
- mergedSeq[key] = append(mergedSeq[key], mergedSeq[vals[0].Hash]...)
|
|
|
- delete(mergedSeq, vals[0].Hash)
|
|
|
- delete(mergedSeq, vals[1].Hash)
|
|
|
- }
|
|
|
- // symmetric
|
|
|
- if len(grand1) == 1 && vals[1].Hash == grand1[0].Hash {
|
|
|
- mergedDag[key] = mergedDag[vals[1].Hash]
|
|
|
- dag[key] = vals[:1]
|
|
|
- delete(mergedDag, vals[0].Hash)
|
|
|
- delete(mergedDag, vals[1].Hash)
|
|
|
- mergedSeq[key] = append(mergedSeq[key], mergedSeq[vals[0].Hash]...)
|
|
|
- mergedSeq[key] = append(mergedSeq[key], mergedSeq[vals[1].Hash]...)
|
|
|
- delete(mergedSeq, vals[0].Hash)
|
|
|
- delete(mergedSeq, vals[1].Hash)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// generatePlan creates the list of actions from the commit DAG.
|
|
|
-func generatePlan(
|
|
|
- orderNodes func(reverse bool) []string,
|
|
|
- numParents func(c *object.Commit) int,
|
|
|
- hashes map[string]*object.Commit,
|
|
|
- mergedDag, dag, mergedSeq map[plumbing.Hash][]*object.Commit) []runAction {
|
|
|
-
|
|
|
- var plan []runAction
|
|
|
- branches := map[plumbing.Hash]int{}
|
|
|
- counter := 1
|
|
|
- for seqIndex, name := range orderNodes(false) {
|
|
|
- commit := hashes[name]
|
|
|
- if seqIndex == 0 {
|
|
|
- branches[commit.Hash] = 0
|
|
|
- }
|
|
|
- var branch int
|
|
|
- {
|
|
|
- var exists bool
|
|
|
- branch, exists = branches[commit.Hash]
|
|
|
- if !exists {
|
|
|
- branch = -1
|
|
|
- }
|
|
|
- }
|
|
|
- branchExists := func() bool { return branch >= 0 }
|
|
|
- appendCommit := func(c *object.Commit, branch int) {
|
|
|
- plan = append(plan, runAction{
|
|
|
- Action: runActionCommit,
|
|
|
- Commit: c,
|
|
|
- Items: []int{branch},
|
|
|
- })
|
|
|
- }
|
|
|
- appendMergeIfNeeded := func() {
|
|
|
- if numParents(commit) < 2 {
|
|
|
- return
|
|
|
- }
|
|
|
- // merge after the merge commit (the first in the sequence)
|
|
|
- var items []int
|
|
|
- minBranch := 1 << 31
|
|
|
- for _, parent := range commit.ParentHashes {
|
|
|
- if _, exists := hashes[parent.String()]; exists {
|
|
|
- parentBranch := branches[parent]
|
|
|
- if len(dag[parent]) == 1 && minBranch > parentBranch {
|
|
|
- minBranch = parentBranch
|
|
|
- }
|
|
|
- items = append(items, parentBranch)
|
|
|
- if parentBranch != branch {
|
|
|
- appendCommit(commit, parentBranch)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- if minBranch < 1 << 31 {
|
|
|
- branch = minBranch
|
|
|
- branches[commit.Hash] = minBranch
|
|
|
- } else if !branchExists() {
|
|
|
- panic("!branchExists()")
|
|
|
- }
|
|
|
- plan = append(plan, runAction{
|
|
|
- Action: runActionMerge,
|
|
|
- Commit: nil,
|
|
|
- Items: items,
|
|
|
- })
|
|
|
- }
|
|
|
- if subseq, exists := mergedSeq[commit.Hash]; exists {
|
|
|
- for subseqIndex, offspring := range subseq {
|
|
|
- if branchExists() {
|
|
|
- appendCommit(offspring, branch)
|
|
|
- }
|
|
|
- if subseqIndex == 0 {
|
|
|
- appendMergeIfNeeded()
|
|
|
- }
|
|
|
- }
|
|
|
- branches[subseq[len(subseq)-1].Hash] = branch
|
|
|
- }
|
|
|
- if len(mergedDag[commit.Hash]) > 1 {
|
|
|
- branches[mergedDag[commit.Hash][0].Hash] = branch
|
|
|
- children := []int{branch}
|
|
|
- for i, child := range mergedDag[commit.Hash] {
|
|
|
- if i > 0 {
|
|
|
- branches[child.Hash] = counter
|
|
|
- children = append(children, counter)
|
|
|
- counter++
|
|
|
- }
|
|
|
- }
|
|
|
- plan = append(plan, runAction{
|
|
|
- Action: runActionFork,
|
|
|
- Commit: nil,
|
|
|
- Items: children,
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
- return plan
|
|
|
-}
|
|
|
-
|
|
|
-// optimizePlan removes "dead" nodes and inserts `runActionDelete` disposal steps.
|
|
|
-//
|
|
|
-// | *
|
|
|
-// * /
|
|
|
-// |\/
|
|
|
-// |/
|
|
|
-// *
|
|
|
-//
|
|
|
-func optimizePlan(plan []runAction) []runAction {
|
|
|
- // lives maps branch index to the number of commits in that branch
|
|
|
- lives := map[int]int{}
|
|
|
- // lastMentioned maps branch index to the index inside `plan` when that branch was last used
|
|
|
- lastMentioned := map[int]int{}
|
|
|
- for i, p := range plan {
|
|
|
- firstItem := p.Items[0]
|
|
|
- switch p.Action {
|
|
|
- case runActionCommit:
|
|
|
- lives[firstItem]++
|
|
|
- lastMentioned[firstItem] = i
|
|
|
- case runActionFork:
|
|
|
- lastMentioned[firstItem] = i
|
|
|
- case runActionMerge:
|
|
|
- for _, item := range p.Items {
|
|
|
- lastMentioned[item] = i
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- branchesToDelete := map[int]bool{}
|
|
|
- for key, life := range lives {
|
|
|
- if life == 1 {
|
|
|
- branchesToDelete[key] = true
|
|
|
- delete(lastMentioned, key)
|
|
|
- }
|
|
|
- }
|
|
|
- var optimizedPlan []runAction
|
|
|
- lastMentionedArr := make([][2]int, 0, len(lastMentioned) + 1)
|
|
|
- for key, val := range lastMentioned {
|
|
|
- if val != len(plan) - 1 {
|
|
|
- lastMentionedArr = append(lastMentionedArr, [2]int{val, key})
|
|
|
- }
|
|
|
- }
|
|
|
- if len(lastMentionedArr) == 0 && len(branchesToDelete) == 0 {
|
|
|
- // early return - we have nothing to optimize
|
|
|
- return plan
|
|
|
- }
|
|
|
- sort.Slice(lastMentionedArr, func(i, j int) bool {
|
|
|
- return lastMentionedArr[i][0] < lastMentionedArr[j][0]
|
|
|
- })
|
|
|
- lastMentionedArr = append(lastMentionedArr, [2]int{len(plan)-1, -1})
|
|
|
- prevpi := -1
|
|
|
- for _, pair := range lastMentionedArr {
|
|
|
- for pi := prevpi + 1; pi <= pair[0]; pi++ {
|
|
|
- p := plan[pi]
|
|
|
- switch p.Action {
|
|
|
- case runActionCommit:
|
|
|
- if !branchesToDelete[p.Items[0]] {
|
|
|
- optimizedPlan = append(optimizedPlan, p)
|
|
|
- }
|
|
|
- case runActionFork:
|
|
|
- var newBranches []int
|
|
|
- for _, b := range p.Items {
|
|
|
- if !branchesToDelete[b] {
|
|
|
- newBranches = append(newBranches, b)
|
|
|
- }
|
|
|
- }
|
|
|
- if len(newBranches) > 1 {
|
|
|
- optimizedPlan = append(optimizedPlan, runAction{
|
|
|
- Action: runActionFork,
|
|
|
- Commit: p.Commit,
|
|
|
- Items: newBranches,
|
|
|
- })
|
|
|
- }
|
|
|
- case runActionMerge:
|
|
|
- var newBranches []int
|
|
|
- for _, b := range p.Items {
|
|
|
- if !branchesToDelete[b] {
|
|
|
- newBranches = append(newBranches, b)
|
|
|
- }
|
|
|
- }
|
|
|
- if len(newBranches) > 1 {
|
|
|
- optimizedPlan = append(optimizedPlan, runAction{
|
|
|
- Action: runActionMerge,
|
|
|
- Commit: p.Commit,
|
|
|
- Items: newBranches,
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- if pair[1] >= 0 {
|
|
|
- prevpi = pair[0]
|
|
|
- optimizedPlan = append(optimizedPlan, runAction{
|
|
|
- Action: runActionDelete,
|
|
|
- Commit: nil,
|
|
|
- Items: []int{pair[1]},
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
- // single commit can be detected as redundant
|
|
|
- if len(optimizedPlan) > 0 {
|
|
|
- return optimizedPlan
|
|
|
- }
|
|
|
- return plan
|
|
|
- // TODO(vmarkovtsev): there can be also duplicate redundant merges, e.g.
|
|
|
- /*
|
|
|
- 0 4e34f03d829fbacb71cde0e010de87ea945dc69a [3]
|
|
|
- 0 4e34f03d829fbacb71cde0e010de87ea945dc69a [12]
|
|
|
- 2 [3 12]
|
|
|
- 0 06716c2b39422938b77ddafa4d5c39bb9e4476da [3]
|
|
|
- 0 06716c2b39422938b77ddafa4d5c39bb9e4476da [12]
|
|
|
- 2 [3 12]
|
|
|
- 0 1219c7bf9e0e1a93459a052ab8b351bfc379dc19 [12]
|
|
|
- */
|
|
|
-}
|