@fugood/bricks-ctor 2.25.0-beta.55 → 2.25.0-beta.56
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/compile/__tests__/util.test.js +113 -0
- package/compile/util.ts +100 -0
- package/package.json +2 -2
- package/tools/__tests__/pull.test.ts +108 -0
- package/tools/pull.ts +15 -9
|
@@ -335,3 +335,116 @@ describe('generateCalulationMap', () => {
|
|
|
335
335
|
expect(result.map[refs[1].id].properties.command).toBe('OBJECT_GET')
|
|
336
336
|
})
|
|
337
337
|
})
|
|
338
|
+
|
|
339
|
+
// Direction A: generateCalulationMap also emits a single-node `script_map` the new runtime
|
|
340
|
+
// prefers. These assert that derived graph, independent of the legacy `map` above.
|
|
341
|
+
const scriptNode = (scriptMap) =>
|
|
342
|
+
Object.entries(scriptMap).find(([, node]) => node.type === 'command-node-script')
|
|
343
|
+
|
|
344
|
+
describe('generateScriptMap (script_map)', () => {
|
|
345
|
+
test('empty config produces a single command-node-script node', () => {
|
|
346
|
+
const { script_map } = genMap(baseConfig())
|
|
347
|
+
expect(Object.values(script_map)).toHaveLength(1)
|
|
348
|
+
const [, node] = scriptNode(script_map)
|
|
349
|
+
expect(node.properties.command).toBe('SCRIPT_RUN')
|
|
350
|
+
expect(node.properties.args.code).toBe('return inputs')
|
|
351
|
+
expect(node.properties.args.inputs).toEqual({})
|
|
352
|
+
expect(node.properties.args.outputs).toEqual({})
|
|
353
|
+
expect(node.in).toEqual({})
|
|
354
|
+
expect(node.out).toEqual({})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('wires keyed input ports and input data nodes, preserving order', () => {
|
|
358
|
+
const { script_map } = genMap(baseConfig({ inputs: { a: 'foo.bar', b: 'baz' } }))
|
|
359
|
+
const [scriptId, node] = scriptNode(script_map)
|
|
360
|
+
expect(script_map.a.out.value).toEqual([{ id: scriptId, port: 'a' }])
|
|
361
|
+
expect(script_map.b.out.value).toEqual([{ id: scriptId, port: 'b' }])
|
|
362
|
+
expect(script_map.a.in.change).toBeNull()
|
|
363
|
+
expect(node.in.a).toEqual([{ id: 'a', port: 'value' }])
|
|
364
|
+
expect(node.in.b).toEqual([{ id: 'b', port: 'value' }])
|
|
365
|
+
// args.inputs maps each input port (data-node id) to its inputs-object path, in order.
|
|
366
|
+
expect(node.properties.args.inputs).toEqual({ a: 'foo.bar', b: 'baz' })
|
|
367
|
+
expect(Object.keys(node.properties.args.inputs)).toEqual(['a', 'b'])
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('fans out output / error / outputs to distinct ports and target data nodes', () => {
|
|
371
|
+
const { script_map } = genMap(
|
|
372
|
+
baseConfig({ output: 'outNode', error: 'errNode', outputs: { sum: ['pb1', 'pb2'] } }),
|
|
373
|
+
)
|
|
374
|
+
const [scriptId, node] = scriptNode(script_map)
|
|
375
|
+
expect(node.out.output).toEqual([{ id: 'outNode', port: 'change' }])
|
|
376
|
+
expect(node.out.error).toEqual([{ id: 'errNode', port: 'change' }])
|
|
377
|
+
expect(node.out['out:sum']).toEqual([
|
|
378
|
+
{ id: 'pb1', port: 'change' },
|
|
379
|
+
{ id: 'pb2', port: 'change' },
|
|
380
|
+
])
|
|
381
|
+
expect(node.properties.args.outputs).toEqual({ 'out:sum': 'sum' })
|
|
382
|
+
expect(script_map.outNode.in.change).toEqual([{ id: scriptId, port: 'output' }])
|
|
383
|
+
expect(script_map.errNode.in.change).toEqual([{ id: scriptId, port: 'error' }])
|
|
384
|
+
expect(script_map.pb1.in.change).toEqual([{ id: scriptId, port: 'out:sum' }])
|
|
385
|
+
expect(script_map.pb2.in.change).toEqual([{ id: scriptId, port: 'out:sum' }])
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('manual mode disables every input edge', () => {
|
|
389
|
+
const { script_map } = genMap(
|
|
390
|
+
baseConfig({ trigger_mode: 'manual', inputs: { a: 'foo', b: 'bar' } }),
|
|
391
|
+
)
|
|
392
|
+
const [, node] = scriptNode(script_map)
|
|
393
|
+
expect(node.in.a[0].disable_trigger_command).toBe(true)
|
|
394
|
+
expect(node.in.b[0].disable_trigger_command).toBe(true)
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
test('auto mode honours per-key disabled_triggers', () => {
|
|
398
|
+
const { script_map } = genMap(
|
|
399
|
+
baseConfig({ inputs: { a: 'foo', b: 'bar' }, disabled_triggers: { a: true, b: false } }),
|
|
400
|
+
)
|
|
401
|
+
const [, node] = scriptNode(script_map)
|
|
402
|
+
expect(node.in.a[0].disable_trigger_command).toBe(true)
|
|
403
|
+
expect(node.in.b[0].disable_trigger_command).toBeUndefined()
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test('a node reused as input and output keeps both edges (manual mode)', () => {
|
|
407
|
+
const { script_map } = genMap(
|
|
408
|
+
baseConfig({
|
|
409
|
+
trigger_mode: 'manual',
|
|
410
|
+
inputs: { shared: 'foo' },
|
|
411
|
+
outputs: { sum: ['shared'] },
|
|
412
|
+
}),
|
|
413
|
+
)
|
|
414
|
+
const [scriptId] = scriptNode(script_map)
|
|
415
|
+
// Input side (out.value) is preserved alongside the output side (in.change).
|
|
416
|
+
expect(script_map.shared.out.value).toEqual([{ id: scriptId, port: 'shared' }])
|
|
417
|
+
expect(script_map.shared.in.change).toEqual([{ id: scriptId, port: 'out:sum' }])
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test('outputs port names never collide with the reserved output port', () => {
|
|
421
|
+
const { script_map } = genMap(baseConfig({ output: 'outNode', outputs: { output: ['pb1'] } }))
|
|
422
|
+
const [, node] = scriptNode(script_map)
|
|
423
|
+
// The whole-return-value port stays `output`; an outputs path literally named "output"
|
|
424
|
+
// is namespaced to `out:output`.
|
|
425
|
+
expect(node.out.output).toEqual([{ id: 'outNode', port: 'change' }])
|
|
426
|
+
expect(node.out['out:output']).toEqual([{ id: 'pb1', port: 'change' }])
|
|
427
|
+
expect(node.properties.args.outputs).toEqual({ 'out:output': 'output' })
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('script node id is byte-stable across recompiles and unique per calc', () => {
|
|
431
|
+
const config = baseConfig({ inputs: { a: 'foo' }, outputs: { out: ['pb1'] } })
|
|
432
|
+
const first = generateCalulationMap(config, CALC_ID)
|
|
433
|
+
const second = generateCalulationMap(config, CALC_ID)
|
|
434
|
+
expect(Object.keys(second.script_map)).toEqual(Object.keys(first.script_map))
|
|
435
|
+
const [idA] = scriptNode(first.script_map)
|
|
436
|
+
const [idB] = scriptNode(
|
|
437
|
+
generateCalulationMap(
|
|
438
|
+
config,
|
|
439
|
+
'PROPERTY_BANK_COMMAND_MAP_bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
|
|
440
|
+
).script_map,
|
|
441
|
+
)
|
|
442
|
+
expect(idA).not.toBe(idB)
|
|
443
|
+
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
|
|
444
|
+
expect(idA).toMatch(new RegExp(`^PROPERTY_BANK_COMMAND_NODE_${uuid}$`))
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('validateConfig still guards the script_map path (auto mode overlap)', () => {
|
|
448
|
+
expect(() => genMap(baseConfig({ inputs: { a: 'foo' }, error: 'a' }))).toThrow(/key: error/)
|
|
449
|
+
})
|
|
450
|
+
})
|
package/compile/util.ts
CHANGED
|
@@ -54,6 +54,104 @@ const padding = 15
|
|
|
54
54
|
const layerXInterval = 300
|
|
55
55
|
const layerYInterval = 150
|
|
56
56
|
|
|
57
|
+
// Direction A — first-class script node (see local-plan/PLAN-data-calculation-refactor.md).
|
|
58
|
+
//
|
|
59
|
+
// `generateCalulationMap` keeps emitting the legacy OBJECT_SET → SANDBOX_RUN_JAVASCRIPT →
|
|
60
|
+
// SANDBOX_GET_*/OBJECT_GET expansion in `map` (consumed by older runtimes). In parallel,
|
|
61
|
+
// `generateScriptMap` derives an equivalent single-node graph in `script_map` that a newer
|
|
62
|
+
// runtime prefers: one `command-node-script` node that assembles `inputs` from keyed input
|
|
63
|
+
// ports and fans out `error` / `output` / each `outputs` path directly to the target data
|
|
64
|
+
// nodes. Both are byte-stable functions of `script_config`, so codegen (which only reads
|
|
65
|
+
// `script_config`) round-trips losslessly and recompiling unchanged source re-derives an
|
|
66
|
+
// identical `script_map`.
|
|
67
|
+
//
|
|
68
|
+
// Trigger semantics stay edge-level (`disable_trigger_command` on the script node's input
|
|
69
|
+
// edges, same as the legacy OBJECT_SET inputs) so the circular-reference check and the
|
|
70
|
+
// manual-trigger overlay keep working unchanged — only the node count collapses N+3 → 1.
|
|
71
|
+
const generateScriptMap = (config: ScriptConfig, calcId: string) => {
|
|
72
|
+
const scriptId = makeSeededId('property_bank_command', `${calcId}:script`)
|
|
73
|
+
const map: Record<string, any> = {}
|
|
74
|
+
|
|
75
|
+
// Compiled run-time config for the script node. `inputs` maps each input port name (the
|
|
76
|
+
// source data-node id) to its OBJECT_SET path; `outputs` maps each fan-out port name to the
|
|
77
|
+
// OBJECT_GET path it extracts from the return value. The runtime reads these from
|
|
78
|
+
// properties.args, keeping the node `value` reserved purely for incoming input-port values.
|
|
79
|
+
const inputArgs: Record<string, string> = {}
|
|
80
|
+
const outputArgs: Record<string, string> = {}
|
|
81
|
+
|
|
82
|
+
const scriptIn: Record<string, any> = {}
|
|
83
|
+
const scriptOut: Record<string, any> = {}
|
|
84
|
+
|
|
85
|
+
// One in-port per input, keyed by the source data-node id (unique per input, so each input
|
|
86
|
+
// keeps its own `disable_trigger_command` flag — a shared path could not). Manual mode
|
|
87
|
+
// disables every input edge; auto mode honours per-key `disabled_triggers`.
|
|
88
|
+
Object.entries(config.inputs).forEach(([dataNodeId, path]) => {
|
|
89
|
+
inputArgs[dataNodeId] = path
|
|
90
|
+
scriptIn[dataNodeId] = [
|
|
91
|
+
{
|
|
92
|
+
id: dataNodeId,
|
|
93
|
+
port: 'value',
|
|
94
|
+
disable_trigger_command:
|
|
95
|
+
config.trigger_mode === 'manual' || config.disabled_triggers?.[dataNodeId] || undefined,
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
map[dataNodeId] = {
|
|
99
|
+
type: 'data-node',
|
|
100
|
+
properties: {},
|
|
101
|
+
in: { change: null },
|
|
102
|
+
out: { value: [{ id: scriptId, port: dataNodeId }] },
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Wire an output target data-node's `change` port back from a script out-port, preserving
|
|
107
|
+
// any `out.value` it already has (a node reused as both input and output — allowed in manual
|
|
108
|
+
// mode). Mirrors the last-wins reuse handling in generateCalulationMap's outputs section.
|
|
109
|
+
const wireOutputTarget = (dataNodeId: string, scriptPort: string) => {
|
|
110
|
+
const existing = map[dataNodeId]
|
|
111
|
+
map[dataNodeId] = {
|
|
112
|
+
type: 'data-node',
|
|
113
|
+
properties: {},
|
|
114
|
+
in: { change: [{ id: scriptId, port: scriptPort }] },
|
|
115
|
+
out: { value: existing?.out?.value ?? null },
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (config.error) {
|
|
120
|
+
scriptOut.error = [{ id: config.error, port: 'change' }]
|
|
121
|
+
wireOutputTarget(config.error, 'error')
|
|
122
|
+
}
|
|
123
|
+
if (config.output) {
|
|
124
|
+
scriptOut.output = [{ id: config.output, port: 'change' }]
|
|
125
|
+
wireOutputTarget(config.output, 'output')
|
|
126
|
+
}
|
|
127
|
+
// `out:`-prefixed port names keep `outputs` paths from colliding with the reserved
|
|
128
|
+
// `error` / `output` ports (a path could legitimately be the string "output").
|
|
129
|
+
Object.entries(config.outputs).forEach(([path, pbList]) => {
|
|
130
|
+
const portName = `out:${path}`
|
|
131
|
+
outputArgs[portName] = path
|
|
132
|
+
scriptOut[portName] = pbList.map((pb) => ({ id: pb, port: 'change' }))
|
|
133
|
+
pbList.forEach((pb) => wireOutputTarget(pb, portName))
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
map[scriptId] = {
|
|
137
|
+
type: 'command-node-script',
|
|
138
|
+
title: 'Command: SCRIPT_RUN',
|
|
139
|
+
properties: {
|
|
140
|
+
command: 'SCRIPT_RUN',
|
|
141
|
+
args: {
|
|
142
|
+
code: config.code,
|
|
143
|
+
enable_async: config.enable_async,
|
|
144
|
+
inputs: inputArgs,
|
|
145
|
+
outputs: outputArgs,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
in: scriptIn,
|
|
149
|
+
out: scriptOut,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return map
|
|
153
|
+
}
|
|
154
|
+
|
|
57
155
|
// `calcId` (the owning script calc's stable id) seeds every derived command-node id, so an
|
|
58
156
|
// unchanged calc recompiles to identical ids and editing one calc never shifts another's —
|
|
59
157
|
// keeping the compiled config byte-stable for change detection. See makeSeededId in utils/id.
|
|
@@ -219,6 +317,8 @@ export const generateCalulationMap = (config: ScriptConfig, calcId: string) => {
|
|
|
219
317
|
)
|
|
220
318
|
|
|
221
319
|
return {
|
|
320
|
+
// Newer runtimes prefer this single-node graph; older runtimes fall back to `map` below.
|
|
321
|
+
script_map: generateScriptMap(config, calcId),
|
|
222
322
|
map: {
|
|
223
323
|
...inputs.map,
|
|
224
324
|
[sandboxId]: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-ctor",
|
|
3
|
-
"version": "2.25.0-beta.
|
|
3
|
+
"version": "2.25.0-beta.56",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"oxfmt": "^0.36.0"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "5d3de40404adc334a9a64046c5514e1cd86132db"
|
|
33
33
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from 'node:child_process'
|
|
2
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
|
|
7
|
+
// bricks-project's tsconfig has no @types/jest, so declare the globals this test uses
|
|
8
|
+
// (mirrors tools/__tests__/_mcp-config.test.ts).
|
|
9
|
+
declare const describe: (name: string, fn: () => void) => void
|
|
10
|
+
declare const it: (name: string, fn: () => void | Promise<void>, timeout?: number) => void
|
|
11
|
+
declare const expect: (actual: unknown) => {
|
|
12
|
+
toBe: (expected: unknown) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const execFile = promisify(execFileCallback)
|
|
16
|
+
|
|
17
|
+
async function run(cmd: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv) {
|
|
18
|
+
return execFile(cmd, args, {
|
|
19
|
+
cwd,
|
|
20
|
+
env: { ...process.env, ...env },
|
|
21
|
+
maxBuffer: 1024 * 1024,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function initGitProject(projectDir: string) {
|
|
26
|
+
await run('git', ['init', '--initial-branch=master'], projectDir)
|
|
27
|
+
await run('git', ['config', 'user.email', 'test@example.com'], projectDir)
|
|
28
|
+
await run('git', ['config', 'user.name', 'Test User'], projectDir)
|
|
29
|
+
await writeFile(join(projectDir, 'application.json'), JSON.stringify({ id: 'app-1' }))
|
|
30
|
+
await run('git', ['add', '.'], projectDir)
|
|
31
|
+
await run('git', ['commit', '-m', 'init'], projectDir)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function writeFakeBricks(binDir: string) {
|
|
35
|
+
const payload = {
|
|
36
|
+
files: [
|
|
37
|
+
{
|
|
38
|
+
name: 'subspaces/subspace-13/data-calc/data-calculation-0.sandbox.js',
|
|
39
|
+
input: 'export const value = 1\n',
|
|
40
|
+
formatable: false,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
lastCommitId: 'server-commit',
|
|
44
|
+
}
|
|
45
|
+
const bricksPath = join(binDir, 'bricks')
|
|
46
|
+
await writeFile(
|
|
47
|
+
bricksPath,
|
|
48
|
+
`#!/usr/bin/env node\nprocess.stdout.write(${JSON.stringify(JSON.stringify(payload))})\n`,
|
|
49
|
+
)
|
|
50
|
+
await chmod(bricksPath, 0o755)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('pull.ts', () => {
|
|
54
|
+
it('creates parent directories before writing pulled project files', async () => {
|
|
55
|
+
const root = await mkdtemp(join(tmpdir(), 'bricks-project-pull-'))
|
|
56
|
+
const projectDir = join(root, 'project')
|
|
57
|
+
const binDir = join(root, 'bin')
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await mkdir(projectDir)
|
|
61
|
+
await mkdir(binDir)
|
|
62
|
+
await initGitProject(projectDir)
|
|
63
|
+
await writeFakeBricks(binDir)
|
|
64
|
+
|
|
65
|
+
const pullScript = resolve(__dirname, '../pull.ts')
|
|
66
|
+
await run('bun', [pullScript, '--force'], projectDir, {
|
|
67
|
+
PATH: `${binDir}:${process.env.PATH || ''}`,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const pulledFile = join(
|
|
71
|
+
projectDir,
|
|
72
|
+
'subspaces/subspace-13/data-calc/data-calculation-0.sandbox.js',
|
|
73
|
+
)
|
|
74
|
+
expect(await readFile(pulledFile, 'utf8')).toBe('export const value = 1\n')
|
|
75
|
+
} finally {
|
|
76
|
+
await rm(root, { recursive: true, force: true })
|
|
77
|
+
}
|
|
78
|
+
}, 30000)
|
|
79
|
+
|
|
80
|
+
it('merges pulled project files back into the starting branch', async () => {
|
|
81
|
+
const root = await mkdtemp(join(tmpdir(), 'bricks-project-pull-'))
|
|
82
|
+
const projectDir = join(root, 'project')
|
|
83
|
+
const binDir = join(root, 'bin')
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await mkdir(projectDir)
|
|
87
|
+
await mkdir(binDir)
|
|
88
|
+
await initGitProject(projectDir)
|
|
89
|
+
await writeFakeBricks(binDir)
|
|
90
|
+
|
|
91
|
+
const pullScript = resolve(__dirname, '../pull.ts')
|
|
92
|
+
await run('bun', [pullScript], projectDir, {
|
|
93
|
+
PATH: `${binDir}:${process.env.PATH || ''}`,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const { stdout: branch } = await run('git', ['branch', '--show-current'], projectDir)
|
|
97
|
+
const pulledFile = join(
|
|
98
|
+
projectDir,
|
|
99
|
+
'subspaces/subspace-13/data-calc/data-calculation-0.sandbox.js',
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
expect(branch.trim()).toBe('master')
|
|
103
|
+
expect(await readFile(pulledFile, 'utf8')).toBe('export const value = 1\n')
|
|
104
|
+
} finally {
|
|
105
|
+
await rm(root, { recursive: true, force: true })
|
|
106
|
+
}
|
|
107
|
+
}, 30000)
|
|
108
|
+
})
|
package/tools/pull.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
1
|
+
import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
-
import { join, relative } from 'node:path'
|
|
3
|
+
import { dirname, join, relative } from 'node:path'
|
|
4
4
|
import { format } from 'oxfmt'
|
|
5
5
|
import { sh } from './_shell'
|
|
6
6
|
import { extractCliErrorMessage } from './_cli-error'
|
|
@@ -86,7 +86,11 @@ const branchName = isModule
|
|
|
86
86
|
? 'BRICKS_PROJECT_try-pull-module'
|
|
87
87
|
: 'BRICKS_PROJECT_try-pull-application'
|
|
88
88
|
|
|
89
|
+
let landingBranch = ''
|
|
89
90
|
if (isGitRepo && !force) {
|
|
91
|
+
landingBranch = (await sh`cd ${cwd} && git branch --show-current`.text()).trim()
|
|
92
|
+
if (!landingBranch) throw new Error('Cannot pull from a detached HEAD')
|
|
93
|
+
|
|
90
94
|
console.log(`Checking commit ${baseCommitId}...`)
|
|
91
95
|
const found = (await sh`cd ${cwd} && git rev-list -1 ${baseCommitId}`.nothrow().text())
|
|
92
96
|
.trim()
|
|
@@ -100,8 +104,8 @@ if (isGitRepo && !force) {
|
|
|
100
104
|
|
|
101
105
|
// When the base commit isn't reachable in this clone (server stored a
|
|
102
106
|
// nanoid, or the commit was pruned), fall back to forking from current
|
|
103
|
-
// HEAD. The downstream merge into
|
|
104
|
-
// same result, just with different merge bases.
|
|
107
|
+
// HEAD. The downstream merge into the starting branch collapses both paths
|
|
108
|
+
// into the same result, just with different merge bases.
|
|
105
109
|
if (found) {
|
|
106
110
|
await sh`cd ${cwd} && git checkout -b ${branchName} ${baseCommitId}`.nothrow()
|
|
107
111
|
} else {
|
|
@@ -143,7 +147,9 @@ await Promise.all(
|
|
|
143
147
|
const result = await format(file.name, file.input, oxfmtConfig)
|
|
144
148
|
content = result.code
|
|
145
149
|
}
|
|
146
|
-
|
|
150
|
+
const target = join(cwd, file.name)
|
|
151
|
+
await mkdir(dirname(target), { recursive: true })
|
|
152
|
+
return writeFile(target, content)
|
|
147
153
|
}),
|
|
148
154
|
)
|
|
149
155
|
|
|
@@ -160,11 +166,11 @@ if (isGitRepo) {
|
|
|
160
166
|
await sh`cd ${cwd} && git ${commitArgs}`
|
|
161
167
|
}
|
|
162
168
|
if (!force) {
|
|
163
|
-
// Land the pulled commits on
|
|
169
|
+
// Land the pulled commits on the starting branch with a single 3-way merge using
|
|
164
170
|
// baseCommit as the merge base. The user doesn't have to manage a side
|
|
165
|
-
// branch, and conflicts (if any) land in the working tree on
|
|
171
|
+
// branch, and conflicts (if any) land in the working tree on the starting branch where
|
|
166
172
|
// auto-compile surfaces them as typecheck errors to resolve in-place.
|
|
167
|
-
await sh`cd ${cwd} && git checkout
|
|
173
|
+
await sh`cd ${cwd} && git checkout ${landingBranch}`
|
|
168
174
|
const mergeResult = await sh`cd ${cwd} && git merge ${branchName} --no-edit`.nothrow()
|
|
169
175
|
if (mergeResult.exitCode !== 0) {
|
|
170
176
|
// Conflict markers are in the working tree — commit them so the tree
|
|
@@ -174,7 +180,7 @@ if (isGitRepo) {
|
|
|
174
180
|
await sh`cd ${cwd} && git add .`
|
|
175
181
|
const conflictArgs = await buildCommitArgs(
|
|
176
182
|
cwd,
|
|
177
|
-
[
|
|
183
|
+
[`chore(project): merge with conflicts (resolve in ${landingBranch})`],
|
|
178
184
|
['--no-verify'],
|
|
179
185
|
)
|
|
180
186
|
await sh`cd ${cwd} && git ${conflictArgs}`
|