Skip to content

Commit 3a33a3a

Browse files
committed
fix: add remote branches to repo graph
1 parent 219de3c commit 3a33a3a

8 files changed

Lines changed: 435 additions & 93 deletions

File tree

dist/index.js

Lines changed: 283 additions & 46 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@actions/exec": "^1.1.1",
2222
"@actions/github": "^6.0.0",
2323
"graphology": "^0.25.4",
24+
"graphology-dag": "^0.4.1",
2425
"graphology-traversal": "^0.3.1",
2526
"remark": "^15.0.1",
2627
"remark-gfm": "^4.0.0",

src/inputs.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ describe('getCurrentPullRequest', () => {
136136
number: 100,
137137
base: { ref: 'main' },
138138
head: { ref: 'feat/git-town-action' },
139+
state: 'open',
139140
},
140141
},
141142
} as unknown as typeof github.context
@@ -144,8 +145,9 @@ describe('getCurrentPullRequest', () => {
144145

145146
expect(currentPullRequest).toStrictEqual({
146147
number: 100,
147-
baseRefName: 'main',
148-
headRefName: 'feat/git-town-action',
148+
base: { ref: 'main' },
149+
head: { ref: 'feat/git-town-action' },
150+
state: 'open',
149151
})
150152
})
151153

src/inputs.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -103,31 +103,21 @@ export const inputs = {
103103

104104
getCurrentPullRequest(context: typeof github.context) {
105105
try {
106-
const pullRequest:
107-
| {
108-
number: number
109-
base?: { ref?: string }
110-
head?: { ref?: string }
111-
}
112-
| undefined = context.payload.pull_request
106+
const pullRequest = pullRequestSchema.parse(context.payload.pull_request)
113107

114108
core.startGroup('Inputs: Current pull request')
115109
core.info(JSON.stringify(pullRequest))
116110
core.endGroup()
117111

118-
return pullRequestSchema.parse({
119-
number: pullRequest?.number,
120-
baseRefName: pullRequest?.base?.ref,
121-
headRefName: pullRequest?.head?.ref,
122-
})
112+
return pullRequest
123113
} catch (error) {
124114
core.setFailed(`Unable to determine current pull request from action payload`)
125115
throw error
126116
}
127117
},
128118

129119
async getPullRequests(octokit: Octokit, context: typeof github.context) {
130-
const openPullRequests = await octokit.paginate(
120+
const pullRequests = await octokit.paginate(
131121
'GET /repos/{owner}/{repo}/pulls',
132122
{
133123
...context.repo,
@@ -138,19 +128,21 @@ export const inputs = {
138128
response.data.map(
139129
(item): PullRequest => ({
140130
number: item.number,
141-
baseRefName: item.base.ref,
142-
headRefName: item.head.ref,
131+
base: { ref: item.base.ref },
132+
head: { ref: item.head.ref },
143133
body: item.body ?? undefined,
134+
state: item.state,
144135
})
145136
)
146137
)
138+
pullRequests.sort((a, b) => b.number - a.number)
147139

148140
core.startGroup('Inputs: Pull requests')
149141
core.info(
150-
JSON.stringify(openPullRequests.map(({ body: _, ...pullRequest }) => pullRequest))
142+
JSON.stringify(pullRequests.map(({ body: _, ...pullRequest }) => pullRequest))
151143
)
152144
core.endGroup()
153145

154-
return openPullRequests
146+
return pullRequests
155147
},
156148
}

src/main.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, beforeEach, expect, vi } from 'vitest'
2-
import { updateDescription } from './main'
2+
import { main, updateDescription } from './main'
3+
import type { Octokit } from './types'
34

45
beforeEach(() => {
56
vi.unstubAllEnvs()
@@ -52,3 +53,46 @@ describe('updateDescription', () => {
5253
expect(actual).toEqual(expected)
5354
})
5455
})
56+
57+
describe('main', () => {
58+
it('should work', async () => {
59+
await main({
60+
octokit: {} as unknown as Octokit,
61+
currentPullRequest: {
62+
number: 361,
63+
head: {
64+
ref: 'test-branch',
65+
},
66+
base: {
67+
ref: 'document-setup',
68+
},
69+
state: 'open',
70+
},
71+
pullRequests: [
72+
// {
73+
// number: 360,
74+
// head: {
75+
// ref: 'document-setup',
76+
// },
77+
// base: {
78+
// ref: 'main',
79+
// },
80+
// state: 'open',
81+
// },
82+
{
83+
number: 361,
84+
head: {
85+
ref: 'test-branch',
86+
},
87+
base: {
88+
ref: 'document-setup',
89+
},
90+
state: 'open',
91+
},
92+
],
93+
mainBranch: 'main',
94+
perennialBranches: [],
95+
skipSingleStacks: false,
96+
})
97+
})
98+
})

src/main.ts

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as core from '@actions/core'
22
import * 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'
56
import type { PullRequest, Context, StackNodeAttributes } from './types'
67
import { 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

src/types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ export type Octokit = ReturnType<typeof getOctokit>
66

77
export const pullRequestSchema = object({
88
number: number(),
9-
baseRefName: string(),
10-
headRefName: string(),
9+
base: object({
10+
ref: string(),
11+
}),
12+
head: object({
13+
ref: string(),
14+
}),
15+
state: string(),
1116
body: string().optional(),
1217
})
1318
export type PullRequest = InferType<typeof pullRequestSchema>
@@ -22,6 +27,10 @@ export type Context = {
2227
}
2328

2429
export type StackNode =
30+
| {
31+
type: 'orphan-branch'
32+
ref: string
33+
}
2534
| {
2635
type: 'perennial'
2736
ref: string

0 commit comments

Comments
 (0)