-
Notifications
You must be signed in to change notification settings - Fork 34
/
edit-github-blob.ts
153 lines (140 loc) · 3.78 KB
/
edit-github-blob.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import type { API } from './api'
import { basename } from 'path'
// avoid importing @octokit/request-error to not have to keep it in sync in package.json
interface RequestError {
status: number
}
async function retry<T>(
times: number,
delay: number,
fn: () => Promise<T>
): Promise<T> {
try {
return await fn()
} catch (err) {
if (times > 0) {
return new Promise((resolve): void => {
setTimeout(() => {
resolve(retry(times - 1, delay, fn))
}, delay)
})
}
throw err
}
}
export type Options = {
owner: string
repo: string
filePath: string
branch?: string
apiClient: API
replace: (oldContent: string) => string
commitMessage?: string
pushTo?: {
owner: string
repo: string
}
makePR?: boolean
}
export default async function (params: Options): Promise<string> {
const baseRepo = {
owner: params.owner,
repo: params.repo,
}
let headRepo = params.pushTo == null ? baseRepo : params.pushTo
const filePath = params.filePath
const api = params.apiClient.rest
const repoRes = await api.repos.get(baseRepo)
const makeFork =
params.pushTo == null &&
(repoRes.data.permissions == null || !repoRes.data.permissions.push)
const inFork =
makeFork ||
`${baseRepo.owner}/${baseRepo.repo}`.toLowerCase() !=
`${headRepo.owner}/${headRepo.repo}`.toLowerCase()
const baseBranch = params.branch ? params.branch : repoRes.data.default_branch
let headBranch = baseBranch
const branchRes = await api.repos.getBranch({
...baseRepo,
branch: baseBranch,
})
const needsBranch =
inFork || branchRes.data.protected || params.makePR === true
if (makeFork) {
const res = await Promise.all([
api.repos.createFork(baseRepo),
api.users.getAuthenticated(),
])
headRepo = {
owner: res[1].data.login,
repo: baseRepo.repo,
}
}
if (needsBranch) {
const timestamp = Math.round(Date.now() / 1000)
headBranch = `update-${basename(filePath)}-${timestamp}`
if (inFork) {
try {
await api.repos.mergeUpstream({
...headRepo,
branch: repoRes.data.default_branch,
})
} catch (err) {
if ((err as RequestError).status === 409) {
// ignore
} else {
throw err
}
}
}
await retry(makeFork ? 6 : 0, 5000, async () => {
await api.git.createRef({
...headRepo,
ref: `refs/heads/${headBranch}`,
sha: branchRes.data.commit.sha,
})
})
}
const fileRes = await api.repos.getContent({
...headRepo,
path: filePath,
ref: headBranch,
})
const fileData = fileRes.data
if (Array.isArray(fileData)) {
throw new Error(`expected '${filePath}' is a file, got a directory`)
}
const content = ('content' in fileData && fileData.content) || ''
const contentBuf = Buffer.from(content, 'base64')
const oldContent = contentBuf.toString('utf8')
const newContent = params.replace(oldContent)
if (newContent == oldContent) {
throw new Error('no replacements ocurred')
}
const commitMessage = params.commitMessage
? params.commitMessage
: `Update ${filePath}`
const commitRes = await api.repos.createOrUpdateFileContents({
...headRepo,
path: filePath,
message: commitMessage,
content: Buffer.from(newContent).toString('base64'),
sha: fileData.sha,
branch: headBranch,
})
if (needsBranch && params.makePR !== false) {
const parts = commitMessage.split('\n\n')
const title = parts[0]
const body = parts.slice(1).join('\n\n')
const prRes = await api.pulls.create({
...baseRepo,
base: baseBranch,
head: `${headRepo.owner}:${headBranch}`,
title,
body,
})
return prRes.data.html_url
} else {
return commitRes.data.commit.html_url || ''
}
}