release-please 17.4.0 → 17.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/bin/release-please.js +39 -10
- package/build/src/bootstrapper.d.ts +2 -2
- package/build/src/changelog-notes/default.js +9 -1
- package/build/src/changelog-notes/github.d.ts +2 -2
- package/build/src/changelog-notes.d.ts +1 -0
- package/build/src/commit.d.ts +6 -0
- package/build/src/factories/changelog-notes-factory.d.ts +2 -2
- package/build/src/factories/plugin-factory.d.ts +2 -2
- package/build/src/factories/versioning-strategy-factory.d.ts +2 -2
- package/build/src/factory.d.ts +2 -2
- package/build/src/github-api.d.ts +260 -0
- package/build/src/github-api.js +701 -0
- package/build/src/github.d.ts +21 -171
- package/build/src/github.js +154 -687
- package/build/src/index.d.ts +2 -2
- package/build/src/index.js +1 -1
- package/build/src/local-github.d.ts +271 -0
- package/build/src/local-github.js +776 -0
- package/build/src/manifest.d.ts +7 -5
- package/build/src/manifest.js +28 -26
- package/build/src/plugin.d.ts +3 -3
- package/build/src/plugins/group-priority.d.ts +2 -2
- package/build/src/plugins/linked-versions.d.ts +2 -2
- package/build/src/plugins/maven-workspace.d.ts +2 -2
- package/build/src/plugins/merge.d.ts +2 -2
- package/build/src/plugins/node-workspace.d.ts +2 -2
- package/build/src/plugins/sentence-case.d.ts +2 -2
- package/build/src/plugins/workspace.d.ts +2 -2
- package/build/src/scm.d.ts +80 -0
- package/build/src/scm.js +16 -0
- package/build/src/strategies/base.d.ts +5 -3
- package/build/src/strategies/base.js +2 -0
- package/build/src/util/pull-request-overflow-handler.d.ts +2 -2
- package/package.json +3 -3
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright 2026 Google LLC
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
//
|
|
8
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
//
|
|
10
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
// See the License for the specific language governing permissions and
|
|
14
|
+
// limitations under the License.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.LocalGitHub = void 0;
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
const os = require("os");
|
|
20
|
+
const child_process = require("child_process");
|
|
21
|
+
const util = require("util");
|
|
22
|
+
const readline = require("readline");
|
|
23
|
+
const execFile = util.promisify(child_process.execFile);
|
|
24
|
+
const mkdtemp = fs.promises.mkdtemp;
|
|
25
|
+
const errors_1 = require("./errors");
|
|
26
|
+
const manifest_1 = require("./manifest");
|
|
27
|
+
const git_file_utils_1 = require("@google-automations/git-file-utils");
|
|
28
|
+
const composite_1 = require("./updaters/composite");
|
|
29
|
+
const github_api_1 = require("./github-api");
|
|
30
|
+
const logger_1 = require("./util/logger");
|
|
31
|
+
/**
|
|
32
|
+
* LocalGitHub implements the Scm interface using a local git clone
|
|
33
|
+
* where possible, and falling back to the GitHub API for other operations.
|
|
34
|
+
*/
|
|
35
|
+
class LocalGitHub {
|
|
36
|
+
constructor(repository, gitHubApi, cloneDir, options) {
|
|
37
|
+
var _a;
|
|
38
|
+
this.repository = repository;
|
|
39
|
+
this.gitHubApi = gitHubApi;
|
|
40
|
+
this.cloneDir = cloneDir;
|
|
41
|
+
this.logger = (_a = options === null || options === void 0 ? void 0 : options.logger) !== null && _a !== void 0 ? _a : logger_1.logger;
|
|
42
|
+
}
|
|
43
|
+
static async create(options) {
|
|
44
|
+
var _a;
|
|
45
|
+
const gitHubApi = await github_api_1.GitHubApi.create(options);
|
|
46
|
+
const logger = (_a = options.logger) !== null && _a !== void 0 ? _a : logger_1.logger;
|
|
47
|
+
let repoDir;
|
|
48
|
+
if (options.localRepoPath) {
|
|
49
|
+
repoDir = options.localRepoPath;
|
|
50
|
+
let isGitRepo = false;
|
|
51
|
+
try {
|
|
52
|
+
await execFile('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
53
|
+
cwd: repoDir,
|
|
54
|
+
});
|
|
55
|
+
isGitRepo = true;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
isGitRepo = false;
|
|
59
|
+
}
|
|
60
|
+
if (!isGitRepo) {
|
|
61
|
+
logger.info(`Path ${repoDir} is not a git clone. Cloning repository...`);
|
|
62
|
+
const url = `https://github.com/${gitHubApi.repository.owner}/${gitHubApi.repository.repo}.git`;
|
|
63
|
+
const args = ['clone', '--', url, repoDir];
|
|
64
|
+
if (options.cloneDepth) {
|
|
65
|
+
args.splice(1, 0, '--depth', options.cloneDepth.toString());
|
|
66
|
+
}
|
|
67
|
+
logger.debug(`Executing: git ${args.join(' ')}`);
|
|
68
|
+
await execFile('git', args);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
logger.info(`Using existing local repository at ${repoDir}...`);
|
|
72
|
+
}
|
|
73
|
+
const branch = gitHubApi.repository.defaultBranch;
|
|
74
|
+
logger.debug('Executing: git fetch origin');
|
|
75
|
+
await execFile('git', ['fetch', 'origin'], { cwd: repoDir });
|
|
76
|
+
logger.debug(`Executing: git checkout ${branch}`);
|
|
77
|
+
await execFile('git', ['checkout', branch], { cwd: repoDir });
|
|
78
|
+
logger.debug(`Executing: git reset --hard origin/${branch}`);
|
|
79
|
+
await execFile('git', ['reset', '--hard', `origin/${branch}`], {
|
|
80
|
+
cwd: repoDir,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'release-please-'));
|
|
85
|
+
logger.info(`Cloning repository to ${tempDir}...`);
|
|
86
|
+
const url = `https://github.com/${gitHubApi.repository.owner}/${gitHubApi.repository.repo}.git`;
|
|
87
|
+
const args = ['clone', '--', url, tempDir];
|
|
88
|
+
if (options.cloneDepth) {
|
|
89
|
+
args.splice(1, 0, '--depth', options.cloneDepth.toString());
|
|
90
|
+
}
|
|
91
|
+
logger.debug(`Executing: git ${args.join(' ')}`);
|
|
92
|
+
await execFile('git', args);
|
|
93
|
+
repoDir = tempDir;
|
|
94
|
+
}
|
|
95
|
+
return new LocalGitHub(gitHubApi.repository, gitHubApi, repoDir, {
|
|
96
|
+
logger: options.logger,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Fetch the contents of a file from the configured branch
|
|
101
|
+
*
|
|
102
|
+
* @param {string} path The path to the file in the repository
|
|
103
|
+
* @returns {GitHubFileContents}
|
|
104
|
+
* @throws {GitHubAPIError} on other API errors
|
|
105
|
+
*/
|
|
106
|
+
async getFileContents(path) {
|
|
107
|
+
return await this.getFileContentsOnBranch(path, this.repository.defaultBranch);
|
|
108
|
+
}
|
|
109
|
+
async execGitStream(args, callback) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const child = child_process.spawn('git', args, { cwd: this.cloneDir });
|
|
112
|
+
let stderr = '';
|
|
113
|
+
child.stderr.on('data', data => {
|
|
114
|
+
stderr += data;
|
|
115
|
+
});
|
|
116
|
+
const rl = readline.createInterface({
|
|
117
|
+
input: child.stdout,
|
|
118
|
+
crlfDelay: Infinity,
|
|
119
|
+
});
|
|
120
|
+
rl.on('line', callback);
|
|
121
|
+
child.on('close', code => {
|
|
122
|
+
if (code !== 0) {
|
|
123
|
+
reject(new Error(`Command failed ${code}: ${stderr}`));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
resolve();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async ensureRef(ref) {
|
|
132
|
+
try {
|
|
133
|
+
await execFile('git', ['rev-parse', '--verify', ref], {
|
|
134
|
+
cwd: this.cloneDir,
|
|
135
|
+
});
|
|
136
|
+
return ref;
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
this.logger.debug(`Ref ${ref} not found locally, trying to fetch from origin...`);
|
|
140
|
+
try {
|
|
141
|
+
await execFile('git', ['fetch', 'origin', '--', ref], {
|
|
142
|
+
cwd: this.cloneDir,
|
|
143
|
+
});
|
|
144
|
+
return 'FETCH_HEAD';
|
|
145
|
+
}
|
|
146
|
+
catch (fetchErr) {
|
|
147
|
+
throw err; // Throw original error if fetch fails
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Fetch the contents of a file
|
|
153
|
+
*
|
|
154
|
+
* @param {string} path The path to the file in the repository
|
|
155
|
+
* @param {string} branch The branch to fetch from
|
|
156
|
+
* @returns {GitHubFileContents}
|
|
157
|
+
* @throws {FileNotFoundError} if the file cannot be found
|
|
158
|
+
* @throws {GitHubAPIError} on other API errors
|
|
159
|
+
*/
|
|
160
|
+
async getFileContentsOnBranch(path, branch) {
|
|
161
|
+
this.logger.debug(`Fetching file contents for file ${path} on branch ${branch}`);
|
|
162
|
+
const ref = await this.ensureRef(branch);
|
|
163
|
+
const lsTreeResult = await execFile('git', ['ls-tree', ref, path], {
|
|
164
|
+
cwd: this.cloneDir,
|
|
165
|
+
});
|
|
166
|
+
if (!lsTreeResult.stdout.trim()) {
|
|
167
|
+
throw new errors_1.FileNotFoundError(path);
|
|
168
|
+
}
|
|
169
|
+
const [info] = lsTreeResult.stdout.split('\t');
|
|
170
|
+
const [mode, , sha] = info.split(' ');
|
|
171
|
+
const { stdout } = await execFile('git', ['show', `${ref}:${path}`], {
|
|
172
|
+
cwd: this.cloneDir,
|
|
173
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
content: Buffer.from(stdout).toString('base64'),
|
|
177
|
+
parsedContent: stdout,
|
|
178
|
+
sha,
|
|
179
|
+
mode,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async getFileJson(path, branch) {
|
|
183
|
+
const content = await this.getFileContentsOnBranch(path, branch);
|
|
184
|
+
return JSON.parse(content.parsedContent);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Returns a list of paths to all files with a given name.
|
|
188
|
+
*
|
|
189
|
+
* If a prefix is specified, only return paths that match
|
|
190
|
+
* the provided prefix.
|
|
191
|
+
*
|
|
192
|
+
* @param filename The name of the file to find
|
|
193
|
+
* @param prefix Optional path prefix used to filter results
|
|
194
|
+
* @returns {string[]} List of file paths
|
|
195
|
+
* @throws {GitHubAPIError} on an API error
|
|
196
|
+
*/
|
|
197
|
+
async findFilesByFilename(filename, prefix) {
|
|
198
|
+
return this.findFilesByFilenameAndRef(filename, this.repository.defaultBranch, prefix);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Returns a list of paths to all files with a given name.
|
|
202
|
+
*
|
|
203
|
+
* If a prefix is specified, only return paths that match
|
|
204
|
+
* the provided prefix.
|
|
205
|
+
*
|
|
206
|
+
* @param filename The name of the file to find
|
|
207
|
+
* @param ref Git reference to search files in
|
|
208
|
+
* @param prefix Optional path prefix used to filter results
|
|
209
|
+
* @throws {GitHubAPIError} on an API error
|
|
210
|
+
*/
|
|
211
|
+
async findFilesByFilenameAndRef(filename, ref, prefix) {
|
|
212
|
+
this.logger.debug(`Looking in local clone for file ${filename} with ref ${ref} and prefix '${prefix}'`);
|
|
213
|
+
let normalizedPrefix = prefix
|
|
214
|
+
? prefix.replace(/^[/\\]/, '').replace(/[/\\]$/, '')
|
|
215
|
+
: '';
|
|
216
|
+
if (normalizedPrefix === manifest_1.ROOT_PROJECT_PATH) {
|
|
217
|
+
normalizedPrefix = '';
|
|
218
|
+
}
|
|
219
|
+
const treePath = normalizedPrefix ? `${normalizedPrefix}/` : '.';
|
|
220
|
+
const resolvedRef = await this.ensureRef(ref);
|
|
221
|
+
this.logger.trace(`Executing stream: git ls-tree -r --name-only ${resolvedRef} ${treePath}`);
|
|
222
|
+
const matchedPaths = [];
|
|
223
|
+
await this.execGitStream(['ls-tree', '-r', '--name-only', resolvedRef, treePath], line => {
|
|
224
|
+
const trimmed = line.trim();
|
|
225
|
+
if (trimmed && path.posix.basename(trimmed) === filename) {
|
|
226
|
+
matchedPaths.push(trimmed);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
if (normalizedPrefix) {
|
|
230
|
+
return matchedPaths
|
|
231
|
+
.map(p => {
|
|
232
|
+
if (p === normalizedPrefix)
|
|
233
|
+
return '';
|
|
234
|
+
if (p.startsWith(`${normalizedPrefix}/`)) {
|
|
235
|
+
return p.slice(normalizedPrefix.length + 1);
|
|
236
|
+
}
|
|
237
|
+
return p;
|
|
238
|
+
})
|
|
239
|
+
.filter(p => p !== '');
|
|
240
|
+
}
|
|
241
|
+
return matchedPaths;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Returns a list of paths to all files matching a glob pattern.
|
|
245
|
+
*
|
|
246
|
+
* If a prefix is specified, only return paths that match
|
|
247
|
+
* the provided prefix.
|
|
248
|
+
*
|
|
249
|
+
* @param glob The glob to match
|
|
250
|
+
* @param prefix Optional path prefix used to filter results
|
|
251
|
+
* @returns {string[]} List of file paths
|
|
252
|
+
* @throws {GitHubAPIError} on an API error
|
|
253
|
+
*/
|
|
254
|
+
async findFilesByGlob(glob, prefix) {
|
|
255
|
+
return this.findFilesByGlobAndRef(glob, this.repository.defaultBranch, prefix);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Returns a list of paths to all files matching a glob pattern.
|
|
259
|
+
*
|
|
260
|
+
* If a prefix is specified, only return paths that match
|
|
261
|
+
* the provided prefix.
|
|
262
|
+
*
|
|
263
|
+
* @param glob The glob to match
|
|
264
|
+
* @param ref Git reference to search files in
|
|
265
|
+
* @param prefix Optional path prefix used to filter results
|
|
266
|
+
* @throws {GitHubAPIError} on an API error
|
|
267
|
+
*/
|
|
268
|
+
async findFilesByGlobAndRef(glob, ref, prefix) {
|
|
269
|
+
this.logger.debug(`Looking in local clone for file matching glob ${glob} with ref ${ref} and prefix '${prefix}'`);
|
|
270
|
+
let normalizedPrefix = prefix
|
|
271
|
+
? prefix.replace(/^[/\\]/, '').replace(/[/\\]$/, '')
|
|
272
|
+
: '';
|
|
273
|
+
if (normalizedPrefix === manifest_1.ROOT_PROJECT_PATH) {
|
|
274
|
+
normalizedPrefix = '';
|
|
275
|
+
}
|
|
276
|
+
const treePath = normalizedPrefix ? `${normalizedPrefix}/` : '.';
|
|
277
|
+
const resolvedRef = await this.ensureRef(ref);
|
|
278
|
+
const files = [];
|
|
279
|
+
const dirs = new Set();
|
|
280
|
+
await this.execGitStream(['ls-tree', '-r', '--name-only', resolvedRef, treePath], line => {
|
|
281
|
+
const trimmed = line.trim();
|
|
282
|
+
if (trimmed) {
|
|
283
|
+
files.push(trimmed);
|
|
284
|
+
let dir = path.posix.dirname(trimmed);
|
|
285
|
+
while (dir !== '.' && dir !== '/') {
|
|
286
|
+
dirs.add(dir);
|
|
287
|
+
dir = path.posix.dirname(dir);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
const allPaths = [...files, ...dirs];
|
|
292
|
+
// Make paths relative to prefix if provided
|
|
293
|
+
let relativePaths = allPaths;
|
|
294
|
+
if (normalizedPrefix) {
|
|
295
|
+
relativePaths = allPaths
|
|
296
|
+
.map(p => {
|
|
297
|
+
if (p === normalizedPrefix)
|
|
298
|
+
return '';
|
|
299
|
+
if (p.startsWith(`${normalizedPrefix}/`)) {
|
|
300
|
+
return p.slice(normalizedPrefix.length + 1);
|
|
301
|
+
}
|
|
302
|
+
return p;
|
|
303
|
+
})
|
|
304
|
+
.filter(p => p !== '');
|
|
305
|
+
}
|
|
306
|
+
const regex = globToRegex(glob);
|
|
307
|
+
return relativePaths.filter(p => regex.test(p));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Returns a list of paths to all files with a given file
|
|
311
|
+
* extension.
|
|
312
|
+
*
|
|
313
|
+
* If a prefix is specified, only return paths that match
|
|
314
|
+
* the provided prefix.
|
|
315
|
+
*
|
|
316
|
+
* @param extension The file extension used to filter results.
|
|
317
|
+
* Example: `js`, `java`
|
|
318
|
+
* @param prefix Optional path prefix used to filter results
|
|
319
|
+
* @returns {string[]} List of file paths
|
|
320
|
+
* @throws {GitHubAPIError} on an API error
|
|
321
|
+
*/
|
|
322
|
+
async findFilesByExtension(extension, prefix) {
|
|
323
|
+
return this.findFilesByExtensionAndRef(extension, this.repository.defaultBranch, prefix);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Returns a list of paths to all files with a given file
|
|
327
|
+
* extension.
|
|
328
|
+
*
|
|
329
|
+
* If a prefix is specified, only return paths that match
|
|
330
|
+
* the provided prefix.
|
|
331
|
+
*
|
|
332
|
+
* @param extension The file extension used to filter results.
|
|
333
|
+
* Example: `js`, `java`
|
|
334
|
+
* @param ref Git reference to search files in
|
|
335
|
+
* @param prefix Optional path prefix used to filter results
|
|
336
|
+
* @returns {string[]} List of file paths
|
|
337
|
+
* @throws {GitHubAPIError} on an API error
|
|
338
|
+
*/
|
|
339
|
+
async findFilesByExtensionAndRef(extension, ref, prefix) {
|
|
340
|
+
this.logger.debug(`Looking in local clone for file matching extension ${extension} with ref ${ref} and prefix '${prefix}'`);
|
|
341
|
+
let normalizedPrefix = prefix
|
|
342
|
+
? prefix.replace(/^[/\\]/, '').replace(/[/\\]$/, '')
|
|
343
|
+
: '';
|
|
344
|
+
if (normalizedPrefix === manifest_1.ROOT_PROJECT_PATH) {
|
|
345
|
+
normalizedPrefix = '';
|
|
346
|
+
}
|
|
347
|
+
const treePath = normalizedPrefix ? `${normalizedPrefix}/` : '.';
|
|
348
|
+
const resolvedRef = await this.ensureRef(ref);
|
|
349
|
+
const matchedPaths = [];
|
|
350
|
+
await this.execGitStream(['ls-tree', '-r', '--name-only', resolvedRef, treePath], line => {
|
|
351
|
+
const trimmed = line.trim();
|
|
352
|
+
if (trimmed && trimmed.endsWith(`.${extension}`)) {
|
|
353
|
+
matchedPaths.push(trimmed);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
if (normalizedPrefix) {
|
|
357
|
+
return matchedPaths
|
|
358
|
+
.map(p => {
|
|
359
|
+
if (p === normalizedPrefix)
|
|
360
|
+
return '';
|
|
361
|
+
if (p.startsWith(`${normalizedPrefix}/`)) {
|
|
362
|
+
return p.slice(normalizedPrefix.length + 1);
|
|
363
|
+
}
|
|
364
|
+
return p;
|
|
365
|
+
})
|
|
366
|
+
.filter(p => p !== '');
|
|
367
|
+
}
|
|
368
|
+
return matchedPaths;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Returns the list of commits to the default branch after the provided filter
|
|
372
|
+
* query has been satified.
|
|
373
|
+
*
|
|
374
|
+
* @param {string} targetBranch Target branch of commit
|
|
375
|
+
* @param {CommitFilter} filter Callback function that returns whether a
|
|
376
|
+
* commit/pull request matches certain criteria
|
|
377
|
+
* @param {CommitIteratorOptions} options Query options
|
|
378
|
+
* @param {number} options.maxResults Limit the number of results searched.
|
|
379
|
+
* Defaults to unlimited.
|
|
380
|
+
* @param {boolean} options.backfillFiles If set, use the REST API for
|
|
381
|
+
* fetching the list of touched files in this commit. Defaults to `false`.
|
|
382
|
+
* @returns {Commit[]} List of commits to current branch
|
|
383
|
+
* @throws {GitHubAPIError} on an API error
|
|
384
|
+
*/
|
|
385
|
+
async commitsSince(targetBranch, filter, options) {
|
|
386
|
+
const commits = [];
|
|
387
|
+
const generator = this.mergeCommitIterator(targetBranch, options);
|
|
388
|
+
for await (const commit of generator) {
|
|
389
|
+
if (filter(commit)) {
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
commits.push(commit);
|
|
393
|
+
}
|
|
394
|
+
return commits;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Iterate through commit history with a max number of results scanned.
|
|
398
|
+
*
|
|
399
|
+
* @param {string} targetBranch target branch of commit
|
|
400
|
+
* @param {CommitIteratorOptions} options Query options
|
|
401
|
+
* @param {number} options.maxResults Limit the number of results searched.
|
|
402
|
+
* Defaults to unlimited.
|
|
403
|
+
* @param {boolean} options.backfillFiles If set, use the REST API for
|
|
404
|
+
* fetching the list of touched files in this commit. Defaults to `false`.
|
|
405
|
+
* @yields {Commit}
|
|
406
|
+
* @throws {GitHubAPIError} on an API error
|
|
407
|
+
*/
|
|
408
|
+
async *mergeCommitIterator(targetBranch, options) {
|
|
409
|
+
var _a;
|
|
410
|
+
this.logger.debug(`Looking in local clone for commits on branch ${targetBranch}`);
|
|
411
|
+
const backfillFiles = (_a = options === null || options === void 0 ? void 0 : options.backfillFiles) !== null && _a !== void 0 ? _a : true;
|
|
412
|
+
let format = '---COMMIT_START---%n%H%n%B';
|
|
413
|
+
if (backfillFiles) {
|
|
414
|
+
format += '%n---FILES_START---';
|
|
415
|
+
}
|
|
416
|
+
const ref = await this.ensureRef(targetBranch);
|
|
417
|
+
const args = ['log', ref, `--pretty=format:${format}`];
|
|
418
|
+
if (backfillFiles) {
|
|
419
|
+
args.push('--name-only');
|
|
420
|
+
}
|
|
421
|
+
if (options === null || options === void 0 ? void 0 : options.maxResults) {
|
|
422
|
+
args.push('-n', options.maxResults.toString());
|
|
423
|
+
}
|
|
424
|
+
const { stdout } = await execFile('git', args, {
|
|
425
|
+
cwd: this.cloneDir,
|
|
426
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
427
|
+
});
|
|
428
|
+
const blocks = stdout.split('---COMMIT_START---\n');
|
|
429
|
+
for (const block of blocks) {
|
|
430
|
+
if (!block.trim())
|
|
431
|
+
continue;
|
|
432
|
+
let commitInfo = block;
|
|
433
|
+
let files = [];
|
|
434
|
+
if (backfillFiles) {
|
|
435
|
+
const parts = block.split('\n---FILES_START---\n');
|
|
436
|
+
commitInfo = parts[0];
|
|
437
|
+
if (parts[1]) {
|
|
438
|
+
files = parts[1]
|
|
439
|
+
.split('\n')
|
|
440
|
+
.map((f) => f.trim())
|
|
441
|
+
.filter((f) => f);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const lines = commitInfo.split('\n');
|
|
445
|
+
const sha = lines[0].trim();
|
|
446
|
+
const message = lines.slice(1).join('\n').trim();
|
|
447
|
+
if (!sha)
|
|
448
|
+
continue;
|
|
449
|
+
const commit = {
|
|
450
|
+
sha,
|
|
451
|
+
message,
|
|
452
|
+
files: backfillFiles ? files : undefined,
|
|
453
|
+
};
|
|
454
|
+
const subject = lines[1] ? lines[1].trim() : '';
|
|
455
|
+
let prNumber;
|
|
456
|
+
let headBranchName = '';
|
|
457
|
+
const squashMatch = subject.match(/\s\(#(\d+)\)$/);
|
|
458
|
+
const mergeMatch = subject.match(/^Merge pull request #(\d+) from (.*)$/);
|
|
459
|
+
if (squashMatch) {
|
|
460
|
+
prNumber = parseInt(squashMatch[1], 10);
|
|
461
|
+
}
|
|
462
|
+
else if (mergeMatch) {
|
|
463
|
+
prNumber = parseInt(mergeMatch[1], 10);
|
|
464
|
+
headBranchName = mergeMatch[2].trim();
|
|
465
|
+
}
|
|
466
|
+
if (prNumber) {
|
|
467
|
+
commit.pullRequest = {
|
|
468
|
+
sha,
|
|
469
|
+
number: prNumber,
|
|
470
|
+
title: subject.replace(/\s\(#(\d+)\)$/, ''),
|
|
471
|
+
body: message,
|
|
472
|
+
labels: [],
|
|
473
|
+
files: backfillFiles ? files : [],
|
|
474
|
+
baseBranchName: targetBranch,
|
|
475
|
+
headBranchName,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
yield commit;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Iterate through merged pull requests with a max number of results scanned.
|
|
483
|
+
*
|
|
484
|
+
* @param {string} targetBranch The base branch of the pull request
|
|
485
|
+
* @param {string} status The status of the pull request
|
|
486
|
+
* @param {number} maxResults Limit the number of results searched. Defaults to
|
|
487
|
+
* unlimited.
|
|
488
|
+
* @param {boolean} includeFiles Whether to fetch the list of files included in
|
|
489
|
+
* the pull request. Defaults to `true`.
|
|
490
|
+
* @yields {PullRequest}
|
|
491
|
+
* @throws {GitHubAPIError} on an API error
|
|
492
|
+
*/
|
|
493
|
+
async *pullRequestIterator(targetBranch, status, maxResults, includeFiles) {
|
|
494
|
+
yield* this.gitHubApi.pullRequestIterator(targetBranch, status, maxResults, includeFiles);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Iterate through releases with a max number of results scanned.
|
|
498
|
+
*
|
|
499
|
+
* @param {ReleaseIteratorOptions} options Query options
|
|
500
|
+
* @param {number} options.maxResults Limit the number of results searched.
|
|
501
|
+
* Defaults to unlimited.
|
|
502
|
+
* @yields {GitHubRelease}
|
|
503
|
+
* @throws {GitHubAPIError} on an API error
|
|
504
|
+
*/
|
|
505
|
+
async *releaseIterator(options) {
|
|
506
|
+
yield* this.gitHubApi.releaseIterator(options);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Iterate through tags with a max number of results scanned.
|
|
510
|
+
*
|
|
511
|
+
* @param {TagIteratorOptions} options Query options
|
|
512
|
+
* @param {number} options.maxResults Limit the number of results searched.
|
|
513
|
+
* Defaults to unlimited.
|
|
514
|
+
* @yields {GitHubTag}
|
|
515
|
+
* @throws {GitHubAPIError} on an API error
|
|
516
|
+
*/
|
|
517
|
+
async *tagIterator(options) {
|
|
518
|
+
const { stdout } = await execFile('git', [
|
|
519
|
+
'for-each-ref',
|
|
520
|
+
'--sort=-version:refname',
|
|
521
|
+
'refs/tags',
|
|
522
|
+
'--format=%(refname:short)|%(objectname)|%(*objectname)',
|
|
523
|
+
], { cwd: this.cloneDir });
|
|
524
|
+
const maxResults = (options === null || options === void 0 ? void 0 : options.maxResults) || Number.MAX_SAFE_INTEGER;
|
|
525
|
+
let results = 0;
|
|
526
|
+
for (const line of stdout.split('\n')) {
|
|
527
|
+
if (!line)
|
|
528
|
+
continue;
|
|
529
|
+
const [name, objectSha, commitSha] = line.split('|');
|
|
530
|
+
const sha = commitSha || objectSha;
|
|
531
|
+
if (sha) {
|
|
532
|
+
yield { name, sha };
|
|
533
|
+
results++;
|
|
534
|
+
if (results >= maxResults)
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async applyEditsAndPush(branch, targetBranch, message, changes) {
|
|
540
|
+
this.logger.debug(`Applying edits and pushing to ${branch}`);
|
|
541
|
+
// Checkout/Reset PR branch
|
|
542
|
+
await execFile('git', ['fetch', 'origin', '--', targetBranch], {
|
|
543
|
+
cwd: this.cloneDir,
|
|
544
|
+
});
|
|
545
|
+
await execFile('git', ['checkout', '-B', branch, `origin/${targetBranch}`], {
|
|
546
|
+
cwd: this.cloneDir,
|
|
547
|
+
});
|
|
548
|
+
// Write file edits
|
|
549
|
+
for (const [filePath, fileUpdate] of changes.entries()) {
|
|
550
|
+
const fullPath = path.join(this.cloneDir, filePath);
|
|
551
|
+
await fs.promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
552
|
+
if (fileUpdate.content !== null) {
|
|
553
|
+
await fs.promises.writeFile(fullPath, fileUpdate.content);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
await fs.promises.unlink(fullPath).catch(() => { });
|
|
557
|
+
}
|
|
558
|
+
if (fileUpdate.mode) {
|
|
559
|
+
await fs.promises.chmod(fullPath, parseInt(fileUpdate.mode, 8));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// Commit changes
|
|
563
|
+
const msgFile = path.join(os.tmpdir(), `release-please-commit-msg-${process.pid}-${Date.now()}`);
|
|
564
|
+
await fs.promises.writeFile(msgFile, message);
|
|
565
|
+
await execFile('git', ['add', '.'], { cwd: this.cloneDir });
|
|
566
|
+
try {
|
|
567
|
+
await execFile('git', ['commit', '-F', msgFile], { cwd: this.cloneDir });
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
const error = err;
|
|
571
|
+
if (error.stdout && error.stdout.includes('nothing to commit')) {
|
|
572
|
+
this.logger.debug('Nothing to commit');
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
throw err;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
finally {
|
|
579
|
+
await fs.promises.unlink(msgFile).catch(() => { });
|
|
580
|
+
}
|
|
581
|
+
// Push transit
|
|
582
|
+
await execFile('git', ['push', '-f', 'origin', branch], {
|
|
583
|
+
cwd: this.cloneDir,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Open a pull request
|
|
588
|
+
*
|
|
589
|
+
* @param {PullRequest} pullRequest Pull request data to update
|
|
590
|
+
* @param {string} targetBranch The base branch of the pull request
|
|
591
|
+
* @param {string} message The commit message for the commit
|
|
592
|
+
* @param {Update[]} updates The files to update
|
|
593
|
+
* @param {CreatePullRequestOptions} options The pull request options
|
|
594
|
+
* @throws {GitHubAPIError} on an API error
|
|
595
|
+
*/
|
|
596
|
+
async createPullRequest(pullRequest, targetBranch, message, updates, options) {
|
|
597
|
+
const changes = await this.buildChangeSet(updates, targetBranch);
|
|
598
|
+
await this.applyEditsAndPush(pullRequest.headBranchName, targetBranch, message, changes);
|
|
599
|
+
this.logger.info('Creating pull request via GitHub API...');
|
|
600
|
+
return await this.gitHubApi.createPullRequest(pullRequest, targetBranch, options);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Update a pull request's title and body.
|
|
604
|
+
* @param {number} number The pull request number
|
|
605
|
+
* @param {ReleasePullRequest} releasePullRequest Pull request data to update
|
|
606
|
+
* @param {string} targetBranch The target branch of the pull request
|
|
607
|
+
* @param {string} options.signoffUser Optional. Commit signoff message
|
|
608
|
+
* @param {boolean} options.fork Optional. Whether to open the pull request from
|
|
609
|
+
* a fork or not. Defaults to `false`
|
|
610
|
+
* @param {PullRequestOverflowHandler} options.pullRequestOverflowHandler Optional.
|
|
611
|
+
* Handles extra large pull request body messages.
|
|
612
|
+
*/
|
|
613
|
+
async updatePullRequest(number, pullRequest, targetBranch, options) {
|
|
614
|
+
const changes = await this.buildChangeSet(pullRequest.updates, targetBranch);
|
|
615
|
+
const message = pullRequest.title.toString();
|
|
616
|
+
await this.applyEditsAndPush(pullRequest.headRefName, targetBranch, message, changes);
|
|
617
|
+
const body = ((options === null || options === void 0 ? void 0 : options.pullRequestOverflowHandler)
|
|
618
|
+
? await options.pullRequestOverflowHandler.handleOverflow(pullRequest)
|
|
619
|
+
: pullRequest.body)
|
|
620
|
+
.toString()
|
|
621
|
+
.slice(0, github_api_1.MAX_ISSUE_BODY_SIZE);
|
|
622
|
+
const pullResponseData = (await this.gitHubApi.octokit.pulls.update({
|
|
623
|
+
owner: this.repository.owner,
|
|
624
|
+
repo: this.repository.repo,
|
|
625
|
+
pull_number: number,
|
|
626
|
+
title: pullRequest.title.toString(),
|
|
627
|
+
body,
|
|
628
|
+
state: 'open',
|
|
629
|
+
})).data;
|
|
630
|
+
return {
|
|
631
|
+
headBranchName: pullResponseData.head.ref,
|
|
632
|
+
baseBranchName: pullResponseData.base.ref,
|
|
633
|
+
number: pullResponseData.number,
|
|
634
|
+
title: pullResponseData.title,
|
|
635
|
+
body: pullResponseData.body || '',
|
|
636
|
+
files: [],
|
|
637
|
+
labels: pullResponseData.labels
|
|
638
|
+
.map((label) => label.name)
|
|
639
|
+
.filter((name) => !!name),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
async getPullRequest(number) {
|
|
643
|
+
return await this.gitHubApi.getPullRequest(number);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Create a GitHub release
|
|
647
|
+
*
|
|
648
|
+
* @param {Release} release Release parameters
|
|
649
|
+
* @param {ReleaseOptions} options Release option parameters
|
|
650
|
+
* @throws {DuplicateReleaseError} if the release tag already exists
|
|
651
|
+
* @throws {GitHubAPIError} on other API errors
|
|
652
|
+
*/
|
|
653
|
+
async createRelease(release, options) {
|
|
654
|
+
return await this.gitHubApi.createRelease(release, options);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Makes a comment on a issue/pull request.
|
|
658
|
+
*
|
|
659
|
+
* @param {string} comment - The body of the comment to post.
|
|
660
|
+
* @param {number} number - The issue or pull request number.
|
|
661
|
+
* @throws {GitHubAPIError} on an API error
|
|
662
|
+
*/
|
|
663
|
+
async commentOnIssue(comment, number) {
|
|
664
|
+
return await this.gitHubApi.commentOnIssue(comment, number);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Removes labels from an issue/pull request.
|
|
668
|
+
*
|
|
669
|
+
* @param {string[]} labels The labels to remove.
|
|
670
|
+
* @param {number} number The issue/pull request number.
|
|
671
|
+
*/
|
|
672
|
+
async removeIssueLabels(labels, number) {
|
|
673
|
+
return await this.gitHubApi.removeIssueLabels(labels, number);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Adds label to an issue/pull request.
|
|
677
|
+
*
|
|
678
|
+
* @param {string[]} labels The labels to add.
|
|
679
|
+
* @param {number} number The issue/pull request number.
|
|
680
|
+
*/
|
|
681
|
+
async addIssueLabels(labels, number) {
|
|
682
|
+
return await this.gitHubApi.addIssueLabels(labels, number);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Generate release notes from GitHub at tag
|
|
686
|
+
* @param {string} tagName Name of new release tag
|
|
687
|
+
* @param {string} targetCommitish Target commitish for new tag
|
|
688
|
+
* @param {string} previousTag Optional. Name of previous tag to analyze commits since
|
|
689
|
+
*/
|
|
690
|
+
async generateReleaseNotes(tagName, targetCommitish, previousTag) {
|
|
691
|
+
return await this.gitHubApi.generateReleaseNotes(tagName, targetCommitish, previousTag);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Create a single file on a new branch based on an existing
|
|
695
|
+
* branch. This will force-push to that branch.
|
|
696
|
+
* @param {string} filename Filename with path in the repository
|
|
697
|
+
* @param {string} contents Contents of the file
|
|
698
|
+
* @param {string} newBranchName Name of the new branch
|
|
699
|
+
* @param {string} baseBranchName Name of the base branch (where
|
|
700
|
+
* new branch is forked from)
|
|
701
|
+
* @returns {string} HTML URL of the new file
|
|
702
|
+
*/
|
|
703
|
+
async createFileOnNewBranch(filename, contents, newBranchName, baseBranchName) {
|
|
704
|
+
return await this.gitHubApi.createFileOnNewBranch(filename, contents, newBranchName, baseBranchName);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Given a set of proposed updates, build a changeset to suggest.
|
|
708
|
+
*
|
|
709
|
+
* @param {Update[]} updates The proposed updates
|
|
710
|
+
* @param {string} defaultBranch The target branch
|
|
711
|
+
* @return {Changes} The changeset to suggest.
|
|
712
|
+
* @throws {GitHubAPIError} on an API error
|
|
713
|
+
*/
|
|
714
|
+
async buildChangeSet(updates, defaultBranch) {
|
|
715
|
+
const mergedUpdates = (0, composite_1.mergeUpdates)(updates);
|
|
716
|
+
const changes = new Map();
|
|
717
|
+
for (const update of mergedUpdates) {
|
|
718
|
+
let content;
|
|
719
|
+
try {
|
|
720
|
+
content = await this.getFileContentsOnBranch(update.path, defaultBranch);
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
if (!(err instanceof errors_1.FileNotFoundError))
|
|
724
|
+
throw err;
|
|
725
|
+
if (!update.createIfMissing) {
|
|
726
|
+
console.warn(`file ${update.path} did not exist`);
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const newContents = update.updater.updateContent(content ? content.parsedContent : undefined);
|
|
731
|
+
if (newContents) {
|
|
732
|
+
changes.set(update.path, {
|
|
733
|
+
content: newContents,
|
|
734
|
+
originalContent: content ? content.parsedContent : null,
|
|
735
|
+
mode: content ? content.mode : git_file_utils_1.DEFAULT_FILE_MODE,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return changes;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
exports.LocalGitHub = LocalGitHub;
|
|
743
|
+
function globToRegex(glob) {
|
|
744
|
+
let reg = '';
|
|
745
|
+
let i = 0;
|
|
746
|
+
while (i < glob.length) {
|
|
747
|
+
const c = glob[i];
|
|
748
|
+
if (c === '*') {
|
|
749
|
+
if (i + 1 < glob.length && glob[i + 1] === '*') {
|
|
750
|
+
if (i + 2 < glob.length && glob[i + 2] === '/') {
|
|
751
|
+
reg += '(?:.*\\/)?';
|
|
752
|
+
i += 2;
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
reg += '.*';
|
|
756
|
+
i++;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
reg += '[^/]*';
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
else if (c === '?') {
|
|
764
|
+
reg += '[^/]';
|
|
765
|
+
}
|
|
766
|
+
else if (['.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\'].includes(c)) {
|
|
767
|
+
reg += '\\' + c;
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
reg += c;
|
|
771
|
+
}
|
|
772
|
+
i++;
|
|
773
|
+
}
|
|
774
|
+
return new RegExp(`^${reg}$`);
|
|
775
|
+
}
|
|
776
|
+
//# sourceMappingURL=local-github.js.map
|