@knowttl/gitea-axi 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +350 -32
- package/axi-credential.schema.json +8 -1
- package/dist/action-values.js +37 -0
- package/dist/api.js +90 -19
- package/dist/attachments.js +171 -0
- package/dist/catalogue.js +65 -0
- package/dist/ci.js +24 -0
- package/dist/credentials.js +78 -6
- package/dist/git.js +89 -0
- package/dist/local-workflows.js +222 -0
- package/dist/main.js +245 -25
- package/dist/private-file.js +3 -1
- package/dist/pulls.js +299 -0
- package/dist/reads.js +15 -9
- package/dist/registry.js +139 -5
- package/dist/releases.js +145 -0
- package/dist/repo-workflows.js +147 -0
- package/dist/search.js +111 -0
- package/dist/stacks.js +486 -0
- package/dist/transport.js +62 -7
- package/dist/writes.js +568 -0
- package/generated/SKILL.md +141 -0
- package/generated/capabilities.json +964 -0
- package/generated/help.txt +5376 -0
- package/generated/host-inventory.json +7000 -151
- package/generated/routes.json +3501 -5
- package/package.json +24 -10
- package/schemas/catalogue.json +2672 -0
package/dist/git.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
import { CliError } from './error.js';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
export class Git {
|
|
7
|
+
cwd;
|
|
8
|
+
origin;
|
|
9
|
+
token;
|
|
10
|
+
constructor(cwd, origin, token) {
|
|
11
|
+
this.cwd = cwd;
|
|
12
|
+
this.origin = origin;
|
|
13
|
+
this.token = token;
|
|
14
|
+
}
|
|
15
|
+
async run(args, allowed = [0]) {
|
|
16
|
+
const env = {
|
|
17
|
+
PATH: process.env.PATH, SystemRoot: process.env.SystemRoot,
|
|
18
|
+
GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
|
|
19
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
20
|
+
GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: '', GIT_EDITOR: 'true', GIT_SEQUENCE_EDITOR: 'true',
|
|
21
|
+
GIT_CONFIG_COUNT: this.token ? '1' : '0',
|
|
22
|
+
...(this.token ? { GIT_CONFIG_KEY_0: 'http.' + this.origin + '/.extraHeader', GIT_CONFIG_VALUE_0: 'Authorization: token ' + this.token } : {}),
|
|
23
|
+
};
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const child = spawn('git', ['-c', 'core.hooksPath=' + (process.platform === 'win32' ? 'NUL' : '/dev/null'),
|
|
26
|
+
'-c', 'core.fsmonitor=false', '-c', 'credential.helper=', '-c', 'http.followRedirects=false', '-c', 'protocol.allow=never',
|
|
27
|
+
'-c', 'protocol.http.allow=always', '-c', 'protocol.https.allow=always',
|
|
28
|
+
...(args[0] === 'push' ? ['push', '--no-follow-tags', '--recurse-submodules=no', ...args.slice(1)] : args)], {
|
|
29
|
+
cwd: this.cwd, env, stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000,
|
|
30
|
+
});
|
|
31
|
+
let output = '', size = 0;
|
|
32
|
+
child.stdout.on('data', (bytes) => { size += bytes.length; if (size > 2_000_000)
|
|
33
|
+
child.kill();
|
|
34
|
+
else
|
|
35
|
+
output += bytes.toString(); });
|
|
36
|
+
child.stderr.on('data', () => { });
|
|
37
|
+
child.on('error', () => reject(new CliError('GIT_FAILED', 'Unable to start native Git')));
|
|
38
|
+
child.on('close', code => allowed.includes(code ?? -1) && size <= 2_000_000 ? resolve(output.trimEnd()) :
|
|
39
|
+
reject(new CliError('GIT_FAILED', 'Native Git ' + args[0] + ' stopped; inspect the workspace for conflicts before continuing')));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async workspace() {
|
|
43
|
+
if (!isAbsolute(this.cwd) || await realpath(this.cwd) !== this.cwd || await this.run(['rev-parse', '--show-toplevel']) !== this.cwd) {
|
|
44
|
+
throw new CliError('USAGE', '--workspace must be the absolute native repository root', 2);
|
|
45
|
+
}
|
|
46
|
+
const config = await this.run(['config', '--local', '--name-only', '--list']) + '\n' +
|
|
47
|
+
await this.run(['config', '--worktree', '--name-only', '--list']);
|
|
48
|
+
if (config.split('\n').some(key => /^(?:url\.|http\.|credential\.|include\.|includeif\.|filter\.|diff\.|core\.sshcommand$|remote\..*\.(?:proxy|uploadpack|receivepack|pushurl|vcs)$)/i.test(key))) {
|
|
49
|
+
throw new CliError('UNSAFE_GIT_CONFIG', 'Workspace transport overrides must be removed before using origin-bound Git');
|
|
50
|
+
}
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
async clean() {
|
|
54
|
+
if (await this.run(['status', '--porcelain=v1', '--untracked-files=all']))
|
|
55
|
+
throw new CliError('DIRTY_WORKSPACE', 'Commit or stash workspace changes before this operation');
|
|
56
|
+
}
|
|
57
|
+
async branch(name) {
|
|
58
|
+
if (!name || name.startsWith('-'))
|
|
59
|
+
throw new CliError('USAGE', 'Select a branch name', 2);
|
|
60
|
+
await this.run(['check-ref-format', '--branch', name]);
|
|
61
|
+
return name;
|
|
62
|
+
}
|
|
63
|
+
async tip(branch = 'HEAD') { return this.run(['rev-parse', '--verify', branch + '^{commit}']); }
|
|
64
|
+
async snapshot() {
|
|
65
|
+
const status = await this.run(['status', '--porcelain=v1', '--untracked-files=all']);
|
|
66
|
+
return { head: await this.tip(), branch: await this.run(['symbolic-ref', '--quiet', '--short', 'HEAD'], [0, 1]),
|
|
67
|
+
refs: await this.run(['for-each-ref', '--format=%(refname) %(objectname)', 'refs/heads']), status,
|
|
68
|
+
diff: createHash('sha256').update(await this.run(['diff', '--no-ext-diff', '--no-textconv', '--binary', 'HEAD'])).digest('hex'),
|
|
69
|
+
index: createHash('sha256').update(await this.run(['diff', '--no-ext-diff', '--no-textconv', '--cached', '--binary', 'HEAD'])).digest('hex'),
|
|
70
|
+
worktrees: await this.run(['worktree', 'list', '--porcelain']) };
|
|
71
|
+
}
|
|
72
|
+
url(repository) {
|
|
73
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) || repository.split('/').some(part => part === '.' || part === '..')) {
|
|
74
|
+
throw new CliError('USAGE', 'Select OWNER/NAME', 2);
|
|
75
|
+
}
|
|
76
|
+
return this.origin + '/' + repository + '.git';
|
|
77
|
+
}
|
|
78
|
+
async remote(name) {
|
|
79
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name))
|
|
80
|
+
throw new CliError('USAGE', 'Select a simple remote name', 2);
|
|
81
|
+
const urls = (await this.run(['remote', 'get-url', '--all', name])).split('\n');
|
|
82
|
+
const url = new URL(urls[0]);
|
|
83
|
+
if (urls.length !== 1 || url.origin !== this.origin || url.username || url.password || url.search || url.hash ||
|
|
84
|
+
!/^\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/.test(url.pathname)) {
|
|
85
|
+
throw new CliError('ORIGIN_MISMATCH', 'Selected remote must be one credential-free OWNER/NAME.git URL on the selected origin');
|
|
86
|
+
}
|
|
87
|
+
return { name, url: url.href, repository: url.pathname.slice(1, -4) };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, open, readdir, realpath, rm } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
import { Git } from './git.js';
|
|
5
|
+
import { CliError } from './error.js';
|
|
6
|
+
import { collect, object, read, resolveCommit } from './api.js';
|
|
7
|
+
import { repoPath, repositoryPlan, validateRepositoryWorkflow } from './repo-workflows.js';
|
|
8
|
+
import { verifyPull } from './pulls.js';
|
|
9
|
+
import { readPrivateFile, writePrivateFile } from './private-file.js';
|
|
10
|
+
import { write } from './writes.js';
|
|
11
|
+
import { stackWorkflow } from './stacks.js';
|
|
12
|
+
export const usage = (message) => { throw new CliError('USAGE', message, 2); };
|
|
13
|
+
export const gap = (message) => { throw new CliError('CAPABILITY_GAP', message); };
|
|
14
|
+
export const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
15
|
+
export function isLocal(operation, options) {
|
|
16
|
+
return operation.startsWith('stack ') || ['repo clone', 'pr checkout', 'pr revert'].includes(operation) ||
|
|
17
|
+
['repo create', 'repo fork'].includes(operation) && ['source', 'push', 'remote', 'clone', 'workspace'].some(key => options[key] !== undefined);
|
|
18
|
+
}
|
|
19
|
+
export function validateLocal(operation, options) {
|
|
20
|
+
if (process.env.GITEA_AXI_HOST_WRITE_OPERATIONS !== undefined || process.env.GITEA_AXI_CREDENTIAL_REVISION !== undefined) {
|
|
21
|
+
throw new CliError('READ_ONLY', 'Workspace workflows are standalone only');
|
|
22
|
+
}
|
|
23
|
+
if (operation.startsWith('stack ') && (options.repo || process.env.GH_REPO))
|
|
24
|
+
usage('Stack commands reject -R/--repo and GH_REPO; use the explicit local workspace');
|
|
25
|
+
if (typeof options.workspace !== 'string' || !isAbsolute(options.workspace))
|
|
26
|
+
usage('--workspace requires an explicit absolute path');
|
|
27
|
+
if (options.config !== undefined && !isAbsolute(String(options.config)))
|
|
28
|
+
usage('--config must be absolute');
|
|
29
|
+
}
|
|
30
|
+
export async function localEffect(client, identity, operation, options, git, preconditions, payload, execute, api) {
|
|
31
|
+
const step = { method: 'LOCAL', path: git.cwd, local: 'gitea.local.' + operation, payload };
|
|
32
|
+
const plan = api ? { ...api, steps: [...api.steps, step], preconditions: { api: api.preconditions, workspace: preconditions } } :
|
|
33
|
+
{ target: git.cwd, steps: [step], reads: [], preconditions };
|
|
34
|
+
// Keep API verification preconditions in their original shape for the existing verifier.
|
|
35
|
+
if (api)
|
|
36
|
+
plan.preconditions = { ...object(api.preconditions), localWorkspace: preconditions };
|
|
37
|
+
return write(client, identity, plan, options, execute);
|
|
38
|
+
}
|
|
39
|
+
export async function localWorkflow(client, identity, operation, args, options) {
|
|
40
|
+
if (operation.startsWith('stack '))
|
|
41
|
+
return stackWorkflow(client, identity, operation.slice(6), args, options);
|
|
42
|
+
const workspace = await realpath(String(options.workspace));
|
|
43
|
+
const git = new Git(workspace, identity.origin, identity.token);
|
|
44
|
+
if (operation === 'repo clone') {
|
|
45
|
+
if (args.length !== 1)
|
|
46
|
+
usage('Select one OWNER/NAME to clone');
|
|
47
|
+
const name = args[0];
|
|
48
|
+
const url = git.url(name);
|
|
49
|
+
const destination = join(workspace, name.split('/')[1]);
|
|
50
|
+
if (existsSync(destination))
|
|
51
|
+
usage('Clone destination already exists');
|
|
52
|
+
const repo = object(await read(client, repoPath(name)));
|
|
53
|
+
if (repo.full_name !== name)
|
|
54
|
+
throw new CliError('INVALID_RESPONSE', 'Repository identity differs from the selected clone target');
|
|
55
|
+
const expected = await resolveCommit(client, repoPath(name), String(repo.default_branch));
|
|
56
|
+
const preconditions = { parent: workspace, contents: await readdir(workspace), repository: repo, commit: expected.sha };
|
|
57
|
+
return localEffect(client, identity, 'clone', options, git, preconditions, { url, destination }, async () => {
|
|
58
|
+
if (existsSync(destination) || !same(await readdir(workspace), preconditions.contents))
|
|
59
|
+
throw new CliError('CONFIRMATION_MISMATCH', 'Clone parent changed');
|
|
60
|
+
await git.run(['clone', '--no-checkout', '--', url, destination]);
|
|
61
|
+
const clone = await new Git(destination, identity.origin, identity.token).workspace();
|
|
62
|
+
await clone.run(['checkout', '--detach', String(expected.sha)]);
|
|
63
|
+
if (await clone.tip() !== expected.sha)
|
|
64
|
+
throw new CliError('READBACK_MISMATCH', 'Clone head differs from the previewed default commit');
|
|
65
|
+
return { workspace: destination, head: expected.sha, remote: await clone.remote('origin') };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (operation === 'repo create' || operation === 'repo fork') {
|
|
69
|
+
if (args.length > 1)
|
|
70
|
+
usage('Select at most one repository');
|
|
71
|
+
if (operation === 'repo create' && (options.push || options.remote) && !options.source || options.push && !options.source)
|
|
72
|
+
usage('--push and repository creation --remote require --source');
|
|
73
|
+
if (options.source && options.clone)
|
|
74
|
+
usage('Choose --source or --clone');
|
|
75
|
+
const apiOptions = { ...options };
|
|
76
|
+
for (const key of ['source', 'push', 'remote', 'clone', 'workspace'])
|
|
77
|
+
delete apiOptions[key];
|
|
78
|
+
validateRepositoryWorkflow(operation, apiOptions);
|
|
79
|
+
const api = await repositoryPlan(client, operation, args[0], apiOptions);
|
|
80
|
+
const destinationName = String(api.workflow.expected.full_name);
|
|
81
|
+
const url = git.url(destinationName);
|
|
82
|
+
let source;
|
|
83
|
+
let preconditions;
|
|
84
|
+
const remote = String(options.remote ?? 'origin');
|
|
85
|
+
if (!/^[A-Za-z0-9_-]+$/.test(remote))
|
|
86
|
+
usage('Select a simple remote name');
|
|
87
|
+
const destination = join(workspace, destinationName.split('/')[1]);
|
|
88
|
+
if (options.source || operation === 'repo fork' && options.remote && !options.clone) {
|
|
89
|
+
const sourcePath = String(options.source ?? workspace);
|
|
90
|
+
if (!isAbsolute(sourcePath))
|
|
91
|
+
usage('--source requires an absolute native repository root');
|
|
92
|
+
source = await new Git(await realpath(sourcePath), identity.origin, identity.token).workspace();
|
|
93
|
+
if (source.cwd !== workspace)
|
|
94
|
+
usage('--source must equal the explicit --workspace');
|
|
95
|
+
await source.clean();
|
|
96
|
+
if (options.push && !await source.run(['symbolic-ref', '--quiet', '--short', 'HEAD'], [0, 1]))
|
|
97
|
+
usage('Publishing requires a named source branch');
|
|
98
|
+
if ((await source.run(['remote'])).split('\n').includes(remote))
|
|
99
|
+
usage('Selected remote already exists; choose another --remote');
|
|
100
|
+
preconditions = await source.snapshot();
|
|
101
|
+
}
|
|
102
|
+
else if (options.clone) {
|
|
103
|
+
if (existsSync(destination))
|
|
104
|
+
usage('Clone destination already exists');
|
|
105
|
+
preconditions = { parent: workspace, contents: await readdir(workspace) };
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
usage('Workspace composite requires --source or --clone');
|
|
109
|
+
return localEffect(client, identity, 'composite', options, git, preconditions, { source: source?.cwd, remote, url, clone: Boolean(options.clone), push: Boolean(options.push) }, async () => {
|
|
110
|
+
if (source) {
|
|
111
|
+
if (!same(await source.snapshot(), preconditions))
|
|
112
|
+
throw new CliError('CONFIRMATION_MISMATCH', 'Source changed after API creation; repository created, local publication stopped');
|
|
113
|
+
await source.run(['remote', 'add', remote, url]);
|
|
114
|
+
if (options.push) {
|
|
115
|
+
const branch = await source.run(['symbolic-ref', '--short', 'HEAD']);
|
|
116
|
+
await source.run(['push', '--', url, 'refs/heads/' + branch + ':refs/heads/' + branch]);
|
|
117
|
+
if (!(await source.run(['ls-remote', '--', url, 'refs/heads/' + branch])).startsWith(await source.tip()))
|
|
118
|
+
throw new CliError('READBACK_MISMATCH', 'Published head was not confirmed');
|
|
119
|
+
}
|
|
120
|
+
return { workspace, remote, published: Boolean(options.push), head: await source.tip() };
|
|
121
|
+
}
|
|
122
|
+
if (existsSync(destination))
|
|
123
|
+
throw new CliError('CONFIRMATION_MISMATCH', 'Clone destination appeared; API repository was created');
|
|
124
|
+
await git.run(['clone', '--origin', remote, '--', url, destination]);
|
|
125
|
+
return { workspace: destination, remote };
|
|
126
|
+
}, api);
|
|
127
|
+
}
|
|
128
|
+
await git.workspace();
|
|
129
|
+
await git.clean();
|
|
130
|
+
if (args.length !== 1 || !/^[1-9]\d*$/.test(args[0]))
|
|
131
|
+
usage('Select one positive PR number');
|
|
132
|
+
const remote = await git.remote('origin');
|
|
133
|
+
if (options.repo && options.repo !== remote.repository)
|
|
134
|
+
usage('--repo must match the workspace origin remote');
|
|
135
|
+
const base = repoPath(remote.repository);
|
|
136
|
+
const pull = object(await read(client, base + '/pulls/' + args[0]));
|
|
137
|
+
const snapshot = await git.snapshot();
|
|
138
|
+
if (operation === 'pr checkout') {
|
|
139
|
+
const head = object(pull.head);
|
|
140
|
+
const headRepository = object(head.repo);
|
|
141
|
+
const url = git.url(String(headRepository.full_name));
|
|
142
|
+
if (!/^[a-f0-9]{40}$/.test(String(head.sha)))
|
|
143
|
+
throw new CliError('INVALID_RESPONSE', 'PR head requires an exact commit SHA');
|
|
144
|
+
await git.branch(String(head.ref));
|
|
145
|
+
return localEffect(client, identity, 'checkout', options, git, { snapshot, pull, remote }, { url, ref: head.ref, sha: head.sha }, async () => {
|
|
146
|
+
if (!same(snapshot, await git.snapshot()))
|
|
147
|
+
throw new CliError('CONFIRMATION_MISMATCH', 'Workspace changed');
|
|
148
|
+
await git.run(['fetch', '--no-tags', '--', url, 'refs/heads/' + head.ref]);
|
|
149
|
+
if (await git.tip('FETCH_HEAD') !== head.sha)
|
|
150
|
+
throw new CliError('STALE_HEAD', 'Fetched PR branch differs from the previewed head; checkout stopped');
|
|
151
|
+
await git.run(['checkout', '--detach', String(head.sha)]);
|
|
152
|
+
return { workspace, head: await git.tip() };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (pull.merged !== true || !/^[a-f0-9]{40}$/.test(String(pull.merge_commit_sha)))
|
|
156
|
+
usage('Revert requires a merged PR with an exact merge commit');
|
|
157
|
+
const branch = 'revert-' + args[0] + '-' + String(pull.merge_commit_sha).slice(0, 12);
|
|
158
|
+
const baseBranch = await git.branch(String(object(pull.base).ref));
|
|
159
|
+
const recordPath = join(await git.run(['rev-parse', '--path-format=absolute', '--git-common-dir']), 'gitea-axi', branch + '.json');
|
|
160
|
+
if ((await git.run(['for-each-ref', '--format=%(refname:short)', 'refs/heads/' + branch])) === branch) {
|
|
161
|
+
if (snapshot.branch !== branch)
|
|
162
|
+
usage('Check out the generated revert branch before previewing its publication');
|
|
163
|
+
if (!existsSync(recordPath))
|
|
164
|
+
usage('The branch has no recorded revert provenance; inspect the existing branch before publication');
|
|
165
|
+
const record = object(await load(recordPath));
|
|
166
|
+
if (record.merge !== pull.merge_commit_sha || record.head !== snapshot.head || record.repository !== remote.repository || record.origin !== identity.origin) {
|
|
167
|
+
throw new CliError('CONFIRMATION_MISMATCH', 'The generated revert branch or its origin-bound provenance changed');
|
|
168
|
+
}
|
|
169
|
+
const pulls = await collect(client, base + '/pulls?state=all');
|
|
170
|
+
if (pulls.truncated)
|
|
171
|
+
throw new CliError('INCOMPLETE_READ', 'Revert publication requires a complete PR collection');
|
|
172
|
+
if (pulls.items.some(item => object(item.head).ref === branch))
|
|
173
|
+
usage('The revert branch already has a PR; inspect it before repeating');
|
|
174
|
+
const payload = { title: 'Revert "' + String(pull.title) + '"', body: 'Reverts #' + args[0], head: branch, base: baseBranch };
|
|
175
|
+
const local = { method: 'LOCAL', path: workspace, local: 'gitea.local.revert' };
|
|
176
|
+
const plan = { target: base + '/pulls', reads: [],
|
|
177
|
+
preconditions: { snapshot, pull, remote, pulls: pulls.items },
|
|
178
|
+
steps: [{ ...local, payload: { phase: 'push', branch, sha: snapshot.head } },
|
|
179
|
+
{ method: 'POST', path: base + '/pulls', payload }, { ...local, payload: { phase: 'verify', payload } }] };
|
|
180
|
+
return write(client, identity, plan, options, async (step, results) => {
|
|
181
|
+
if (object(step.payload).phase === 'push') {
|
|
182
|
+
if (!same(snapshot, await git.snapshot()))
|
|
183
|
+
throw new CliError('CONFIRMATION_MISMATCH', 'Revert branch changed');
|
|
184
|
+
await git.run(['push', '--', remote.url, 'refs/heads/' + branch + ':refs/heads/' + branch]);
|
|
185
|
+
return { branch, head: snapshot.head };
|
|
186
|
+
}
|
|
187
|
+
const created = object(results[1]);
|
|
188
|
+
const actual = object(await read(client, base + '/pulls/' + created.number));
|
|
189
|
+
if (actual.id !== created.id || object(actual.head).sha !== snapshot.head || object(actual.head).ref !== branch ||
|
|
190
|
+
object(actual.base).ref !== baseBranch || actual.title !== payload.title || actual.body !== payload.body) {
|
|
191
|
+
throw new CliError('READBACK_MISMATCH', 'Revert PR did not confirm its exact branch and generated commit');
|
|
192
|
+
}
|
|
193
|
+
return { pull: actual, branch, head: snapshot.head };
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const preconditions = { snapshot, pull, remote };
|
|
197
|
+
return localEffect(client, identity, 'revert', options, git, preconditions, { branch, baseBranch, merge: pull.merge_commit_sha }, async () => {
|
|
198
|
+
await git.run(['fetch', '--no-tags', '--', remote.url, 'refs/heads/' + baseBranch]);
|
|
199
|
+
await git.run(['checkout', '-b', branch, 'FETCH_HEAD']);
|
|
200
|
+
const parents = (await git.run(['rev-list', '--parents', '-n', '1', String(pull.merge_commit_sha)])).split(' ');
|
|
201
|
+
if (parents.length !== 3)
|
|
202
|
+
gap('Revert currently requires a two-parent merge commit; branch retained without a revert');
|
|
203
|
+
await git.run(['revert', '--no-edit', '-m', '1', String(pull.merge_commit_sha)]);
|
|
204
|
+
await save(recordPath, { merge: pull.merge_commit_sha, head: await git.tip(), repository: remote.repository, origin: identity.origin });
|
|
205
|
+
return { branch, head: await git.tip(), state: 'local revert prepared',
|
|
206
|
+
next: 'Repeat pr revert ' + args[0] + ' with --preview to bind publication of the generated commit and creation of its revert PR' };
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
export async function lock(directory) {
|
|
210
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
211
|
+
const path = join(directory, 'lock');
|
|
212
|
+
const file = await open(path, 'wx', 0o600).catch(() => { throw new CliError('STACK_LOCKED', 'Stack mutation is locked; inspect the owning process before manual recovery'); });
|
|
213
|
+
return async () => { await file.close(); await rm(path); };
|
|
214
|
+
}
|
|
215
|
+
export async function verifyPlannedPull(client, plan, result) {
|
|
216
|
+
const observations = [];
|
|
217
|
+
for (const path of plan.reads)
|
|
218
|
+
observations.push({ path, data: await read(client, path) });
|
|
219
|
+
await verifyPull(client, plan, [result], observations);
|
|
220
|
+
}
|
|
221
|
+
export async function save(path, value) { await writePrivateFile(path, JSON.stringify(value)); }
|
|
222
|
+
export async function load(path) { return JSON.parse(await readPrivateFile(path, true, 1_000_000)); }
|