@heyquilt/seam 0.1.1 → 0.16.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/bin/seam.js +5 -0
- package/lib/quilts/bisect-run.js +31 -0
- package/lib/quilts/block.js +133 -14
- package/lib/quilts/cli.js +190 -11
- package/lib/quilts/client.js +34 -1
- package/lib/quilts/describe.js +142 -3
- package/lib/quilts/errors.js +5 -0
- package/lib/quilts/export-git.js +5 -1
- package/lib/quilts/hash.js +1 -1
- package/lib/quilts/import-git.js +154 -56
- package/lib/quilts/records.js +2 -1
- package/lib/quilts/signing.js +149 -0
- package/lib/quilts/tools.js +186 -19
- package/package.json +1 -1
package/bin/seam.js
CHANGED
|
@@ -24,9 +24,14 @@ Docs: https://seam.heyquilt.com/docs
|
|
|
24
24
|
seam square open "<title>" --quilt <q> [--dir <folder>]
|
|
25
25
|
seam checkout <sq:id | revision> --quilt <q> [--dir <folder>] [-- paths]
|
|
26
26
|
seam status | diff | commit -m "<message>" in a block folder
|
|
27
|
+
seam restore <paths> | reset [rev] | clean [-f] throw away uncommitted edits
|
|
28
|
+
seam live [sq] | live share | live clear uncommitted work others read as sq:<id>:live
|
|
27
29
|
seam merge [sq:id] [--into main] seam rebase | revert <patch:n> | cherry-pick <rev>
|
|
28
30
|
seam resolve <path>... --ours | --theirs | --base | --side n | --delete
|
|
29
31
|
seam log [rev] [-n 20] [--oneline] seam show <rev> seam blame <path>[@rev] seam cat | ls
|
|
32
|
+
seam key [add [--name n] | remove <key>] seam commit --sign | sign [rev] seam log --show-signature
|
|
33
|
+
seam line [new <name> [--from rev] | freeze | unfreeze] Lines: main, release/<name>
|
|
34
|
+
seam tag [<name> [rev]] seam release [<name> [rev] -m <notes>] Read as tag:<name>
|
|
30
35
|
seam events [--webhook <url>] What happened to your squares
|
|
31
36
|
seam export git <dir> Back to git
|
|
32
37
|
seam mcp The same commands as MCP tools, for your AI tool
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The check side of bisect (the server narrows, history.js): each commit the server names is
|
|
2
|
+
// checked out into a fresh folder and the check command runs there. Exit 0 is good, 125 is skip,
|
|
3
|
+
// 1 to 127 is bad, anything else (a signal, 128 and up) stops, as git bisect run does.
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import os from 'node:os'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { spawn } from 'node:child_process'
|
|
8
|
+
import { checkout } from './block.js'
|
|
9
|
+
|
|
10
|
+
const runIn = (cmd, cwd) => new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(cmd, { cwd, shell: true, stdio: 'ignore' })
|
|
12
|
+
child.on('error', reject)
|
|
13
|
+
child.on('exit', (code, signal) => resolve(signal ? null : code))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export async function bisectRun (client, { quilt, bad, good, run, marks = {}, paths = [''], api, log = () => {} }) {
|
|
17
|
+
const m = { ...marks }
|
|
18
|
+
for (let i = 0; i < 200; i++) {
|
|
19
|
+
const r = await client.bisect(quilt, { bad, good, marks: m })
|
|
20
|
+
if (r.done) return { ...r, marks: m }
|
|
21
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'seam-bisect-'))
|
|
22
|
+
try {
|
|
23
|
+
await checkout(client, { quilt, rev: r.next.id, paths, dir, api })
|
|
24
|
+
const code = await runIn(run, dir)
|
|
25
|
+
if (code == null || code > 127) throw new Error(`The check stopped on ${r.next.short} (exit ${code ?? 'by a signal'}): bisect stopped.`)
|
|
26
|
+
m[r.next.id] = code === 0 ? 'good' : code === 125 ? 'skip' : 'bad'
|
|
27
|
+
log(`${r.next.short} ${m[r.next.id]}`)
|
|
28
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }) }
|
|
29
|
+
}
|
|
30
|
+
throw new Error('bisect took more than 200 steps')
|
|
31
|
+
}
|
package/lib/quilts/block.js
CHANGED
|
@@ -35,29 +35,57 @@ export function saveBlock (dir, block) {
|
|
|
35
35
|
|
|
36
36
|
const inBlock = (block, p) => block.paths.some((x) => under(p, x))
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
/** The folders that hold committed files: they are walked even when a .gitignore names them. */
|
|
39
|
+
function trackedDirs (block) {
|
|
40
|
+
const out = new Set()
|
|
41
|
+
for (const p of Object.keys(block.files)) for (let i = p.indexOf('/'); i > 0; i = p.indexOf('/', i + 1)) out.add(p.slice(0, i))
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Reads `rel`/.gitignore into the stack of rules in force there (deepest last). */
|
|
46
|
+
function rulesIn (dir, rel, stack) {
|
|
47
|
+
try { return [...stack, { base: rel, ig: ignore().add(fs.readFileSync(path.join(dir, rel, '.gitignore'), 'utf8')) }] } catch { return stack }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** As git decides: the deepest .gitignore with a rule for the path wins (a `!` rule re-includes). */
|
|
51
|
+
function ignoredBy (stack, r, isDir) {
|
|
52
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
53
|
+
const { base, ig } = stack[i]
|
|
54
|
+
const rel = base ? r.slice(base.length + 1) : r
|
|
55
|
+
const res = ig.test(isDir ? rel + '/' : rel)
|
|
56
|
+
if (res.ignored) return true
|
|
57
|
+
if (res.unignored) return false
|
|
58
|
+
}
|
|
59
|
+
return false
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
const ALWAYS = ignore().add(ALWAYS_IGNORED)
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Every file under the block's paths on disk: Map path → { abs, stat }. Every .gitignore on the
|
|
66
|
+
* way down is read, as git does; a committed file is never ignored.
|
|
67
|
+
*/
|
|
45
68
|
function scan (dir, block) {
|
|
46
|
-
const ig = ignorer(dir)
|
|
47
69
|
const out = new Map()
|
|
48
|
-
const
|
|
70
|
+
const dirs = trackedDirs(block)
|
|
71
|
+
const walk = (rel, stack) => {
|
|
49
72
|
let names
|
|
50
73
|
try { names = fs.readdirSync(path.join(dir, rel), { withFileTypes: true }) } catch { return }
|
|
51
74
|
for (const d of names) {
|
|
52
75
|
const r = rel ? `${rel}/${d.name}` : d.name
|
|
53
|
-
|
|
54
|
-
|
|
76
|
+
const n = r.normalize('NFC')
|
|
77
|
+
const isDir = d.isDirectory()
|
|
78
|
+
if (ALWAYS.ignores(isDir ? r + '/' : r)) continue
|
|
79
|
+
if (!(isDir ? dirs.has(n) : block.files[n]) && ignoredBy(stack, r, isDir)) continue
|
|
80
|
+
// A submodule's folder stands for its gitlink entry: what is inside belongs to the submodule.
|
|
81
|
+
if (isDir && block.files[n]?.mode === 'gitlink') { out.set(n, { abs: path.join(dir, r), gitlink: true }); continue }
|
|
82
|
+
if (isDir) { if (block.paths.some((x) => under(r, x) || under(x, r))) walk(r, rulesIn(dir, r, stack)); continue }
|
|
55
83
|
if (!(d.isFile() || d.isSymbolicLink()) || !inBlock(block, r)) continue
|
|
56
84
|
const abs = path.join(dir, r)
|
|
57
|
-
out.set(
|
|
85
|
+
out.set(n, { abs, stat: fs.lstatSync(abs) })
|
|
58
86
|
}
|
|
59
87
|
}
|
|
60
|
-
walk('')
|
|
88
|
+
walk('', rulesIn(dir, '', []))
|
|
61
89
|
return out
|
|
62
90
|
}
|
|
63
91
|
|
|
@@ -75,8 +103,8 @@ export function localChanges (dir, block, { paths = [] } = {}) {
|
|
|
75
103
|
const wanted = (p) => !only.length || only.some((x) => under(p, x))
|
|
76
104
|
const files = scan(dir, block)
|
|
77
105
|
const out = []
|
|
78
|
-
for (const [p, { abs, stat }] of files) {
|
|
79
|
-
if (!wanted(p)) continue
|
|
106
|
+
for (const [p, { abs, stat, gitlink }] of files) {
|
|
107
|
+
if (!wanted(p) || gitlink) continue
|
|
80
108
|
const known = block.files[p]
|
|
81
109
|
if (known && known.size === stat.size && known.mtimeMs === stat.mtimeMs) continue
|
|
82
110
|
const { bytes, mode } = readFileEntry(abs, stat)
|
|
@@ -94,6 +122,8 @@ async function writeEntry (dir, e, content) {
|
|
|
94
122
|
const abs = path.join(dir, ...e.path.split('/'))
|
|
95
123
|
fs.mkdirSync(path.dirname(abs), { recursive: true })
|
|
96
124
|
try { fs.rmSync(abs, { force: true }) } catch {}
|
|
125
|
+
// A git submodule is an empty folder, as git leaves one it has not cloned.
|
|
126
|
+
if (e.mode === 'gitlink') { fs.mkdirSync(abs, { recursive: true }); return { blob: e.blob, mode: 'gitlink', size: e.size, mtimeMs: 0 } }
|
|
97
127
|
if (e.mode === 'symlink') fs.symlinkSync(content.toString('utf8'), abs)
|
|
98
128
|
else fs.writeFileSync(abs, content, { mode: e.mode === 'exec' ? 0o755 : 0o644 })
|
|
99
129
|
const st = fs.lstatSync(abs)
|
|
@@ -156,7 +186,11 @@ export async function syncTo (client, dir, block, to, { square } = {}) {
|
|
|
156
186
|
for (const f of d.files) {
|
|
157
187
|
if (local.has(f.path)) { kept.push(f.path); continue }
|
|
158
188
|
const e = byPath.get(f.path)
|
|
159
|
-
if (!e) {
|
|
189
|
+
if (!e) {
|
|
190
|
+
const abs = path.join(dir, ...f.path.split('/'))
|
|
191
|
+
if (block.files[f.path]?.mode === 'gitlink') { try { fs.rmdirSync(abs) } catch {} } else fs.rmSync(abs, { force: true })
|
|
192
|
+
delete block.files[f.path]; written++; continue
|
|
193
|
+
}
|
|
160
194
|
block.files[f.path] = await writeEntry(dir, e, await get(e))
|
|
161
195
|
written++
|
|
162
196
|
}
|
|
@@ -213,6 +247,25 @@ export async function commitFolder (client, dir, block, { message, paths = [] })
|
|
|
213
247
|
return r
|
|
214
248
|
}
|
|
215
249
|
|
|
250
|
+
/** Amends the square's last commit with the folder's changes (paths: only these) and, if given, a new message. */
|
|
251
|
+
export async function amendFolder (client, dir, block, { message, paths = [] }) {
|
|
252
|
+
if (!block.square) throw new Error('This folder is a read-only checkout of a line; there is no commit of yours to amend.')
|
|
253
|
+
const changes = localChanges(dir, block, { paths })
|
|
254
|
+
const blobs = changes.filter((c) => c.bytes).map((c) => ({ kind: 'blob', id: c.blob, bytes: c.bytes }))
|
|
255
|
+
if (blobs.length) await client.upload(block.quilt, blobs)
|
|
256
|
+
const list = changes.map((c) => c.status === 'deleted' ? { path: c.path, op: 'delete' } : { path: c.path, op: 'put', blob: c.blob, mode: c.mode || 'file' })
|
|
257
|
+
const r = await client.amend(block.quilt, block.square, { expect_head: block.head, ...(message ? { message } : {}), changes: list })
|
|
258
|
+
for (const c of changes) {
|
|
259
|
+
if (c.status === 'deleted') { delete block.files[c.path]; continue }
|
|
260
|
+
const st = fs.lstatSync(path.join(dir, ...c.path.split('/')))
|
|
261
|
+
block.files[c.path] = { blob: c.blob, mode: c.mode, size: st.size, mtimeMs: st.mtimeMs }
|
|
262
|
+
}
|
|
263
|
+
block.at = r.square.head
|
|
264
|
+
block.head = r.square.head
|
|
265
|
+
saveBlock(dir, block)
|
|
266
|
+
return r
|
|
267
|
+
}
|
|
268
|
+
|
|
216
269
|
/** The folder's changes as a unified diff against `at` (diff with no revision, in a block). */
|
|
217
270
|
export async function localDiff (client, dir, block, { paths = [] } = {}) {
|
|
218
271
|
const changes = localChanges(dir, block, { paths })
|
|
@@ -226,3 +279,69 @@ export async function localDiff (client, dir, block, { paths = [] } = {}) {
|
|
|
226
279
|
}
|
|
227
280
|
return { changes: changes.map(({ bytes, ...c }) => c), text: out.join('') }
|
|
228
281
|
}
|
|
282
|
+
|
|
283
|
+
const absOf = (dir, p) => path.join(dir, ...p.split('/'))
|
|
284
|
+
const removeEntry = (dir, p, mode) => { const abs = absOf(dir, p); if (mode === 'gitlink') { try { fs.rmdirSync(abs) } catch {} } else fs.rmSync(abs, { force: true }) }
|
|
285
|
+
|
|
286
|
+
/** Takes away the folders a removed file leaves empty, up to the block folder. */
|
|
287
|
+
function pruneEmpty (dir, p) {
|
|
288
|
+
for (let d = path.dirname(absOf(dir, p)); d.startsWith(dir + path.sep); d = path.dirname(d)) {
|
|
289
|
+
try { if (fs.readdirSync(d).length) return; fs.rmdirSync(d) } catch { return }
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* restore: puts the named paths back as they were at the folder's last commit (`at`), throwing
|
|
295
|
+
* away their uncommitted edits and deletions. Files nobody committed are left (clean removes them).
|
|
296
|
+
*/
|
|
297
|
+
export async function restore (client, dir, block, { paths }) {
|
|
298
|
+
if (!paths?.length) throw new Error('Which files? seam restore <paths> (. for everything here)')
|
|
299
|
+
const changes = localChanges(dir, block, { paths }).filter((c) => c.status !== 'added')
|
|
300
|
+
const entries = changes.map((c) => ({ path: c.path, ...block.files[c.path] }))
|
|
301
|
+
const get = await contentsFor(client, block.quilt, `c:${block.at}`, entries)
|
|
302
|
+
for (const e of entries) block.files[e.path] = { ...await writeEntry(dir, e, await get(e)), ...(e.term ? { term: e.term } : {}) }
|
|
303
|
+
saveBlock(dir, block)
|
|
304
|
+
return { restored: entries.map((e) => e.path) }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* reset: makes the folder's files those of `rev` (default: the folder's last commit, throwing
|
|
309
|
+
* away every uncommitted edit). The folder stays on its square or line: what differs from there
|
|
310
|
+
* shows as uncommitted changes, so a commit makes the square match `rev`. Files nobody committed
|
|
311
|
+
* stay unless `rev` has them.
|
|
312
|
+
*/
|
|
313
|
+
export async function reset (client, dir, block, { rev } = {}) {
|
|
314
|
+
if (!rev) {
|
|
315
|
+
const r = await restore(client, dir, block, { paths: [dir] })
|
|
316
|
+
return { rev: block.at, written: r.restored.length, removed: 0, changes: 0 }
|
|
317
|
+
}
|
|
318
|
+
const t = await client.tree(block.quilt, rev, block.paths)
|
|
319
|
+
const local = new Map(localChanges(dir, block).map((c) => [c.path, c]))
|
|
320
|
+
const want = new Map(t.entries.map((e) => [e.path, e]))
|
|
321
|
+
const toWrite = t.entries.filter((e) => {
|
|
322
|
+
const known = block.files[e.path]
|
|
323
|
+
return local.has(e.path) || !known || known.blob !== e.blob || known.mode !== e.mode || (e.term || null) !== (known.term || null)
|
|
324
|
+
})
|
|
325
|
+
const get = await contentsFor(client, block.quilt, rev, toWrite)
|
|
326
|
+
let removed = 0
|
|
327
|
+
for (const e of toWrite) {
|
|
328
|
+
const rec = await writeEntry(dir, e, await get(e))
|
|
329
|
+
const known = block.files[e.path]
|
|
330
|
+
// Same as the folder's commit: clean again. Otherwise the manifest keeps the commit's record,
|
|
331
|
+
// so the file shows as changed.
|
|
332
|
+
if (known && known.blob === e.blob && known.mode === e.mode) block.files[e.path] = rec
|
|
333
|
+
}
|
|
334
|
+
for (const [p, f] of Object.entries(block.files)) {
|
|
335
|
+
if (want.has(p) || !fs.existsSync(absOf(dir, p))) continue
|
|
336
|
+
removeEntry(dir, p, f.mode); pruneEmpty(dir, p); removed++
|
|
337
|
+
}
|
|
338
|
+
saveBlock(dir, block)
|
|
339
|
+
return { rev: t.commit, written: toWrite.length, removed, changes: localChanges(dir, block).length }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** clean: removes the files under `paths` that nobody committed and no .gitignore names. */
|
|
343
|
+
export function clean (dir, block, { paths = [], dryRun = false } = {}) {
|
|
344
|
+
const added = localChanges(dir, block, { paths }).filter((c) => c.status === 'added').map((c) => c.path)
|
|
345
|
+
if (!dryRun) for (const p of added) { removeEntry(dir, p, 'file'); pruneEmpty(dir, p) }
|
|
346
|
+
return { removed: added, dry_run: dryRun }
|
|
347
|
+
}
|
package/lib/quilts/cli.js
CHANGED
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
// --quilt and --square. It is not part of the Quilt app's `quilt` command.
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import fs from 'node:fs'
|
|
7
|
+
import os from 'node:os'
|
|
7
8
|
import { quiltClient } from './client.js'
|
|
8
|
-
import { findBlock, checkout, status as blockStatus, commitFolder, syncTo, localDiff, saveBlock } from './block.js'
|
|
9
|
-
import { describeLog, describeShow, describeDiff, describeBlame, describeStatus, describeMerge, describeWrite, describeSquare, describeSquareStatus, describeEvents } from './describe.js'
|
|
9
|
+
import { findBlock, checkout, status as blockStatus, commitFolder, amendFolder, syncTo, localDiff, saveBlock, restore, reset, clean, localChanges } from './block.js'
|
|
10
|
+
import { describeLog, describeShow, describeDiff, describeBlame, describeStatus, describeMerge, describeWrite, describeSquare, describeSquareStatus, describeEvents, describeTag, describeRelease, describeImport, describeRestore, describeReset, describeClean, describeLive, describeClaims, describeResolutions, describeLiveSet, describeMergeBase, describeGrep, describeBisect, describeCheck, describeReviews, describeReviewed, describeSquareCheck, describePolicy, describeKeys } from './describe.js'
|
|
11
|
+
import { signWithLocalKey, localKey, makeLocalKey, signingKeyFile } from './signing.js'
|
|
10
12
|
|
|
11
13
|
export const API_URL = 'https://api.heyquilt.com'
|
|
12
14
|
|
|
13
15
|
/** The verbs `seam` answers. */
|
|
14
|
-
export const SEAM_VERBS = new Set(['quilts', 'square', 'checkout', 'status', 'commit', 'log', 'show', 'diff', 'merge', 'rebase', 'revert', 'cherry-pick', 'blame', 'resolve', 'cat', 'ls', 'import', 'export', 'events'])
|
|
16
|
+
export const SEAM_VERBS = new Set(['quilts', 'square', 'checkout', 'status', 'commit', 'log', 'show', 'diff', 'merge', 'rebase', 'revert', 'cherry-pick', 'blame', 'resolve', 'cat', 'ls', 'import', 'export', 'events', 'tag', 'release', 'line', 'restore', 'reset', 'clean', 'live', 'bisect', 'grep', 'describe', 'merge-base', 'claim', 'unclaim', 'check', 'resolutions', 'review', 'sign', 'key'])
|
|
15
17
|
|
|
16
|
-
const VALUE_FLAGS = new Set(['-m', '--message', '-n', '--max-count', '--into', '--onto', '--on-conflict', '--quilt', '--square', '--dir', '--line', '--task', '--level', '--title', '--slug', '--format', '--expect-tip', '--side', '--file', '--batch', '--to', '--note', '--after', '--kinds', '--webhook', '--secret', '--branch'])
|
|
18
|
+
const VALUE_FLAGS = new Set(['-m', '--quilts', '--message', '-n', '--max-count', '--into', '--onto', '--on-conflict', '--quilt', '--square', '--dir', '--line', '--task', '--level', '--title', '--slug', '--format', '--expect-tip', '--side', '--file', '--batch', '--to', '--note', '--after', '--kinds', '--webhook', '--secret', '--branch', '--from', '-S', '--run', '--marks', '--patch', '--on', '--path', '--checks', '--name', '--public-key'])
|
|
17
19
|
|
|
18
20
|
export function parse (argv) {
|
|
19
21
|
const pos = []; const flags = {}; const paths = []
|
|
@@ -61,6 +63,43 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
61
63
|
const list = await client.quilts()
|
|
62
64
|
return print(list, list.length ? list.map((q) => `${q.slug} ${q.id} (${q.level})`).join('\n') : 'No quilts yet.')
|
|
63
65
|
}
|
|
66
|
+
case 'line': {
|
|
67
|
+
// seam line [list] | new release/<name> [--from <rev>] | freeze <line> | unfreeze <line>
|
|
68
|
+
const sub = pos[0] || 'list'
|
|
69
|
+
const describeLine = (l) => `${l.name}: ${l.patch_count} patches, tip ${l.tip ? l.tip.slice(0, 12) : '(empty)'}${l.frozen ? ', frozen' : ''}`
|
|
70
|
+
if (sub === 'list') { const ls = await client.lines(needQuilt()); return print(ls, ls.map(describeLine).join('\n')) }
|
|
71
|
+
if (sub === 'new') {
|
|
72
|
+
if (!pos[1]) throw new Error('Name the line: seam line new release/<name> [--from <rev>]')
|
|
73
|
+
const l = await client.newLine(needQuilt(), { name: pos[1], from: flags['--from'] })
|
|
74
|
+
return print(l, `Made line ${describeLine(l)}.\nOpen a square on it with: seam square open "<title>" --line ${l.name}`)
|
|
75
|
+
}
|
|
76
|
+
if (sub === 'freeze' || sub === 'unfreeze') {
|
|
77
|
+
if (!pos[1]) throw new Error(`Which line? seam line ${sub} <line>`)
|
|
78
|
+
const l = await (sub === 'freeze' ? client.freezeLine : client.unfreezeLine)(needQuilt(), pos[1])
|
|
79
|
+
return print(l, sub === 'freeze' ? `${l.name} is frozen: merges into it are refused until seam line unfreeze ${l.name}.` : `${l.name} takes merges again.`)
|
|
80
|
+
}
|
|
81
|
+
if (sub === 'require') {
|
|
82
|
+
if (!pos[1]) throw new Error('Which line? seam line require <line> [<approvals>] [--checks ci,lint]')
|
|
83
|
+
const checks = flags['--checks'] === undefined ? undefined : String(flags['--checks']).split(',').map((c) => c.trim()).filter(Boolean)
|
|
84
|
+
if (pos[2] === undefined && checks === undefined) { const p = await client.linePolicy(needQuilt(), pos[1]); return print(p, describePolicy(p)) }
|
|
85
|
+
const p = await client.setLinePolicy(needQuilt(), pos[1], { ...(pos[2] !== undefined ? { approvals: Number(pos[2]) } : {}), ...(checks ? { checks } : {}) })
|
|
86
|
+
return print(p, describePolicy(p))
|
|
87
|
+
}
|
|
88
|
+
throw new Error('seam line [list] | new release/<name> [--from <rev>] | freeze <line> | unfreeze <line> | require <line> [<approvals>] [--checks ci,lint]')
|
|
89
|
+
}
|
|
90
|
+
case 'tag':
|
|
91
|
+
case 'release': {
|
|
92
|
+
// seam tag [<name> [rev]] | seam release [<name> [rev] -m <notes>]: no name lists them.
|
|
93
|
+
const where = (x) => `${x.short}${x.patch ? ` (${x.patch.line}:${x.patch.patch_no})` : ''}`
|
|
94
|
+
if (!pos[0]) {
|
|
95
|
+
if (cmd === 'tag') { const ts = await client.tags(needQuilt()); return print(ts, ts.length ? ts.map(describeTag).join('\n') : 'No tags yet.') }
|
|
96
|
+
const rs = await client.releases(needQuilt())
|
|
97
|
+
return print(rs, rs.length ? rs.map(describeRelease).join('\n') : 'No releases yet.')
|
|
98
|
+
}
|
|
99
|
+
if (cmd === 'tag') { const x = await client.createTag(needQuilt(), { name: pos[0], rev: pos[1] }); return print(x, `Tagged ${x.name} at ${where(x)}. Read it as tag:${x.name}.`) }
|
|
100
|
+
const x = await client.createRelease(needQuilt(), { name: pos[0], rev: pos[1], notes: flags['-m'] ?? flags['--message'] ?? '' })
|
|
101
|
+
return print(x, `Released ${x.name} at ${where(x)}. Read it as tag:${x.name}.`)
|
|
102
|
+
}
|
|
64
103
|
case 'events': {
|
|
65
104
|
// seam events [--after n] [--kinds a,b] | --webhook <url> [--secret s] [--kinds a,b] | --webhook off
|
|
66
105
|
const kinds = flags['--kinds'] ? String(flags['--kinds']).split(',') : undefined
|
|
@@ -75,7 +114,7 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
75
114
|
case 'square': {
|
|
76
115
|
const sub = pos[0] || 'list'
|
|
77
116
|
if (sub === 'open') {
|
|
78
|
-
const sq = await client.openSquare(needQuilt(), { title: pos.slice(1).join(' ') || flags['--title'] || 'untitled', line: flags['--line'], task_id: flags['--task'] })
|
|
117
|
+
const sq = await client.openSquare(needQuilt(), { title: pos.slice(1).join(' ') || flags['--title'] || 'untitled', line: flags['--line'], task_id: flags['--task'], ...(flags['--on'] ? { on: flags['--on'] } : {}) })
|
|
79
118
|
let text = describeSquare(sq)
|
|
80
119
|
if (flags['--dir']) {
|
|
81
120
|
const dir = path.resolve(cwd, flags['--dir'])
|
|
@@ -130,20 +169,150 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
130
169
|
const st = await blockStatus(client, found.dir, block)
|
|
131
170
|
return print(st, describeStatus(st))
|
|
132
171
|
}
|
|
172
|
+
case 'review': {
|
|
173
|
+
// seam review [sq] (the reviews and where the square stands)
|
|
174
|
+
// seam review approve|request-changes|comment|dismiss [sq] [-m <text>] [--path <file> [--line <n>]]
|
|
175
|
+
const VERDICT = { approve: 'approve', 'request-changes': 'request_changes', comment: 'comment', dismiss: 'dismiss' }
|
|
176
|
+
const kind = VERDICT[pos[0]]
|
|
177
|
+
const sq = (kind ? pos[1] : pos[0]) || needSquare()
|
|
178
|
+
if (!kind) { const r = await client.reviews(needQuilt(), sq); return print(r, describeReviews(r)) }
|
|
179
|
+
const r = await client.review(needQuilt(), sq, { kind, body: message || '', ...(flags['--path'] ? { path: flags['--path'] } : {}), ...(flags['--line'] ? { line: Number(flags['--line']) } : {}) })
|
|
180
|
+
return print(r, describeReviewed(r))
|
|
181
|
+
}
|
|
182
|
+
case 'claim': {
|
|
183
|
+
// seam claim (lists the quilt's claims) | seam claim <pattern>... [--note <why>] (claims them for your square)
|
|
184
|
+
if (!pos.length) { const cs = await client.claims(needQuilt(), flags['--square']); return print(cs, describeClaims(cs)) }
|
|
185
|
+
const sq = needSquare()
|
|
186
|
+
const made = []
|
|
187
|
+
for (const p of pos) made.push(await client.claim(needQuilt(), sq, { pattern: p, note: flags['--note'] || message }))
|
|
188
|
+
return print(made, made.map((c) => `Claimed ${c.pattern} for ${c.square}: writes to it from other squares on ${c.line} are refused until the square merges or you run seam unclaim ${c.pattern}.`).join('\n'))
|
|
189
|
+
}
|
|
190
|
+
case 'check': {
|
|
191
|
+
// seam check <name> red|green|flaky [--line main] [--patch n] | seam check <name> green|red --square <sq>
|
|
192
|
+
const [name, status] = pos
|
|
193
|
+
if (!name || !['red', 'green', 'flaky'].includes(status)) throw new Error('seam check <name> red|green|flaky [--line <line>] [--patch <n>]')
|
|
194
|
+
if (flags['--square']) {
|
|
195
|
+
// seam check <name> green|red --square <sq>: a result for a square's latest commit (a line may require it before a merge)
|
|
196
|
+
const r = await client.reportSquareCheck(needQuilt(), flags['--square'], { check: name, status })
|
|
197
|
+
return print(r, describeSquareCheck(r))
|
|
198
|
+
}
|
|
199
|
+
const r = await client.reportCheck(needQuilt(), flags['--line'] || 'main', { check: name, status, ...(flags['--patch'] != null ? { patch: Number(flags['--patch']) } : {}) })
|
|
200
|
+
return print(r, describeCheck(r))
|
|
201
|
+
}
|
|
202
|
+
case 'unclaim': {
|
|
203
|
+
// seam unclaim <pattern|claim id>...
|
|
204
|
+
if (!pos.length) throw new Error('Which claim? seam unclaim <pattern>')
|
|
205
|
+
const sq = needSquare()
|
|
206
|
+
const out = []
|
|
207
|
+
for (const p of pos) out.push(await client.unclaim(needQuilt(), sq, p))
|
|
208
|
+
return print(out, out.map((r) => `Let go of ${r.released.pattern}.`).join('\n'))
|
|
209
|
+
}
|
|
210
|
+
case 'live': {
|
|
211
|
+
// seam live [sq] | live share (a block folder's uncommitted edits) | live clear
|
|
212
|
+
const sub = pos[0]
|
|
213
|
+
if (sub === 'share' || sub === 'clear') {
|
|
214
|
+
const sqRef = flags['--square'] || block?.square
|
|
215
|
+
if (!sqRef) throw new Error(`seam live ${sub} works in a block folder on a square (or pass --square sq:<id>).`)
|
|
216
|
+
if (sub === 'share' && !found) throw new Error('seam live share sends a block folder\'s uncommitted edits: run it in a folder made by seam checkout.')
|
|
217
|
+
const changes = sub === 'clear' ? [] : localChanges(found.dir, block).map((c) => c.status === 'deleted' ? { path: c.path, op: 'delete' } : { path: c.path, op: 'put', content: c.bytes.toString('base64'), mode: c.mode })
|
|
218
|
+
const r = await client.setLive(needQuilt(), sqRef, { changes })
|
|
219
|
+
return print(r, describeLiveSet(r))
|
|
220
|
+
}
|
|
221
|
+
const r = await client.live(needQuilt(), sub || needSquare())
|
|
222
|
+
return print(r, describeLive(r))
|
|
223
|
+
}
|
|
224
|
+
case 'restore':
|
|
225
|
+
case 'reset':
|
|
226
|
+
case 'clean': {
|
|
227
|
+
if (!found) throw new Error(`seam ${cmd} works on a block folder; run it in a folder made by seam checkout.`)
|
|
228
|
+
const files = [...(cmd === 'reset' ? pos.slice(1) : pos), ...paths].map((p) => path.resolve(cwd, p))
|
|
229
|
+
if (cmd === 'restore') { const r = await restore(client, found.dir, block, { paths: files }); return print(r, describeRestore(r)) }
|
|
230
|
+
if (cmd === 'reset') { const r = await reset(client, found.dir, block, { rev: pos[0] }); return print(r, describeReset(r, block)) }
|
|
231
|
+
const r = clean(found.dir, block, { paths: files.length ? files : [cwd], dryRun: !flags['-f'] && !flags['--force'] })
|
|
232
|
+
return print(r, describeClean(r, 'Run it again as seam clean -f to remove them.'))
|
|
233
|
+
}
|
|
133
234
|
case 'commit': {
|
|
134
235
|
if (!found) {
|
|
135
236
|
throw new Error('seam commit seals a block folder\'s changes; run it in a folder made by seam checkout.')
|
|
136
237
|
}
|
|
238
|
+
// --sign: signs the new commit with this computer's key (seam key add).
|
|
239
|
+
const signed = async (r) => {
|
|
240
|
+
if (!flags['--sign'] || !r?.result?.id) return ''
|
|
241
|
+
const s = await signWithLocalKey(client, needQuilt(), r.result.id)
|
|
242
|
+
r.signature = s.signature
|
|
243
|
+
return `\nSigned with ${s.signature.key}.`
|
|
244
|
+
}
|
|
245
|
+
if (flags['--amend']) {
|
|
246
|
+
const r = await amendFolder(client, found.dir, block, { message, paths: [...pos, ...paths].map((p) => path.resolve(cwd, p)) })
|
|
247
|
+
const s = await signed(r)
|
|
248
|
+
return print(r, describeWrite(r, `Amended ${r.amended.slice(0, 12)} as`) + s)
|
|
249
|
+
}
|
|
137
250
|
if (!message) throw new Error('Say what changed: seam commit -m "message" [paths...]')
|
|
138
251
|
const r = await commitFolder(client, found.dir, block, { message, paths: [...pos, ...paths].map((p) => path.resolve(cwd, p)) })
|
|
139
252
|
if (r.nothing) return print(r, 'Nothing to commit.')
|
|
140
|
-
|
|
253
|
+
const s = await signed(r)
|
|
254
|
+
return print(r, describeWrite(r, 'Committed') + s)
|
|
255
|
+
}
|
|
256
|
+
case 'sign': {
|
|
257
|
+
// seam sign [rev]: signs a commit you made (default: your square's head, or main's tip).
|
|
258
|
+
const r = await signWithLocalKey(client, needQuilt(), pos[0] || (block?.square ? block.square_short : block?.line) || 'main')
|
|
259
|
+
return print(r, `Signed ${r.short} with ${r.signature.key}.`)
|
|
260
|
+
}
|
|
261
|
+
case 'key': {
|
|
262
|
+
// seam key [list [member]] | add [--name <n>] [--public-key <file>] | remove <key>
|
|
263
|
+
const sub = pos[0] || 'list'
|
|
264
|
+
if (sub === 'list') { const r = await client.signingKeys({ member: pos[1] }); return print(r, describeKeys(r)) }
|
|
265
|
+
if (sub === 'remove') {
|
|
266
|
+
if (!pos[1]) throw new Error('seam key remove <key>')
|
|
267
|
+
const r = await client.removeKey(pos[1])
|
|
268
|
+
return print(r, `Removed ${r.key}: commits it signed now show "Signature by a removed key".`)
|
|
269
|
+
}
|
|
270
|
+
if (sub !== 'add') throw new Error('seam key [list [member]] | add [--name <name>] [--public-key <file>] | remove <key>')
|
|
271
|
+
let pub = flags['--public-key'] ? fs.readFileSync(path.resolve(cwd, flags['--public-key']), 'utf8') : null
|
|
272
|
+
let made = false
|
|
273
|
+
if (!pub) { const k = localKey(); if (k) pub = k.public_key; else { pub = makeLocalKey(); made = true } }
|
|
274
|
+
const r = await client.addKey({ public_key: pub, name: flags['--name'] || os.hostname() })
|
|
275
|
+
return print(r, `${made ? `Made a signing key at ${signingKeyFile()} and added` : 'Added'} ${r.key}. Sign with seam commit --sign or seam sign <rev>.`)
|
|
141
276
|
}
|
|
142
277
|
case 'log': {
|
|
143
278
|
const rev = pos[0] || (block?.square ? block.square_short : block?.line) || 'main'
|
|
144
|
-
const r = await client.log(needQuilt(), { rev, paths, limit: flags['-n'] || flags['--max-count'] || 20 })
|
|
279
|
+
const r = await client.log(needQuilt(), { rev, paths, limit: flags['-n'] || flags['--max-count'] || 20, follow: flags['--follow'] ? true : undefined, show_signature: flags['--show-signature'] ? true : undefined, search: typeof flags['-S'] === 'string' ? flags['-S'] : undefined })
|
|
145
280
|
return print(r, describeLog(r, { oneline: !!flags['--oneline'] }))
|
|
146
281
|
}
|
|
282
|
+
case 'describe': {
|
|
283
|
+
const r = await client.describe(needQuilt(), pos[0] || (block?.square ? block.square_short : block?.line) || 'main')
|
|
284
|
+
return print(r, r.describe)
|
|
285
|
+
}
|
|
286
|
+
case 'merge-base': {
|
|
287
|
+
if (pos.length !== 2) throw new Error('seam merge-base <revision> <revision>')
|
|
288
|
+
const r = await client.mergeBase(needQuilt(), pos[0], pos[1])
|
|
289
|
+
return print(r, describeMergeBase(r))
|
|
290
|
+
}
|
|
291
|
+
case 'grep': {
|
|
292
|
+
// seam grep <pattern> [revision] [-- paths]: -i ignores case, -F takes the pattern as plain text, -l lists files.
|
|
293
|
+
if (!pos[0]) throw new Error('seam grep <pattern> [revision] [-i] [-F] [-l] [--all] [-- paths]')
|
|
294
|
+
if (flags['--all']) {
|
|
295
|
+
// --all: every quilt you can read (or --quilts a,b), at the revision (default main).
|
|
296
|
+
const r = await client.search({ pattern: pos[0], rev: pos[1] || 'main', quilts: flags['--quilts'] ? String(flags['--quilts']).split(',') : undefined, paths, ignore_case: flags['-i'] ? 1 : undefined, fixed: flags['-F'] ? 1 : undefined, limit: flags['--max-count'] })
|
|
297
|
+
return print(r, describeGrep(r, { filesOnly: !!flags['-l'] }))
|
|
298
|
+
}
|
|
299
|
+
const rev = pos[1] || (block?.square ? block.square_short : block?.line) || 'main'
|
|
300
|
+
const rel = (p) => found ? path.relative(found.dir, path.resolve(cwd, p)).split(path.sep).join('/') : p
|
|
301
|
+
const r = await client.grep(needQuilt(), { pattern: pos[0], rev, paths: paths.map(rel), ignore_case: flags['-i'] ? 1 : undefined, fixed: flags['-F'] ? 1 : undefined, limit: flags['--max-count'] })
|
|
302
|
+
return print(r, describeGrep(r, { filesOnly: !!flags['-l'] }))
|
|
303
|
+
}
|
|
304
|
+
case 'bisect': {
|
|
305
|
+
// seam bisect <bad> <good> --run "<check>": exit 0 good, 125 skip, else bad. Without --run, one step (--marks id:good,id:bad).
|
|
306
|
+
if (pos.length !== 2) throw new Error('seam bisect <bad> <good> --run "<check command>" (or --marks <commit>:good,<commit>:bad for one step)')
|
|
307
|
+
const marks = Object.fromEntries(String(flags['--marks'] || '').split(',').filter(Boolean).map((x) => x.split(':')))
|
|
308
|
+
if (typeof flags['--run'] === 'string') {
|
|
309
|
+
const { bisectRun } = await import('./bisect-run.js')
|
|
310
|
+
const r = await bisectRun(client, { quilt: needQuilt(), bad: pos[0], good: pos[1], run: flags['--run'], marks, api, log: (s) => { if (!json) out(s) } })
|
|
311
|
+
return print(r, describeBisect(r))
|
|
312
|
+
}
|
|
313
|
+
const r = await client.bisect(needQuilt(), { bad: pos[0], good: pos[1], marks })
|
|
314
|
+
return print(r, describeBisect(r) + (r.done ? '' : `\nThen: seam bisect ${pos[0]} ${pos[1]} --marks ${[...Object.entries(marks).map(([k, v]) => `${k}:${v}`), `${r.next.short}:<good|bad|skip>`].join(',')}`))
|
|
315
|
+
}
|
|
147
316
|
case 'show': {
|
|
148
317
|
const rev = pos[0] || (block?.square ? block.square_short : 'main')
|
|
149
318
|
const format = flags['--stat'] ? 'stat' : flags['--name-only'] || flags['--name-status'] ? 'names' : 'unified'
|
|
@@ -206,13 +375,23 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
206
375
|
}
|
|
207
376
|
case 'resolve': {
|
|
208
377
|
const choose = flags['--ours'] ? 'ours' : flags['--theirs'] ? 'theirs' : flags['--base'] ? 'base' : flags['--delete'] ? 'delete' : flags['--suggestion'] ? 'suggestion' : flags['--side'] != null ? Number(flags['--side']) : null
|
|
209
|
-
if (choose === null || !pos.length) throw new Error('seam resolve <path>... --ours | --theirs | --base | --suggestion | --side n | --delete (or edit the file and commit it without the markers)')
|
|
378
|
+
if (choose === null || !pos.length) throw new Error('seam resolve <path>... --ours | --theirs | --base | --suggestion | --side n | --delete [--no-remember] (or edit the file and commit it without the markers)')
|
|
210
379
|
const rel = (p) => found ? path.relative(found.dir, path.resolve(cwd, p)).split(path.sep).join('/') : p
|
|
211
|
-
const r = await client.resolve(needQuilt(), needSquare(), { ...(found ? { expect_head: block.head } : {}), entries: pos.map((p) => ({ path: rel(p), choose })) })
|
|
380
|
+
const r = await client.resolve(needQuilt(), needSquare(), { ...(found ? { expect_head: block.head } : {}), entries: pos.map((p) => ({ path: rel(p), choose })), ...(flags['--no-remember'] ? { remember: false } : {}) })
|
|
212
381
|
let text = describeWrite(r, 'Resolved')
|
|
213
382
|
if (found) { const s = await syncTo(client, found.dir, block, r.square.head, { square: r.square }); text += `\n${s.written} file(s) updated in the folder.` }
|
|
214
383
|
return print(r, text)
|
|
215
384
|
}
|
|
385
|
+
case 'resolutions': {
|
|
386
|
+
// seam resolutions [path] (the remembered answers) | seam resolutions forget <key>
|
|
387
|
+
if (pos[0] === 'forget') {
|
|
388
|
+
if (!pos[1]) throw new Error('Which one? seam resolutions forget <key>')
|
|
389
|
+
const r = await client.forgetResolution(needQuilt(), pos[1])
|
|
390
|
+
return print(r, `Forgot ${r.forgotten.short} (${r.forgotten.path}): that conflict will be asked about again.`)
|
|
391
|
+
}
|
|
392
|
+
const rows = await client.resolutions(needQuilt(), pos[0])
|
|
393
|
+
return print(rows, describeResolutions(rows))
|
|
394
|
+
}
|
|
216
395
|
case 'blame': {
|
|
217
396
|
const arg = pos[0]
|
|
218
397
|
if (!arg) throw new Error('seam blame <path>[@revision]')
|
|
@@ -236,12 +415,12 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
236
415
|
return print(r, r.entries.map((e) => `${e.mode}\t${e.blob ? e.blob.slice(0, 12) : e.term.slice(0, 12)}\t${e.path}`).join('\n'))
|
|
237
416
|
}
|
|
238
417
|
case 'import': {
|
|
239
|
-
if (pos[0] !== 'git' || !pos[1]) throw new Error('seam import git <path> [--into <
|
|
418
|
+
if (pos[0] !== 'git' || !pos[1]) throw new Error('seam import git <path> [--into <quilt-slug>] [--validate]')
|
|
240
419
|
const { importGit } = await import('./import-git.js')
|
|
241
420
|
const repo = path.resolve(cwd, pos[1])
|
|
242
421
|
if (!fs.existsSync(path.join(repo, '.git')) && !fs.existsSync(path.join(repo, 'HEAD'))) throw new Error(`${repo} is not a git repository`)
|
|
243
422
|
const r = await importGit(client, { repo, slug: flags['--into'] || path.basename(repo).toLowerCase().replace(/[^a-z0-9-]/g, '-'), log: (s) => { if (!json) out(s) }, validate: !!flags['--validate'], batch: Number(flags['--batch']) || 100 })
|
|
244
|
-
return print(r,
|
|
423
|
+
return print(r, describeImport(r))
|
|
245
424
|
}
|
|
246
425
|
case 'export': {
|
|
247
426
|
if (pos[0] !== 'git' || !pos[1]) throw new Error('seam export git <dir> [--line main] [--branch <name>]')
|
package/lib/quilts/client.js
CHANGED
|
@@ -45,6 +45,7 @@ export function quiltClient ({ api, token, fetch: fetchImpl = globalThis.fetch,
|
|
|
45
45
|
openSquare: (q, b) => call('POST', `${Q(q)}/squares`, b),
|
|
46
46
|
square: (q, s) => call('GET', `${Q(q)}/squares/${enc(s)}`),
|
|
47
47
|
commit: (q, s, b) => call('POST', `${Q(q)}/squares/${enc(s)}/commit`, b),
|
|
48
|
+
amend: (q, s, b) => call('POST', `${Q(q)}/squares/${enc(s)}/amend`, b),
|
|
48
49
|
handoff: (q, s, b) => call('POST', `${Q(q)}/squares/${enc(s)}/handoff`, b),
|
|
49
50
|
abandon: (q, s, b = {}) => call('POST', `${Q(q)}/squares/${enc(s)}/abandon`, b),
|
|
50
51
|
reopen: (q, s) => call('POST', `${Q(q)}/squares/${enc(s)}/reopen`, {}),
|
|
@@ -54,6 +55,16 @@ export function quiltClient ({ api, token, fetch: fetchImpl = globalThis.fetch,
|
|
|
54
55
|
rebase: (q, s, b) => call('POST', `${Q(q)}/squares/${enc(s)}/rebase`, b),
|
|
55
56
|
cherryPick: (q, s, b) => call('POST', `${Q(q)}/squares/${enc(s)}/cherry-pick`, b),
|
|
56
57
|
resolve: (q, s, b) => call('POST', `${Q(q)}/squares/${enc(s)}/resolve`, b),
|
|
58
|
+
lines: async (q) => (await call('GET', `${Q(q)}/lines`)).lines,
|
|
59
|
+
newLine: (q, b) => call('POST', `${Q(q)}/lines`, b),
|
|
60
|
+
reportCheck: (q, line, b) => call('POST', `${Q(q)}/lines/${enc(line)}/checks`, b),
|
|
61
|
+
freezeLine: (q, line) => call('POST', `${Q(q)}/lines/${enc(line)}/freeze`, {}),
|
|
62
|
+
unfreezeLine: (q, line) => call('POST', `${Q(q)}/lines/${enc(line)}/unfreeze`, {}),
|
|
63
|
+
tags: async (q) => (await call('GET', `${Q(q)}/tags`)).tags,
|
|
64
|
+
tag: (q, name) => call('GET', `${Q(q)}/tags/${name.split('/').map(enc).join('/')}`),
|
|
65
|
+
createTag: (q, b) => call('POST', `${Q(q)}/tags`, b),
|
|
66
|
+
releases: async (q) => (await call('GET', `${Q(q)}/releases`)).releases,
|
|
67
|
+
createRelease: (q, b) => call('POST', `${Q(q)}/releases`, b),
|
|
57
68
|
merge: (q, line, b) => call('POST', `${Q(q)}/lines/${enc(line)}/merge`, b),
|
|
58
69
|
revert: (q, b) => call('POST', `${Q(q)}/revert`, b),
|
|
59
70
|
events: (q, o = {}) => call('GET', `${Q(q)}/events${qs(o)}`),
|
|
@@ -61,15 +72,37 @@ export function quiltClient ({ api, token, fetch: fetchImpl = globalThis.fetch,
|
|
|
61
72
|
show: (q, rev, o = {}) => call('GET', `${Q(q)}/show/${enc(rev)}${qs(o)}`),
|
|
62
73
|
exportPatches: (q, o = {}) => call('GET', `${Q(q)}/export/git${qs(o)}`),
|
|
63
74
|
log: (q, o = {}) => call('GET', `${Q(q)}/log${qs(o)}`),
|
|
75
|
+
sign: (q, rev, b) => call('POST', `${Q(q)}/commits/${enc(rev)}/signatures`, b),
|
|
76
|
+
signingKeys: (o = {}) => call('GET', `/v1/signing-keys${qs(o)}`),
|
|
77
|
+
addKey: (b) => call('POST', '/v1/signing-keys', b),
|
|
78
|
+
removeKey: (key) => call('DELETE', `/v1/signing-keys/${enc(key)}`),
|
|
79
|
+
describe: (q, rev) => call('GET', `${Q(q)}/describe${qs({ rev })}`),
|
|
80
|
+
mergeBase: (q, a, b) => call('GET', `${Q(q)}/merge-base${qs({ a, b })}`),
|
|
81
|
+
grep: (q, o = {}) => call('GET', `${Q(q)}/grep${qs(o)}`),
|
|
82
|
+
search: (o = {}) => call('GET', `/v1/search${qs(o)}`),
|
|
83
|
+
bisect: (q, b) => call('POST', `${Q(q)}/bisect`, b),
|
|
64
84
|
linePatches: (q, line = 'main', o = {}) => call('GET', `${Q(q)}/lines/${encodeURIComponent(line)}/patches${qs(o)}`),
|
|
65
85
|
diff: (q, o = {}) => call('GET', `${Q(q)}/diff${qs(o)}`),
|
|
86
|
+
resolutions: async (q, p) => (await call('GET', `${Q(q)}/resolutions${p ? `?path=${enc(p)}` : ''}`)).resolutions,
|
|
87
|
+
forgetResolution: (q, key) => call('DELETE', `${Q(q)}/resolutions/${enc(key)}`),
|
|
88
|
+
reviews: (q, sq) => call('GET', `${Q(q)}/squares/${enc(sq)}/reviews`),
|
|
89
|
+
review: (q, sq, b) => call('POST', `${Q(q)}/squares/${enc(sq)}/reviews`, b),
|
|
90
|
+
reportSquareCheck: (q, sq, b) => call('POST', `${Q(q)}/squares/${enc(sq)}/checks`, b),
|
|
91
|
+
linePolicy: (q, line) => call('GET', `${Q(q)}/lines/${enc(line)}/policy`),
|
|
92
|
+
setLinePolicy: (q, line, b) => call('PUT', `${Q(q)}/lines/${enc(line)}/policy`, b),
|
|
93
|
+
claims: async (q, square) => (await call('GET', `${Q(q)}/claims${square ? `?square=${enc(square)}` : ''}`)).claims,
|
|
94
|
+
claim: (q, sq, b) => call('POST', `${Q(q)}/squares/${enc(sq)}/claims`, b),
|
|
95
|
+
unclaim: (q, sq, which) => call('DELETE', `${Q(q)}/squares/${enc(sq)}/claims/${enc(which)}`),
|
|
96
|
+
live: (q, sq) => call('GET', `${Q(q)}/squares/${enc(sq)}/live`),
|
|
97
|
+
setLive: (q, sq, body) => call('PUT', `${Q(q)}/squares/${enc(sq)}/live`, body),
|
|
66
98
|
tree: (q, rev, paths) => call('GET', `${Q(q)}/tree/${enc(rev || 'main')}${qs({ paths })}`),
|
|
67
99
|
cat: (q, rev, path, o = {}) => call('GET', `${Q(q)}/blob/${enc(rev || 'main')}/${pathPart(path)}${qs(o)}`),
|
|
68
100
|
blame: (q, rev, path) => call('GET', `${Q(q)}/blame/${enc(rev || 'main')}/${pathPart(path)}`),
|
|
69
101
|
have: (q, ids) => call('POST', `${Q(q)}/have`, { ids }),
|
|
70
102
|
blobs: (q, ids) => call('POST', `${Q(q)}/blobs`, { ids }),
|
|
71
103
|
uploadPack: (q, bytes) => call('POST', `${Q(q)}/packs`, bytes, { raw: true }),
|
|
72
|
-
importCommits: (q, items) => call('POST', `${Q(q)}/import/commits`, { items }),
|
|
104
|
+
importCommits: (q, items, lines = []) => call('POST', `${Q(q)}/import/commits`, { items, ...(lines.length ? { lines } : {}) }),
|
|
105
|
+
importKnown: (q, shas) => call('POST', `${Q(q)}/import/known`, { shas }),
|
|
73
106
|
|
|
74
107
|
/**
|
|
75
108
|
* Uploads objects the server does not have yet ([{ kind, id, bytes }]), in packs of at most
|