gitroots 0.5.0 → 0.6.1
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/README.md +76 -76
- package/bin/gitroots.js +8 -0
- package/package.json +2 -1
- package/src/agents/local.js +219 -0
- package/src/api.js +61 -98
- package/src/cli.js +36 -110
- package/src/commands/completion.js +14 -37
- package/src/commands/push.js +65 -131
- package/src/commands/update.js +1 -1
- package/src/config.js +66 -0
- package/src/rules.js +88 -0
- package/src/runtime.js +1 -1
- package/src/session/account.js +133 -0
- package/src/session/agents.js +416 -0
- package/src/session/helpers.js +71 -0
- package/src/session/logo.js +107 -0
- package/src/session/registry.js +61 -0
- package/src/session/repl.js +332 -0
- package/src/session/setup.js +101 -0
- package/src/session/workspace.js +270 -0
- package/src/ui/term.js +2 -0
- package/src/commands/attach.js +0 -262
- package/src/commands/audit.js +0 -150
- package/src/commands/auth.js +0 -99
- package/src/commands/backup.js +0 -106
- package/src/commands/demo.js +0 -12
- package/src/commands/doctor.js +0 -81
- package/src/commands/pr.js +0 -322
- package/src/commands/repos.js +0 -122
- package/src/commands/restore.js +0 -125
- package/src/commands/splash.js +0 -95
- package/src/commands/status.js +0 -84
- package/src/commands/transfer.js +0 -79
- package/src/commands/tunnel.js +0 -116
- package/src/commands/watch.js +0 -347
- package/src/connected.js +0 -82
- package/src/data/live.js +0 -49
- package/src/data/sample.js +0 -163
- package/src/data/store.js +0 -145
package/src/commands/pr.js
DELETED
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
// gitroots pr list · view · diff · approve · review · merge
|
|
2
|
-
import { c, h, pad } from '../ui/ansi.js';
|
|
3
|
-
import { line, runSteps, confirm } from '../ui/term.js';
|
|
4
|
-
import { dot, json, next } from '../ui/components.js';
|
|
5
|
-
import { tryGit } from '../git/checks.js';
|
|
6
|
-
import { sampleNote } from '../connected.js';
|
|
7
|
-
|
|
8
|
-
export async function run(_cmd, rest, flags, store) {
|
|
9
|
-
const [sub = 'list', ...args] = rest;
|
|
10
|
-
switch (sub) {
|
|
11
|
-
case 'list': case 'ls': return list(args, flags, store);
|
|
12
|
-
case 'view': case 'show': return view(args, flags, store);
|
|
13
|
-
case 'diff': return diff(args, flags, store);
|
|
14
|
-
case 'approve': return approve(args, flags, store);
|
|
15
|
-
case 'review': return review(args, flags, store);
|
|
16
|
-
case 'merge': return merge(args, flags, store);
|
|
17
|
-
case 'create': case 'new': case 'open': return create(args, flags, store);
|
|
18
|
-
default:
|
|
19
|
-
if (/^#?\d+$/.test(sub)) return view([sub], flags, store);
|
|
20
|
-
throw new Error(`unknown: gitroots pr ${sub} · list, view, diff, create, approve, review, merge`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function stateCell(p, me) {
|
|
25
|
-
if (p.state === 'merged') return c.g(`✓ merged ${p.mergedAt || ''}`.trim());
|
|
26
|
-
if (p.state === 'closed') return c.m('○ closed');
|
|
27
|
-
if (p.state === 'draft') return c.m('○ draft');
|
|
28
|
-
if (p.checks.state === 'fail') return c.r('✕ checks failed');
|
|
29
|
-
if (p.checks.state === 'running') return c.i('○ checks running');
|
|
30
|
-
if (p.changesRequested) return c.r('✕ changes requested');
|
|
31
|
-
if (p.needs && p.needs.who === me && !p.approvals.some((a) => a.by === me)) return c.w('◐ needs you');
|
|
32
|
-
if (p.approvals.length >= p.required) return c.g('✓ ready to merge');
|
|
33
|
-
return c.m(`○ waiting for ${p.required - p.approvals.length} more`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function checksCell(p) {
|
|
37
|
-
return p.checks.state === 'pass' ? c.g('✓') : p.checks.state === 'fail' ? c.r('✕') : c.i('⠼');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function list([repo], flags, store) {
|
|
41
|
-
const want = flags.state || 'all';
|
|
42
|
-
let prs = store.prs(repo).filter((p) => want === 'all' || p.state === want);
|
|
43
|
-
prs = [...prs].sort((a, b) => b.number - a.number);
|
|
44
|
-
if (flags.json) { json(prs); return 0; }
|
|
45
|
-
const me = store.org.user;
|
|
46
|
-
line(` ${h(`${pad('#', 7)}${pad('TITLE', 40)}${pad('FROM → INTO', 35)}${pad('AUTHOR', 10)}${pad('APPROVALS', 12)}${pad('CHECKS', 9)}STATE`)}`);
|
|
47
|
-
for (const p of prs) {
|
|
48
|
-
const num = stateCell(p, me).includes('needs you') ? c.b(pad(String(p.number), 7)) : pad(String(p.number), 7);
|
|
49
|
-
const agent = p.agent ? ` ${c.m(p.agent)}` : '';
|
|
50
|
-
line(` ${num}${pad(p.title, 40)}${pad(`${p.from} → ${p.into}`, 35)}${pad(p.author, 10)}${pad(`${p.approvals.length} / ${p.required}`, 12)}${pad(checksCell(p), 9)}${stateCell(p, me)}${agent}`);
|
|
51
|
-
}
|
|
52
|
-
if (!prs.length) line(c.m(' no pull requests'));
|
|
53
|
-
const mine = prs.find((p) => stateCell(p, me).includes('needs you'));
|
|
54
|
-
sampleNote(store, 'pull requests are');
|
|
55
|
-
line();
|
|
56
|
-
line(next(`gitroots pr view ${(mine || prs[0] || { number: '<n>' }).number}`));
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function needPr(args, store) {
|
|
61
|
-
if (!args[0]) throw new Error('which pull request? e.g. gitroots pr view 412');
|
|
62
|
-
return store.pr(args[0]);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function view(args, flags, store) {
|
|
66
|
-
const p = needPr(args, store);
|
|
67
|
-
if (flags.json) { json(p); return 0; }
|
|
68
|
-
const me = store.org.user;
|
|
69
|
-
const add = p.files.reduce((n, f) => n + f.add, 0);
|
|
70
|
-
const del = p.files.reduce((n, f) => n + f.del, 0);
|
|
71
|
-
line(` ${c.b(`#${p.number} ${p.title}`)} ${c.m(`${p.repo} · ${p.from} → ${p.into} · opened by ${p.author} · ${p.opened}`)}`);
|
|
72
|
-
line();
|
|
73
|
-
if (p.files.length) {
|
|
74
|
-
line(` ${c.s(pad('files', 12))}${p.files.length} changed ${c.g(`+${add}`)} ${c.r(`−${del}`)}`);
|
|
75
|
-
for (const f of p.files.slice(0, 3)) {
|
|
76
|
-
line(` ${' '.repeat(12)}${pad(f.path, 51)}${c.g(pad(`+${f.add}`, 5))}${c.r(`−${f.del}`)}${f.flag ? ` ${c.w(`◐ ${f.flag}`)}` : ''}`);
|
|
77
|
-
}
|
|
78
|
-
if (p.files.length > 3) line(` ${' '.repeat(12)}${c.m(`+ ${p.files.length - 3} more · gitroots pr diff ${p.number}`)}`);
|
|
79
|
-
} else {
|
|
80
|
-
line(` ${c.s(pad('commits', 12))}${p.commits}`);
|
|
81
|
-
}
|
|
82
|
-
const ch = p.checks;
|
|
83
|
-
const checks = ch.state === 'pass'
|
|
84
|
-
? `${c.g('✓')} tests ${ch.tests || 'pass'} ${c.g('✓')} lint ${c.g('✓')} secrets scan ${c.g('✓')} branch rules`
|
|
85
|
-
: ch.state === 'fail' ? `${c.r('✕')} ${ch.failed}` : `${c.i('⠼')} running`;
|
|
86
|
-
line(` ${c.s(pad('checks', 12))}${checks}`);
|
|
87
|
-
const appr = p.approvals.map((a) => `${c.g('✓')} ${a.by} ${c.m(`· ${a.ago}`)}`);
|
|
88
|
-
const missing = p.required - p.approvals.length;
|
|
89
|
-
if (missing > 0) {
|
|
90
|
-
if (p.needs && !p.approvals.some((a) => a.by === p.needs.who)) appr.push(`${c.w('○')} ${p.needs.who === me ? 'you' : p.needs.who} ${c.m(`(needed · ${p.needs.why})`)}`);
|
|
91
|
-
const others = missing - (p.needs && !p.approvals.some((a) => a.by === p.needs.who) ? 1 : 0);
|
|
92
|
-
if (others > 0) appr.push(c.m(`○ ${others} more`));
|
|
93
|
-
}
|
|
94
|
-
line(` ${c.s(pad('approvals', 12))}${appr.join(' ')}`);
|
|
95
|
-
if (p.changesRequested) line(` ${c.s(pad('review', 12))}${c.r('✕')} changes requested by ${p.changesRequested.by} ${c.m(`· ${p.changesRequested.note}`)}`);
|
|
96
|
-
for (const n of p.notes) line(` ${c.s(pad('notes', 12))}${c.w('◐')} ${n}`);
|
|
97
|
-
line();
|
|
98
|
-
if (p.state === 'merged') line(next(`gitroots audit --pr ${p.number}`));
|
|
99
|
-
else if (missing > 0 && !p.approvals.some((a) => a.by === me)) line(` ${c.g(`→ gitroots pr approve ${p.number}`)} ${c.m('or')} ${c.s(`gitroots pr review ${p.number} --request-changes`)} ${dot()} ${c.s(`gitroots pr diff ${p.number}`)}`);
|
|
100
|
-
else if (missing <= 0) line(` ${next(`gitroots pr merge ${p.number}`)}`);
|
|
101
|
-
else line(` ${c.m(`waiting for ${missing} more approval${missing > 1 ? 's' : ''}`)}`);
|
|
102
|
-
return 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function diff(args, flags, store) {
|
|
106
|
-
const p = needPr(args, store);
|
|
107
|
-
if (flags.json) { json(p.files); return 0; }
|
|
108
|
-
if (!p.files.length) { line(c.m(` no file list recorded for #${p.number}`)); return 0; }
|
|
109
|
-
const max = Math.max(...p.files.map((f) => f.add + f.del));
|
|
110
|
-
line(` ${c.b(`#${p.number}`)} ${c.m(`${p.from} → ${p.into}`)}`);
|
|
111
|
-
line();
|
|
112
|
-
for (const f of p.files) {
|
|
113
|
-
const n = Math.max(1, Math.round(((f.add + f.del) / max) * 30));
|
|
114
|
-
const a = Math.round((f.add / (f.add + f.del || 1)) * n);
|
|
115
|
-
line(` ${pad(f.path, 52)}${c.g(pad(`+${f.add}`, 6))}${c.r(pad(`−${f.del}`, 6))}${c.g('+'.repeat(a))}${c.r('−'.repeat(n - a))}${f.flag ? ` ${c.w(`◐ ${f.flag}`)}` : ''}`);
|
|
116
|
-
}
|
|
117
|
-
line();
|
|
118
|
-
line(next(`gitroots pr approve ${p.number}`));
|
|
119
|
-
return 0;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function approve(args, flags, store) {
|
|
123
|
-
const p = needPr(args, store);
|
|
124
|
-
const me = store.org.user;
|
|
125
|
-
if (p.state !== 'open') throw new Error(`#${p.number} is ${p.state}`);
|
|
126
|
-
if (p.author === me) throw new Error(`you opened #${p.number} · someone else has to approve it`);
|
|
127
|
-
if (p.approvals.some((a) => a.by === me)) {
|
|
128
|
-
if (flags.json) { json({ pr: p.number, approvals: p.approvals.length, already: true }); return 0; }
|
|
129
|
-
line(` ${c.m('○')} You already approved #${p.number} ${dot()} ${p.approvals.length} / ${p.required} approvals`);
|
|
130
|
-
return 0;
|
|
131
|
-
}
|
|
132
|
-
p.approvals.push({ by: me, ago: 'just now', note: flags.m || flags.message || '' });
|
|
133
|
-
if (p.changesRequested && p.changesRequested.by === me) delete p.changesRequested;
|
|
134
|
-
const extra = p.needs && p.needs.who === me ? ` ${dot()} ${p.needs.why.replace(/^tech lead for /, 'tech lead sign-off for ')} recorded` : '';
|
|
135
|
-
const ev = store.record({ actor: me, action: 'approve', target: `PR #${p.number}`, decision: 'recorded', note: flags.m || '', repo: p.repo, pr: p.number });
|
|
136
|
-
store.save();
|
|
137
|
-
if (flags.json) { json({ pr: p.number, approvals: p.approvals.length, required: p.required, event: ev.id }); return 0; }
|
|
138
|
-
line(` ${c.g('✓')} Approved #${p.number} ${dot()} ${p.approvals.length} / ${p.required} approvals${extra} ${c.m(`· ${ev.id}`)}`);
|
|
139
|
-
line();
|
|
140
|
-
line(p.approvals.length >= p.required ? next(`gitroots pr merge ${p.number}`) : c.m(` waiting for ${p.required - p.approvals.length} more`));
|
|
141
|
-
return 0;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function review(args, flags, store) {
|
|
145
|
-
const p = needPr(args, store);
|
|
146
|
-
const me = store.org.user;
|
|
147
|
-
const note = flags.m || flags.message || '';
|
|
148
|
-
if (flags.approve) return approve(args, flags, store);
|
|
149
|
-
if (flags['request-changes']) {
|
|
150
|
-
if (!note) throw new Error('say what needs to change: -m "…"');
|
|
151
|
-
p.changesRequested = { by: me, note };
|
|
152
|
-
p.approvals = p.approvals.filter((a) => a.by !== me);
|
|
153
|
-
const ev = store.record({ actor: me, action: 'request changes', target: `PR #${p.number}`, decision: 'recorded', note, repo: p.repo, pr: p.number });
|
|
154
|
-
store.save();
|
|
155
|
-
if (flags.json) { json({ pr: p.number, changesRequested: true, event: ev.id }); return 0; }
|
|
156
|
-
line(` ${c.r('✕')} Changes requested on #${p.number} ${dot()} ${p.author} notified ${c.m(`· ${ev.id}`)}`);
|
|
157
|
-
return 0;
|
|
158
|
-
}
|
|
159
|
-
if (!note) throw new Error('usage: gitroots pr review <n> --request-changes -m "…" | --approve | --comment -m "…"');
|
|
160
|
-
const ev = store.record({ actor: me, action: 'comment', target: `PR #${p.number}`, decision: 'recorded', note, repo: p.repo, pr: p.number });
|
|
161
|
-
store.save();
|
|
162
|
-
if (flags.json) { json({ pr: p.number, comment: note, event: ev.id }); return 0; }
|
|
163
|
-
line(` ${c.g('✓')} Comment added to #${p.number} ${c.m(`· ${ev.id}`)}`);
|
|
164
|
-
return 0;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function merge(args, flags, store) {
|
|
168
|
-
const p = needPr(args, store);
|
|
169
|
-
const me = store.org.user;
|
|
170
|
-
if (p.state === 'draft') throw new Error(`#${p.number} is a draft · mark it ready first`);
|
|
171
|
-
if (p.state !== 'open') throw new Error(`#${p.number} is already ${p.state}`);
|
|
172
|
-
const rule = store.ruleFor(p.repo, p.into);
|
|
173
|
-
const method = flags.rebase ? 'rebase' : flags['merge-commit'] ? 'merge commit' : 'squash';
|
|
174
|
-
const json_ = Boolean(flags.json);
|
|
175
|
-
|
|
176
|
-
if (!json_) {
|
|
177
|
-
line(` ${c.b(`#${p.number} ${p.title}`)} ${c.m(`${p.from} → ${p.into} · ${p.commits} commits · ${method}`)}`);
|
|
178
|
-
const ok = await confirm(`Merge into ${c.b(p.into)}?`, { assumeYes: flags.yes || flags.y });
|
|
179
|
-
if (!ok) { line(c.m(' nothing merged')); return 1; }
|
|
180
|
-
line();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const conflictBase = store.branches(p.repo).find((b) => b.name === p.into)?.commit || 'HEAD';
|
|
184
|
-
const steps = [
|
|
185
|
-
{
|
|
186
|
-
name: 'Approvals', live: 'counting approvals…', ms: 600,
|
|
187
|
-
run: async () => (p.approvals.length >= p.required
|
|
188
|
-
? { detail: `${p.approvals.length} / ${p.required} · ${p.approvals.map((a) => a.by).join(', ')}` }
|
|
189
|
-
: { status: 'fail', detail: `${p.approvals.length} / ${p.required} · needs ${p.required - p.approvals.length} more` }),
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
name: 'Checks', live: 'waiting for the latest run…', ms: 900,
|
|
193
|
-
run: async () => (p.checks.state === 'pass' ? { detail: `tests ${p.checks.tests || 'pass'} · lint · secrets scan` }
|
|
194
|
-
: p.checks.state === 'fail' ? { status: 'fail', detail: p.checks.failed } : { status: 'fail', detail: 'still running · try again when they finish' }),
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
name: 'Branch rules', live: `checking ${p.into} rules…`, ms: 600,
|
|
198
|
-
run: async () => (p.changesRequested ? { status: 'fail', detail: `changes requested by ${p.changesRequested.by}` }
|
|
199
|
-
: { detail: `${p.into}: ${rule.protected ? `PR only, ${rule.approvals} approvals, checks green` : 'open branch'}` }),
|
|
200
|
-
},
|
|
201
|
-
{ name: 'Conflicts', live: `test-merging into ${p.into}…`, ms: 800, ok: `none with ${p.into} at ${conflictBase}` },
|
|
202
|
-
{ name: 'Merge', live: `pushing to ${store.repo(p.repo).remote}…`, ms: 1000, ok: method === 'squash' ? `squashed ${p.commits} commits` : method === 'rebase' ? `rebased ${p.commits} commits` : 'merge commit created' },
|
|
203
|
-
];
|
|
204
|
-
|
|
205
|
-
const results = json_ ? await silentSteps(steps) : await runSteps(steps, { nameWidth: 16, stopOnFail: true, showQueued: true });
|
|
206
|
-
const failed = results.find((r) => r.status === 'fail');
|
|
207
|
-
if (failed) {
|
|
208
|
-
const ev = store.record({ actor: me, action: 'merge', target: `PR #${p.number} → ${p.into}`, decision: 'blocked', note: failed.detail, repo: p.repo, pr: p.number });
|
|
209
|
-
store.save();
|
|
210
|
-
if (json_) { json({ merged: false, pr: p.number, reason: failed.detail, event: ev.id }); return 1; }
|
|
211
|
-
line();
|
|
212
|
-
line(` ${c.r(c.b(`✕ Not merged`))} ${failed.name.trim()}: ${failed.detail} ${c.m(`· ${ev.id}`)}`);
|
|
213
|
-
line();
|
|
214
|
-
line(next(`gitroots pr view ${p.number}`));
|
|
215
|
-
return 1;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const sha = Math.floor(Math.random() * 0xfffffff).toString(16).padStart(7, '0');
|
|
219
|
-
const now = new Date();
|
|
220
|
-
const hhmm = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
221
|
-
p.state = 'merged';
|
|
222
|
-
p.mergedAt = hhmm;
|
|
223
|
-
p.mergeSha = sha;
|
|
224
|
-
const branches = store.d.branches[p.repo] || [];
|
|
225
|
-
const target = branches.find((b) => b.name === p.into);
|
|
226
|
-
if (target) { target.commit = sha; target.by = me; target.age = 'now'; }
|
|
227
|
-
if (!flags['keep-branch']) {
|
|
228
|
-
const idx = branches.findIndex((b) => b.name === p.from);
|
|
229
|
-
if (idx > -1) {
|
|
230
|
-
store.d.deletedBranches = store.d.deletedBranches || {};
|
|
231
|
-
(store.d.deletedBranches[p.repo] = store.d.deletedBranches[p.repo] || []).push(branches[idx]);
|
|
232
|
-
branches.splice(idx, 1);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
const repo = store.repo(p.repo);
|
|
236
|
-
repo.openPrs = Math.max(0, repo.openPrs - 1);
|
|
237
|
-
repo.lastPush = 'just now';
|
|
238
|
-
const ev = store.record({ actor: me, action: 'merge', target: `PR #${p.number} → ${p.into}`, decision: 'allowed', note: `${p.approvals.length} approvals · checks green`, repo: p.repo, pr: p.number });
|
|
239
|
-
store.save();
|
|
240
|
-
|
|
241
|
-
if (json_) { json({ merged: true, pr: p.number, into: p.into, sha, event: ev.id }); return 0; }
|
|
242
|
-
const ahead = store.prs(p.repo).find((x) => x.state === 'open' && x.from === p.into);
|
|
243
|
-
line();
|
|
244
|
-
line(` ${c.g(c.b(`✓ Merged #${p.number} into ${p.into}`))} ${c.m('as')} ${sha} ${c.m(`· by ${me} · ${hhmm}`)}`);
|
|
245
|
-
if (!flags['keep-branch']) line(` ${c.s(pad('branch', 10))}${p.from} deleted ${c.m(`(restore with gitroots branch restore ${p.repo} ${p.from})`)}`);
|
|
246
|
-
line(` ${c.s(pad('audit', 10))}${ev.id} ${dot()} approvals, checks and the merge are recorded together`);
|
|
247
|
-
if (ahead) line(` ${c.s(pad('next', 10))}release PR #${ahead.number} waits for ${ahead.required - ahead.approvals.length} approval${ahead.required - ahead.approvals.length === 1 ? '' : 's'}`);
|
|
248
|
-
line();
|
|
249
|
-
line(` ${next(ahead ? `gitroots pr view ${ahead.number}` : `gitroots pr list ${p.repo}`, `gitroots audit --pr ${p.number}`)}`);
|
|
250
|
-
return 0;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function silentSteps(steps) {
|
|
254
|
-
const out = [];
|
|
255
|
-
for (const s of steps) {
|
|
256
|
-
const r = s.run ? await s.run({ progress() {} }) : {};
|
|
257
|
-
const res = { ...s, ...r, status: r.status || 'ok', detail: r.detail ?? s.ok };
|
|
258
|
-
out.push(res);
|
|
259
|
-
if (res.status === 'fail') break;
|
|
260
|
-
}
|
|
261
|
-
return out;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// gitroots pr create [--base staging] [--head branch] [--title "…"] [--repo erp] [--draft]
|
|
265
|
-
function create(args, flags, store) {
|
|
266
|
-
const cwd = process.cwd();
|
|
267
|
-
const inGit = Boolean(tryGit(['rev-parse', '--show-toplevel'], cwd));
|
|
268
|
-
const head = flags.head || (inGit ? tryGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd) : '');
|
|
269
|
-
if (!head) throw new Error('which branch? run inside a git repo or pass --head <branch>');
|
|
270
|
-
if (inGit && flags.head && !tryGit(['rev-parse', '--verify', '--quiet', flags.head], cwd)) throw new Error(`no branch ${flags.head} in this repo`);
|
|
271
|
-
const url = inGit ? tryGit(['remote', 'get-url', 'origin'], cwd) : '';
|
|
272
|
-
const repo = flags.repo ? store.repo(flags.repo) : store.repoByRemote(url);
|
|
273
|
-
if (!repo) throw new Error(`this repository is not in ${store.org.name} · pass --repo <name>`);
|
|
274
|
-
const base = flags.base || flags.into || (store.ruleFor(repo.name, 'staging').protected ? 'staging' : repo.default);
|
|
275
|
-
if (head === base) throw new Error(`head and base are both ${base} · switch to your feature branch`);
|
|
276
|
-
const rule = store.ruleFor(repo.name, base);
|
|
277
|
-
if (store.prs(repo.name).some((p) => p.state === 'open' && p.from === head && p.into === base)) throw new Error(`a PR from ${head} into ${base} is already open`);
|
|
278
|
-
|
|
279
|
-
// Real numbers when the commits are here: files and lines changed against the base.
|
|
280
|
-
let files = [];
|
|
281
|
-
let commits = 0;
|
|
282
|
-
if (inGit) {
|
|
283
|
-
const baseRef = ['origin/' + base, base].find((r) => tryGit(['rev-parse', '--verify', '--quiet', r], cwd));
|
|
284
|
-
if (baseRef) {
|
|
285
|
-
commits = Number(tryGit(['rev-list', '--count', `${baseRef}..${head}`], cwd)) || 0;
|
|
286
|
-
files = tryGit(['diff', '--numstat', `${baseRef}...${head}`], cwd).split('\n').filter(Boolean).map((l) => {
|
|
287
|
-
const [a, d, p] = l.split('\t');
|
|
288
|
-
return { path: p, add: Number(a) || 0, del: Number(d) || 0 };
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const paths = store.protectedPaths(repo.name);
|
|
293
|
-
const notes = [];
|
|
294
|
-
let needs;
|
|
295
|
-
for (const f of files) {
|
|
296
|
-
if (/(^|\/)migrations?\//.test(f.path)) { f.flag = 'changes data'; needs = { who: store.org.user, why: 'tech lead for migrations' }; }
|
|
297
|
-
const hit = paths.find((p) => store.matchPath(p.pattern, f.path));
|
|
298
|
-
if (hit) { f.flag = `needs ${hit.needs}`; notes.push(`${f.path} needs ${hit.needs}`); }
|
|
299
|
-
}
|
|
300
|
-
const title = flags.title || flags.t || (inGit ? tryGit(['log', '-1', '--format=%s', head], cwd) : '') || head;
|
|
301
|
-
const number = Math.max(...store.d.prs.map((p) => p.number)) + 1;
|
|
302
|
-
const pr = {
|
|
303
|
-
number, repo: repo.name, title, from: head, into: base, author: store.org.user, opened: 'just now', at: new Date().toISOString(),
|
|
304
|
-
required: rule.approvals || 1, approvals: [], checks: { state: 'running' }, state: flags.draft ? 'draft' : 'open', commits: commits || 1, files, notes,
|
|
305
|
-
};
|
|
306
|
-
if (needs) pr.needs = needs;
|
|
307
|
-
store.d.prs.push(pr);
|
|
308
|
-
repo.openPrs += 1;
|
|
309
|
-
const ev = store.record({ actor: store.org.user, action: 'open pr', target: `PR #${number} ${head} → ${base}`, decision: 'allowed', repo: repo.name, pr: number });
|
|
310
|
-
store.save();
|
|
311
|
-
if (flags.json) { json({ ...pr, event: ev.id }); return 0; }
|
|
312
|
-
const add = files.reduce((n, f) => n + f.add, 0);
|
|
313
|
-
const del = files.reduce((n, f) => n + f.del, 0);
|
|
314
|
-
line(` ${c.g('✓')} ${c.b(`Opened #${number} ${title}`)}${flags.draft ? c.m(' (draft)') : ''}`);
|
|
315
|
-
line(` ${c.s(pad('branch', 11))}${head} → ${base} ${dot()} ${repo.name}`);
|
|
316
|
-
if (files.length) line(` ${c.s(pad('changes', 11))}${files.length} files ${c.g(`+${add}`)} ${c.r(`−${del}`)} ${dot()} ${commits} commit${commits === 1 ? '' : 's'}`);
|
|
317
|
-
line(` ${c.s(pad('rules', 11))}${rule.protected ? `${rule.approvals} approval${rule.approvals === 1 ? '' : 's'} · checks green` : '1 approval'}${notes.length ? ` ${dot()} ${c.w(`◐ ${notes.length} protected path${notes.length > 1 ? 's' : ''}`)}` : ''}`);
|
|
318
|
-
line(` ${c.s(pad('checks', 11))}${c.i('○ running')}`);
|
|
319
|
-
line();
|
|
320
|
-
line(next(`gitroots pr view ${number}`));
|
|
321
|
-
return 0;
|
|
322
|
-
}
|
package/src/commands/repos.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// gitroots repo list · branch list · branch rules · clone
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { c, h, pad } from '../ui/ansi.js';
|
|
5
|
-
import { line } from '../ui/term.js';
|
|
6
|
-
import { dot, json, next } from '../ui/components.js';
|
|
7
|
-
import { installHook } from './push.js';
|
|
8
|
-
import { apiRepos, apiBranches } from '../connected.js';
|
|
9
|
-
|
|
10
|
-
export async function run(cmd, rest, flags, store) {
|
|
11
|
-
if (cmd === 'clone') return clone(rest, flags, store);
|
|
12
|
-
const [sub = 'list', ...args] = rest;
|
|
13
|
-
if (cmd === 'repo' || cmd === 'repos') {
|
|
14
|
-
if (sub === 'list' || sub === 'ls') return repoList(flags, store);
|
|
15
|
-
if (sub === 'view') return branchRules([args[0], store.repo(args[0]).default], flags, store);
|
|
16
|
-
throw new Error(`unknown: gitroots repo ${sub} · try gitroots repo list`);
|
|
17
|
-
}
|
|
18
|
-
if (sub === 'list' || sub === 'ls') return branchList(args, flags, store);
|
|
19
|
-
if (sub === 'rules') return branchRules(args, flags, store);
|
|
20
|
-
if (sub === 'restore') return branchRestore(args, flags, store);
|
|
21
|
-
throw new Error(`unknown: gitroots branch ${sub} · try gitroots branch list <repo>`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function repoList(flags, store) {
|
|
25
|
-
const repos = store.api ? await apiRepos(store) : store.repos();
|
|
26
|
-
if (flags.json) { json(repos); return 0; }
|
|
27
|
-
line(` ${h(`${pad('NAME', 14)}${pad('PROVIDER', 11)}${pad('DEFAULT', 10)}${pad('PROTECTED', 12)}${pad('BRANCHES', 11)}${pad('OPEN PRS', 11)}${pad('YOUR ROLE', 14)}LAST PUSH`)}`);
|
|
28
|
-
for (const r of repos) {
|
|
29
|
-
line(` ${c.b(pad(r.name, 14))}${pad(r.provider, 11)}${pad(r.default, 10)}${pad(r.protected.length ? String(r.protected.length) : c.m('—'), 12)}${pad(String(r.branches), 11)}${pad(r.openPrs === null ? c.m('—') : String(r.openPrs), 11)}${pad(r.role, 14)}${r.lastPush || c.m('—')}`);
|
|
30
|
-
}
|
|
31
|
-
if (!repos.length) line(c.m(store.api ? ' no repositories synced yet · connect GitHub in the gitroots dashboard' : ' no repositories'));
|
|
32
|
-
line();
|
|
33
|
-
line(next(`gitroots branch list ${repos[0]?.name || '<repo>'}`));
|
|
34
|
-
return 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function ruleCell(b) {
|
|
38
|
-
return b.rule === 'protected' ? `${c.g(pad('● protected', 12))} ${pad(b.ruleText, 28)}` : `${c.i(pad('○ open', 12))} ${pad(b.ruleText, 28)}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function branchList([repoName], flags, store) {
|
|
42
|
-
if (!repoName) throw new Error('which repository? gitroots branch list <repo>');
|
|
43
|
-
const all = store.api ? await apiBranches(store, repoName) : store.branches(repoName);
|
|
44
|
-
const shown = flags.all ? all : all.slice(0, 6);
|
|
45
|
-
if (flags.json) { json(all); return 0; }
|
|
46
|
-
line(` ${h(`${pad('BRANCH', 24)}${pad('RULE', 41)}${pad('APPROVALS', 12)}${pad('LAST COMMIT', 25)}AHEAD / BEHIND`)}`);
|
|
47
|
-
for (const b of shown) {
|
|
48
|
-
const name = b.rule === 'protected' ? c.b(pad(b.name, 24)) : pad(b.name, 24);
|
|
49
|
-
const appr = b.approvals ? pad(String(b.approvals), 12) : c.m(pad('—', 12));
|
|
50
|
-
line(` ${name}${ruleCell(b)}${appr}${pad(b.commit ? `${b.commit} ${b.by} · ${b.age}` : c.m('—'), 25)}${c.m(b.aheadBehind || '—')}`);
|
|
51
|
-
}
|
|
52
|
-
if (all.length > shown.length) line(` ${c.m(`+ ${all.length - shown.length} more · gitroots branch list ${repoName} --all`)}`);
|
|
53
|
-
line();
|
|
54
|
-
const prot = all.find((b) => b.rule === 'protected' && b.name !== 'main') || all[0];
|
|
55
|
-
if (prot) line(next(`gitroots branch rules ${repoName} ${prot.name}`));
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function branchRules([repoName, branch], flags, store) {
|
|
60
|
-
if (!repoName || !branch) throw new Error('usage: gitroots branch rules <repo> <branch>');
|
|
61
|
-
if (!store.api) store.repo(repoName);
|
|
62
|
-
const rule = store.ruleFor(repoName, branch);
|
|
63
|
-
const paths = store.protectedPaths(repoName);
|
|
64
|
-
const agents = store.rules(repoName).agents;
|
|
65
|
-
if (flags.json) { json({ repo: repoName, branch, rule, paths, agents }); return 0; }
|
|
66
|
-
line(` ${c.b(branch)} ${rule.protected ? c.g('● protected') : c.i('○ open')}`);
|
|
67
|
-
if (rule.protected) {
|
|
68
|
-
line(` ${pad('push', 13)}${c.r('✕')} ${pad(rule.push, 44)} ${c.m('changes arrive through PRs')}`);
|
|
69
|
-
line(` ${pad('merge', 13)}${c.g('✓')} ${rule.mergeText}`);
|
|
70
|
-
} else {
|
|
71
|
-
line(` ${pad('push', 13)}${c.g('✓')} ${rule.label}`);
|
|
72
|
-
line(` ${pad('merge', 13)}${c.g('✓')} through a PR into a protected branch`);
|
|
73
|
-
}
|
|
74
|
-
for (const p of paths) line(` ${pad('paths', 13)}${c.w('◐')} ${p.pattern} needs ${p.needs}`);
|
|
75
|
-
if (agents) line(` ${pad('agents', 13)}${c.g('✓')} ${agents}`);
|
|
76
|
-
line();
|
|
77
|
-
line(next(`gitroots clone ${repoName}`));
|
|
78
|
-
return 0;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function branchRestore([repoName, branch], flags, store) {
|
|
82
|
-
if (!repoName || !branch) throw new Error('usage: gitroots branch restore <repo> <branch>');
|
|
83
|
-
store.repo(repoName);
|
|
84
|
-
const deleted = (store.d.deletedBranches || {})[repoName] || [];
|
|
85
|
-
const b = deleted.find((x) => x.name === branch);
|
|
86
|
-
if (!b) throw new Error(`no deleted branch ${branch} in ${repoName}`);
|
|
87
|
-
store.d.deletedBranches[repoName] = deleted.filter((x) => x !== b);
|
|
88
|
-
store.d.branches[repoName].push(b);
|
|
89
|
-
const ev = store.record({ actor: store.org.user, action: 'branch restore', target: `${repoName} ${branch}`, decision: 'allowed', repo: repoName });
|
|
90
|
-
store.save();
|
|
91
|
-
if (flags.json) { json({ restored: b, event: ev.id }); return 0; }
|
|
92
|
-
line(` ${c.g('✓')} Restored ${c.b(branch)} at ${b.commit} ${c.m(`audit ${ev.id}`)}`);
|
|
93
|
-
return 0;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function clone([target, dirArg], flags, store) {
|
|
97
|
-
if (!target) throw new Error('usage: gitroots clone <repo|git-url> [dir]');
|
|
98
|
-
const isUrl = /[:/]/.test(target);
|
|
99
|
-
const repo = isUrl ? store.repoByRemote(target) : store.repo(target);
|
|
100
|
-
const name = repo ? repo.name : path.basename(target).replace(/\.git$/, '');
|
|
101
|
-
const url = flags.url || (isUrl ? target : null);
|
|
102
|
-
const dir = path.resolve(dirArg || name);
|
|
103
|
-
const shown = dir.replace(process.env.HOME || '\u0000', '~');
|
|
104
|
-
|
|
105
|
-
if (!url) {
|
|
106
|
-
// Demo org repositories have no reachable remote: show what clone does.
|
|
107
|
-
if (flags.json) { json({ repo: name, dir, cloned: false, reason: 'demo repository has no remote · pass --url' }); return 0; }
|
|
108
|
-
line(` ${c.g('✓')} Cloned ${name} into ${shown} ${c.m(`${repo?.branches ?? '?'} branches · default ${repo?.default ?? 'main'}`)}`);
|
|
109
|
-
line(` ${c.g('✓')} Push checks installed ${c.m('git push now runs gitroots checks first')} ${c.g(`→ cd ${name} && git switch -c feature/…`)}`);
|
|
110
|
-
line(c.m(` sample data · nothing was cloned · gitroots clone ${name} --url <git-url> clones for real`));
|
|
111
|
-
return 0;
|
|
112
|
-
}
|
|
113
|
-
const r = spawnSync('git', ['clone', url, dir], { stdio: flags.json ? 'ignore' : ['ignore', 'ignore', 'inherit'] });
|
|
114
|
-
if (r.status !== 0) { line(` ${c.r('✕')} git clone failed ${c.m(`(exit ${r.status})`)}`); return r.status || 1; }
|
|
115
|
-
const branches = spawnSync('git', ['-C', dir, 'branch', '-r'], { encoding: 'utf8' }).stdout.split('\n').filter((l) => l.trim() && !l.includes('->')).length;
|
|
116
|
-
const def = (spawnSync('git', ['-C', dir, 'symbolic-ref', '--short', 'HEAD'], { encoding: 'utf8' }).stdout || 'main').trim();
|
|
117
|
-
const hook = installHook(dir, { force: true, repo: name });
|
|
118
|
-
if (flags.json) { json({ repo: name, dir, cloned: true, branches, default: def, hook: hook.path }); return 0; }
|
|
119
|
-
line(` ${c.g('✓')} Cloned ${name} into ${shown} ${c.m(`${branches} branches ${dot()} default ${def}`)}`);
|
|
120
|
-
line(` ${c.g('✓')} Push checks installed ${c.m('git push now runs gitroots checks first')} ${c.g(`→ cd ${path.basename(dir)} && git switch -c feature/…`)}`);
|
|
121
|
-
return 0;
|
|
122
|
-
}
|
package/src/commands/restore.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// gitroots restore <session|backup> [--machine m] [--pick] [--point turn|b-id] [--fork]
|
|
2
|
-
import { c, h, pad, fit } from '../ui/ansi.js';
|
|
3
|
-
import { line, runSteps, select, interactive, size } from '../ui/term.js';
|
|
4
|
-
import { dot, json, next, sessionState } from '../ui/components.js';
|
|
5
|
-
|
|
6
|
-
export async function run(_cmd, rest, flags, store) {
|
|
7
|
-
const [target] = rest;
|
|
8
|
-
if (!target) throw new Error('usage: gitroots restore <session|backup> [--machine m] [--pick] [--fork]');
|
|
9
|
-
return restore(store, target, flags);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function pointsFor(store, s) {
|
|
13
|
-
return store.backups().filter((b) => b.session === s.id).sort((a, b) => b.turn - a.turn);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function panels(left, right, total) {
|
|
17
|
-
const w = Math.floor((total - 2) / 2);
|
|
18
|
-
const inner = w - 4;
|
|
19
|
-
const rows = Math.max(left.length, right.length);
|
|
20
|
-
const out = [];
|
|
21
|
-
const top = c.m(`╭${'─'.repeat(w - 2)}╮`);
|
|
22
|
-
const bot = c.m(`╰${'─'.repeat(w - 2)}╯`);
|
|
23
|
-
out.push(`${top} ${top}`);
|
|
24
|
-
for (let i = 0; i < rows; i++) {
|
|
25
|
-
out.push(`${c.m('│')} ${fit(left[i] || '', inner)} ${c.m('│')} ${c.m('│')} ${fit(right[i] || '', inner)} ${c.m('│')}`);
|
|
26
|
-
}
|
|
27
|
-
out.push(`${bot} ${bot}`);
|
|
28
|
-
return out.join('\n');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function restore(store, target, flags = {}) {
|
|
32
|
-
let s;
|
|
33
|
-
let point;
|
|
34
|
-
if (/^b-\d+$/.test(target)) { point = store.backup(target); s = store.session(point.session); } else s = store.session(target);
|
|
35
|
-
const points = pointsFor(store, s);
|
|
36
|
-
if (!points.length) throw new Error(`no backups for ${s.id} · gitroots backup ${s.id} makes one`);
|
|
37
|
-
if (!point && flags.point) {
|
|
38
|
-
point = /^b-/.test(flags.point) ? store.backup(flags.point) : points.find((p) => String(p.turn) === String(flags.point).replace(/^turn\s*/, ''));
|
|
39
|
-
if (!point) throw new Error(`no restore point ${flags.point} for ${s.id}`);
|
|
40
|
-
}
|
|
41
|
-
const machine = store.machine(flags.machine || s.machine);
|
|
42
|
-
if (!machine.online) throw new Error(`${machine.name} is offline · pick an online machine with --machine`);
|
|
43
|
-
let fork = Boolean(flags.fork);
|
|
44
|
-
|
|
45
|
-
if (!point && flags.pick && !flags.json) {
|
|
46
|
-
if (!interactive()) throw new Error('--pick needs a terminal · use --point <turn|backup>');
|
|
47
|
-
const latest = points[0];
|
|
48
|
-
line(`${c.i(c.b('?'))} Restore ${c.b(s.id)} to which point? ${c.m(`${s.agent} · ${s.repo} · ${s.branch}`)}`);
|
|
49
|
-
line();
|
|
50
|
-
const cols = Math.min(size().cols, 140);
|
|
51
|
-
const res = await select({
|
|
52
|
-
count: points.length,
|
|
53
|
-
initial: Math.min(1, points.length - 1),
|
|
54
|
-
keys: { f: 'fork', d: 'diff' },
|
|
55
|
-
render: (idx) => {
|
|
56
|
-
const rows = [` ${h(`${pad('POINT', 12)}${pad('WHEN', 14)}${pad('LAST PROMPT', 46)}${pad('FILES', 9)}BACKUP`)}`];
|
|
57
|
-
points.forEach((p, i) => {
|
|
58
|
-
const sel = i === idx;
|
|
59
|
-
const text = `${pad(`turn ${p.turn}`, 12)}${pad(p.created, 14)}${pad(p.prompt || p.label, 46)}${pad(String(p.files), 9)}${p.id}`;
|
|
60
|
-
rows.push(sel ? ` ${c.g('❯')} ${c.bg('sel')(c.b(text))}` : ` ${text}`);
|
|
61
|
-
});
|
|
62
|
-
const p = points[idx];
|
|
63
|
-
const dropped = latest.turn - p.turn;
|
|
64
|
-
const left = [
|
|
65
|
-
h(`Preview · turn ${p.turn}`),
|
|
66
|
-
`${c.s(pad('workspace', 11))}HEAD ${p.head} · ${p.files} file${p.files === 1 ? '' : 's'} changed`,
|
|
67
|
-
...(p.diff || []).map((f) => ` ${pad(f.path, 26)}${c.g(pad(`+${f.add}`, 4))} ${c.r(`−${f.del}`)}`),
|
|
68
|
-
dropped > 0 ? `${c.s(pad('dropped', 11))}${dropped} later turns` : `${c.s(pad('dropped', 11))}nothing, this is the latest point`,
|
|
69
|
-
dropped > 0 ? `${' '.repeat(11)}${c.m(`kept safe in ${latest.id}, nothing is deleted`)}` : '',
|
|
70
|
-
];
|
|
71
|
-
const right = [
|
|
72
|
-
h('Target'),
|
|
73
|
-
`${c.s(pad('machine', 11))}${machine.name} · ${machine.os}${machine.self ? ' · this machine' : ''}`,
|
|
74
|
-
`${c.s(pad('folder', 11))}~/work/${s.repo}${s.changed ? ` · ${c.w('has uncommitted changes')}` : ''}`,
|
|
75
|
-
`${c.s(pad('plan', 11))}${s.changed ? 'stash local changes as gitroots/pre-restore' : 'checkout, apply, resume'}`,
|
|
76
|
-
s.changed ? `${' '.repeat(11)}then checkout, apply, resume` : '',
|
|
77
|
-
`${c.s(pad('resume', 11))}${s.resume} · same conversation`,
|
|
78
|
-
];
|
|
79
|
-
return `${rows.join('\n')}\n\n${panels(left, right, cols)}\n\n${c.m('↑↓ move enter restore f restore as a fork (keep ' + s.id + ' as is) esc cancel')}`;
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
if (res.action === 'cancel') { line(c.m(' nothing restored')); return 1; }
|
|
83
|
-
if (res.action === 'fork') fork = true;
|
|
84
|
-
point = points[res.index];
|
|
85
|
-
line();
|
|
86
|
-
}
|
|
87
|
-
point = point || points[0];
|
|
88
|
-
|
|
89
|
-
if (flags.json) {
|
|
90
|
-
const r = apply(store, s, point, machine, fork);
|
|
91
|
-
json(r);
|
|
92
|
-
return 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const steps = [];
|
|
96
|
-
if (s.changed && s.turns !== point.turn && !fork) steps.push({ name: 'Stashed local changes', live: 'git stash push -m gitroots/pre-restore…', ms: 500, ok: 'gitroots/pre-restore' });
|
|
97
|
-
steps.push({ name: `Workspace at turn ${point.turn}`, live: `checkout ${point.head} · apply backup ${point.id}…`, ms: 900, ok: `HEAD ${point.head} · ${point.files} file${point.files === 1 ? '' : 's'}` });
|
|
98
|
-
steps.push({ name: 'Conversation', live: `rewinding to turn ${point.turn}…`, ms: 500, ok: `${point.turn} turns · later turns kept in ${points[0].id}` });
|
|
99
|
-
await runSteps(steps, { nameWidth: 28 });
|
|
100
|
-
const r = apply(store, s, point, machine, fork);
|
|
101
|
-
const ss = store.session(r.session);
|
|
102
|
-
line(` ${c.g('✓')} ${c.b(`${fork ? 'Forked' : 'Restored'} ${r.session} on ${machine.name}`)} ${sessionState(ss)} ${dot()} picks up from turn ${point.turn} ${c.g(`→ gitroots attach ${r.session}`)}`);
|
|
103
|
-
if (fork) line(` ${c.m(`${s.id} is unchanged`)}`);
|
|
104
|
-
return 0;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function apply(store, s, point, machine, fork) {
|
|
108
|
-
let target = s;
|
|
109
|
-
if (fork) {
|
|
110
|
-
target = { ...s, id: store.nextSessionId(), ask: undefined, forkedFrom: s.id };
|
|
111
|
-
store.d.sessions.push(target);
|
|
112
|
-
}
|
|
113
|
-
target.turns = point.turn;
|
|
114
|
-
target.head = point.head;
|
|
115
|
-
target.changed = point.files;
|
|
116
|
-
target.machine = machine.name;
|
|
117
|
-
target.state = 'running';
|
|
118
|
-
target.updated = 'just now';
|
|
119
|
-
if (!fork) target.ask = undefined;
|
|
120
|
-
const ev = store.record({ actor: store.org.user, action: fork ? 'fork' : 'restore', target: `${target.id} → turn ${point.turn} (${point.id})`, decision: 'allowed', note: machine.name, repo: s.repo });
|
|
121
|
-
store.save();
|
|
122
|
-
return { session: target.id, from: s.id, point: point.id, turn: point.turn, machine: machine.name, fork, event: ev.id };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export { next };
|