11import * as core from '@actions/core'
22import * as github from '@actions/github'
3- import { MultiDirectedGraph } from 'graphology'
4- import { bfsFromNode , dfs , dfsFromNode } from 'graphology-traversal'
3+ import { DirectedGraph } from 'graphology'
4+ import { bfsFromNode , dfsFromNode } from 'graphology-traversal'
5+ import { topologicalSort } from 'graphology-dag'
56import type { PullRequest , Context , StackNodeAttributes } from './types'
67import { remark } from './remark'
78
@@ -13,65 +14,104 @@ export async function main({
1314 perennialBranches,
1415 skipSingleStacks,
1516} : Context ) {
16- const repoGraph = new MultiDirectedGraph < StackNodeAttributes > ( )
17+ const repoGraph = new DirectedGraph < StackNodeAttributes > ( )
1718
18- repoGraph . addNode ( mainBranch , {
19+ repoGraph . mergeNode ( mainBranch , {
1920 type : 'perennial' ,
2021 ref : mainBranch ,
2122 } )
2223
2324 perennialBranches . forEach ( ( perennialBranch ) => {
24- repoGraph . addNode ( perennialBranch , {
25+ repoGraph . mergeNode ( perennialBranch , {
2526 type : 'perennial' ,
2627 ref : perennialBranch ,
2728 } )
2829 } )
2930
30- pullRequests . forEach ( ( pullRequest ) => {
31- repoGraph . addNode ( pullRequest . headRefName , {
31+ const openPullRequests = pullRequests . filter (
32+ ( pullRequest ) => pullRequest . state === 'open'
33+ )
34+
35+ openPullRequests . forEach ( ( openPullRequest ) => {
36+ repoGraph . mergeNode ( openPullRequest . head . ref , {
3237 type : 'pull-request' ,
33- ...pullRequest ,
38+ ...openPullRequest ,
3439 } )
3540 } )
3641
37- pullRequests . forEach ( ( pullRequest ) => {
38- repoGraph . addDirectedEdge ( pullRequest . baseRefName , pullRequest . headRefName )
42+ openPullRequests . forEach ( ( openPullRequest ) => {
43+ const hasExistingBasePullRequest = repoGraph . hasNode ( openPullRequest . base . ref )
44+ if ( hasExistingBasePullRequest ) {
45+ repoGraph . mergeDirectedEdge ( openPullRequest . base . ref , openPullRequest . head . ref )
46+
47+ return
48+ }
49+
50+ const basePullRequest = pullRequests . find (
51+ ( basePullRequest ) => basePullRequest . head . ref === openPullRequest . base . ref
52+ )
53+ if ( basePullRequest ?. state === 'closed' ) {
54+ repoGraph . mergeNode ( openPullRequest . base . ref , {
55+ type : 'pull-request' ,
56+ ...basePullRequest ,
57+ } )
58+ repoGraph . mergeDirectedEdge ( openPullRequest . base . ref , openPullRequest . head . ref )
59+
60+ return
61+ }
62+
63+ repoGraph . mergeNode ( openPullRequest . base . ref , {
64+ type : 'orphan-branch' ,
65+ ref : openPullRequest . base . ref ,
66+ } )
67+ repoGraph . mergeDirectedEdge ( openPullRequest . base . ref , openPullRequest . head . ref )
3968 } )
4069
70+ const terminatingRefs = [ mainBranch , ...perennialBranches ]
71+
4172 const getStackGraph = ( pullRequest : PullRequest ) => {
42- const stackGraph = repoGraph . copy ( ) as MultiDirectedGraph < StackNodeAttributes >
43- stackGraph . setNodeAttribute ( pullRequest . headRefName , 'isCurrent' , true )
73+ const stackGraph = repoGraph . copy ( ) as DirectedGraph < StackNodeAttributes >
74+ stackGraph . setNodeAttribute ( pullRequest . head . ref , 'isCurrent' , true )
4475
4576 bfsFromNode (
4677 stackGraph ,
47- pullRequest . headRefName ,
78+ pullRequest . head . ref ,
4879 ( ref , attributes ) => {
4980 stackGraph . setNodeAttribute ( ref , 'shouldPrint' , true )
50- return attributes . type === 'perennial'
81+ return attributes . type === 'perennial' || attributes . type === 'orphan-branch'
5182 } ,
52- {
53- mode : 'inbound' ,
54- }
83+ { mode : 'inbound' }
5584 )
5685
5786 dfsFromNode (
5887 stackGraph ,
59- pullRequest . headRefName ,
88+ pullRequest . head . ref ,
6089 ( ref ) => {
6190 stackGraph . setNodeAttribute ( ref , 'shouldPrint' , true )
6291 } ,
6392 { mode : 'outbound' }
6493 )
6594
66- return stackGraph
95+ stackGraph . forEachNode ( ( ref , stackNode ) => {
96+ if ( ! stackNode . shouldPrint ) {
97+ stackGraph . dropNode ( ref )
98+ }
99+ } )
100+
101+ return DirectedGraph . from ( stackGraph . toJSON ( ) )
67102 }
68103
69- const getOutput = ( graph : MultiDirectedGraph < StackNodeAttributes > ) => {
104+ const getOutput = ( graph : DirectedGraph < StackNodeAttributes > ) => {
70105 const lines : string [ ] = [ ]
71- const terminatingRefs = [ mainBranch , ...perennialBranches ]
72106
73- dfs (
107+ // `dfs` is bugged and doesn't traverse in topological order.
108+ // `dfsFromNode` does, so we'll do the topological sort ourselves
109+ // start traversal from the root.
110+ const rootRef = topologicalSort ( graph ) [ 0 ]
111+
112+ dfsFromNode (
74113 graph ,
114+ rootRef ,
75115 ( _ , stackNode , depth ) => {
76116 if ( ! stackNode . shouldPrint ) return
77117
@@ -80,6 +120,10 @@ export async function main({
80120
81121 let line = indentation
82122
123+ if ( stackNode . type === 'orphan-branch' ) {
124+ line += `- \`${ stackNode . ref } \` - :warning: No PR associated with branch`
125+ }
126+
83127 if ( stackNode . type === 'perennial' && terminatingRefs . includes ( stackNode . ref ) ) {
84128 line += `- \`${ stackNode . ref } \``
85129 }
@@ -100,12 +144,10 @@ export async function main({
100144 return lines . join ( '\n' )
101145 }
102146
103- const jobs : Array < ( ) => Promise < void > > = [ ]
104-
105147 const stackGraph = getStackGraph ( currentPullRequest )
106148
107149 const shouldSkip = ( ) => {
108- const neighbors = stackGraph . neighbors ( currentPullRequest . headRefName )
150+ const neighbors = stackGraph . neighbors ( currentPullRequest . head . ref )
109151 const allPerennialBranches = stackGraph . filterNodes (
110152 ( _ , nodeAttributes ) => nodeAttributes . type === 'perennial'
111153 )
@@ -121,6 +163,8 @@ export async function main({
121163 return
122164 }
123165
166+ const jobs : Array < ( ) => Promise < void > > = [ ]
167+
124168 stackGraph . forEachNode ( ( _ , stackNode ) => {
125169 if ( stackNode . type !== 'pull-request' || ! stackNode . shouldPrint ) {
126170 return
0 commit comments