@teambit/ci 1.0.485 → 1.0.487
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/ci.docs.mdx +344 -83
- package/commands/sync.cmd.ts +77 -0
- package/dist/ci.docs.mdx +344 -83
- package/dist/ci.main.runtime.d.ts +67 -6
- package/dist/ci.main.runtime.js +152 -25
- package/dist/ci.main.runtime.js.map +1 -1
- package/dist/commands/sync.cmd.d.ts +34 -0
- package/dist/commands/sync.cmd.js +57 -0
- package/dist/commands/sync.cmd.js.map +1 -0
- package/dist/git.d.ts +11 -1
- package/dist/git.js +11 -1
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/{preview-1785858957533.js → preview-1785883495880.js} +1 -1
- package/dist/sync/git-host-provider.d.ts +47 -0
- package/dist/sync/git-host-provider.js +54 -0
- package/dist/sync/git-host-provider.js.map +1 -0
- package/dist/sync/git-host-provider.spec.d.ts +1 -0
- package/dist/sync/git-host-provider.spec.js +171 -0
- package/dist/sync/git-host-provider.spec.js.map +1 -0
- package/dist/sync/git-ops.d.ts +171 -0
- package/dist/sync/git-ops.js +354 -0
- package/dist/sync/git-ops.js.map +1 -0
- package/dist/sync/git-ops.spec.d.ts +1 -0
- package/dist/sync/git-ops.spec.js +476 -0
- package/dist/sync/git-ops.spec.js.map +1 -0
- package/dist/sync/github-client.d.ts +79 -0
- package/dist/sync/github-client.js +198 -0
- package/dist/sync/github-client.js.map +1 -0
- package/dist/sync/github-client.spec.d.ts +1 -0
- package/dist/sync/github-client.spec.js +205 -0
- package/dist/sync/github-client.spec.js.map +1 -0
- package/dist/sync/init-scaffold.d.ts +40 -0
- package/dist/sync/init-scaffold.js +326 -0
- package/dist/sync/init-scaffold.js.map +1 -0
- package/dist/sync/init-scaffold.spec.d.ts +1 -0
- package/dist/sync/init-scaffold.spec.js +169 -0
- package/dist/sync/init-scaffold.spec.js.map +1 -0
- package/dist/sync/lane-sync-executor.d.ts +256 -0
- package/dist/sync/lane-sync-executor.js +1249 -0
- package/dist/sync/lane-sync-executor.js.map +1 -0
- package/dist/sync/lane-sync-executor.spec.d.ts +1 -0
- package/dist/sync/lane-sync-executor.spec.js +416 -0
- package/dist/sync/lane-sync-executor.spec.js.map +1 -0
- package/dist/sync/main-sync-executor.d.ts +62 -0
- package/dist/sync/main-sync-executor.js +305 -0
- package/dist/sync/main-sync-executor.js.map +1 -0
- package/dist/sync/sync-config.d.ts +83 -0
- package/dist/sync/sync-config.js +187 -0
- package/dist/sync/sync-config.js.map +1 -0
- package/dist/sync/sync-config.spec.d.ts +1 -0
- package/dist/sync/sync-config.spec.js +254 -0
- package/dist/sync/sync-config.spec.js.map +1 -0
- package/dist/sync/sync-orchestrator.d.ts +76 -0
- package/dist/sync/sync-orchestrator.js +396 -0
- package/dist/sync/sync-orchestrator.js.map +1 -0
- package/dist/sync/sync-orchestrator.spec.d.ts +1 -0
- package/dist/sync/sync-orchestrator.spec.js +155 -0
- package/dist/sync/sync-orchestrator.spec.js.map +1 -0
- package/dist/sync/sync-planner.d.ts +60 -0
- package/dist/sync/sync-planner.js +114 -0
- package/dist/sync/sync-planner.js.map +1 -0
- package/dist/sync/sync-planner.spec.d.ts +1 -0
- package/dist/sync/sync-planner.spec.js +210 -0
- package/dist/sync/sync-planner.spec.js.map +1 -0
- package/dist/sync/sync-state.d.ts +94 -0
- package/dist/sync/sync-state.js +178 -0
- package/dist/sync/sync-state.js.map +1 -0
- package/dist/sync/sync-state.spec.d.ts +1 -0
- package/dist/sync/sync-state.spec.js +271 -0
- package/dist/sync/sync-state.spec.js.map +1 -0
- package/dist/sync/workspace-lane.d.ts +13 -0
- package/dist/sync/workspace-lane.js +27 -0
- package/dist/sync/workspace-lane.js.map +1 -0
- package/package.json +20 -15
- package/sync/git-host-provider.spec.ts +197 -0
- package/sync/git-host-provider.ts +89 -0
- package/sync/git-ops.spec.ts +506 -0
- package/sync/git-ops.ts +334 -0
- package/sync/github-client.spec.ts +174 -0
- package/sync/github-client.ts +209 -0
- package/sync/init-scaffold.spec.ts +142 -0
- package/sync/init-scaffold.ts +340 -0
- package/sync/lane-sync-executor.spec.ts +389 -0
- package/sync/lane-sync-executor.ts +1363 -0
- package/sync/main-sync-executor.ts +321 -0
- package/sync/sync-config.spec.ts +283 -0
- package/sync/sync-config.ts +214 -0
- package/sync/sync-orchestrator.spec.ts +120 -0
- package/sync/sync-orchestrator.ts +386 -0
- package/sync/sync-planner.spec.ts +126 -0
- package/sync/sync-planner.ts +99 -0
- package/sync/sync-state.spec.ts +263 -0
- package/sync/sync-state.ts +176 -0
- package/sync/workspace-lane.ts +20 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { BitError } from '@teambit/bit-error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Is this a branch name git will accept? A conservative pure subset of `git check-ref-format --branch`:
|
|
5
|
+
* stricter than git is fine, laxer is not. A leading `-` must be refused here because it would be read
|
|
6
|
+
* as a command-line option once interpolated into a git invocation.
|
|
7
|
+
*/
|
|
8
|
+
export function isValidGitBranchName(name: string): boolean {
|
|
9
|
+
return validateBranchName(name) === undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Why `name` is not a usable branch name, or undefined when it is. */
|
|
13
|
+
export function validateBranchName(name: string): string | undefined {
|
|
14
|
+
if (!name) return 'it is empty';
|
|
15
|
+
if (name.startsWith('-')) return 'it starts with "-", which git would read as a command-line option';
|
|
16
|
+
if (/\s/.test(name)) return 'it contains whitespace';
|
|
17
|
+
// oxlint-disable-next-line no-control-regex
|
|
18
|
+
if (/[\x00-\x1f\x7f]/.test(name)) return 'it contains a control character';
|
|
19
|
+
if (/[~^:?*[\\]/.test(name)) return 'it contains one of the characters git forbids in a ref: ~ ^ : ? * [ \\';
|
|
20
|
+
if (name.includes('..')) return 'it contains ".."';
|
|
21
|
+
if (name.includes('@{')) return 'it contains "@{"';
|
|
22
|
+
if (name === '@') return 'it is "@", which git reserves';
|
|
23
|
+
// Leading `refs/` only: pushes interpolate the name into `refs/heads/<b>`, so it would double up.
|
|
24
|
+
if (name.startsWith('refs/'))
|
|
25
|
+
return 'it starts with "refs/" — configure the bare branch name ("main", not "refs/heads/main")';
|
|
26
|
+
if (name.startsWith('/') || name.endsWith('/')) return 'it starts or ends with "/"';
|
|
27
|
+
if (name.includes('//')) return 'it contains an empty path component ("//")';
|
|
28
|
+
if (name.endsWith('.')) return 'it ends with "."';
|
|
29
|
+
if (name.endsWith('.lock')) return 'it ends with ".lock"';
|
|
30
|
+
if (name.split('/').some((component) => component.startsWith('.'))) return 'a path component starts with "."';
|
|
31
|
+
if (name.split('/').some((component) => component.endsWith('.lock'))) return 'a path component ends with ".lock"';
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Throw a `BitError` naming the offending config key and value unless `name` is a usable branch name. */
|
|
36
|
+
export function assertValidBranchName(name: string, configKey: string): void {
|
|
37
|
+
const problem = validateBranchName(name);
|
|
38
|
+
if (!problem) return;
|
|
39
|
+
throw new BitError(
|
|
40
|
+
`bit ci sync: ${configKey} is "${name}", which is not a valid git branch name — ${problem}. ` +
|
|
41
|
+
`Fix it in the "teambit.git/ci" sync config in workspace.jsonc.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A `branchPrefix` may end in `/` or be empty, so it is validated as the *start* of a branch name.
|
|
47
|
+
* The full check still happens on the derived name (see `laneNameToBranch`).
|
|
48
|
+
*/
|
|
49
|
+
export function assertValidBranchPrefix(prefix: string, configKey: string): void {
|
|
50
|
+
if (!prefix) return;
|
|
51
|
+
const problem = validateBranchName(`${prefix}x`);
|
|
52
|
+
if (!problem) return;
|
|
53
|
+
throw new BitError(
|
|
54
|
+
`bit ci sync: ${configKey} is "${prefix}", which cannot start a valid git branch name — ${problem}. ` +
|
|
55
|
+
`Fix it in the "teambit.git/ci" sync config in workspace.jsonc.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CiSyncConfig {
|
|
60
|
+
/** The prefix for a lane-mapped branch. 'lane/' maps lane my-lane to branch lane/my-lane. */
|
|
61
|
+
branchPrefix?: string;
|
|
62
|
+
/** Explicit overrides that map a lane name to a branch name. */
|
|
63
|
+
branches?: Record<string, string>;
|
|
64
|
+
/** The glob patterns of the lane names to sync. An empty list stops lane mirroring. */
|
|
65
|
+
lanes?: string[];
|
|
66
|
+
/** The branch that carries the main-scope drift. */
|
|
67
|
+
mainSyncBranch?: string;
|
|
68
|
+
/**
|
|
69
|
+
* How main-scope drift reaches the default branch. 'pr' (the default): the command commits the drift
|
|
70
|
+
* to `mainSyncBranch` and opens a pull request. 'direct-push': the command commits the drift on the
|
|
71
|
+
* default branch and pushes it, and does not use `mainSyncBranch`.
|
|
72
|
+
*/
|
|
73
|
+
mainSync?: 'pr' | 'direct-push';
|
|
74
|
+
/**
|
|
75
|
+
* What the command does with one contested line during a `merge-diverged` action. 'halt' (the
|
|
76
|
+
* default): the command stops, labels the pull request, and writes the recovery steps. 'git-wins':
|
|
77
|
+
* the command keeps the branch version. 'lane-wins': the command takes the lane version.
|
|
78
|
+
* Non-conflicting changes always merge, with every value.
|
|
79
|
+
*/
|
|
80
|
+
onConflict?: 'halt' | 'git-wins' | 'lane-wins';
|
|
81
|
+
/** Reserved. The command warns and enables no auto-merge on the main sync pull request. */
|
|
82
|
+
autoMergeMainSyncPr?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve the configured sync options, validating every branch name before anything runs — a bad name
|
|
87
|
+
* must fail at startup naming the config key, not mid-run with git's own ref error.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveSyncConfig(raw?: CiSyncConfig): Required<CiSyncConfig> {
|
|
90
|
+
const resolved: Required<CiSyncConfig> = {
|
|
91
|
+
branchPrefix: raw?.branchPrefix ?? '',
|
|
92
|
+
branches: raw?.branches ?? {},
|
|
93
|
+
lanes: raw?.lanes ?? ['*'],
|
|
94
|
+
mainSyncBranch: raw?.mainSyncBranch ?? 'bit-sync/main',
|
|
95
|
+
autoMergeMainSyncPr: raw?.autoMergeMainSyncPr ?? false,
|
|
96
|
+
mainSync: raw?.mainSync ?? 'pr',
|
|
97
|
+
onConflict: raw?.onConflict ?? 'halt',
|
|
98
|
+
};
|
|
99
|
+
// Config comes from workspace.jsonc, so the union types are not enforced at runtime; a typo must
|
|
100
|
+
// fail at startup rather than fall through to whichever mode a comparison happens to miss.
|
|
101
|
+
if (resolved.mainSync !== 'pr' && resolved.mainSync !== 'direct-push') {
|
|
102
|
+
throw new BitError(
|
|
103
|
+
`sync.mainSync: "${resolved.mainSync}" is not a valid value. Use "pr" (propose the main-scope drift ` +
|
|
104
|
+
`as a pull request from sync.mainSyncBranch) or "direct-push" (push the drift straight onto the ` +
|
|
105
|
+
`default branch)`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (resolved.onConflict !== 'halt' && resolved.onConflict !== 'git-wins' && resolved.onConflict !== 'lane-wins') {
|
|
109
|
+
throw new BitError(
|
|
110
|
+
`sync.onConflict: "${resolved.onConflict}" is not a valid value. Use "halt" (stop the conflicted ` +
|
|
111
|
+
`lane, label the PR and let a human resolve — the default), "git-wins" (conflicting hunks keep ` +
|
|
112
|
+
`the branch's version) or "lane-wins" (conflicting hunks take the lane's version)`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
assertValidBranchPrefix(resolved.branchPrefix, 'sync.branchPrefix');
|
|
116
|
+
assertValidBranchName(resolved.mainSyncBranch, 'sync.mainSyncBranch');
|
|
117
|
+
Object.entries(resolved.branches).forEach(([laneName, branch]) =>
|
|
118
|
+
assertValidBranchName(branch, `sync.branches["${laneName}"]`)
|
|
119
|
+
);
|
|
120
|
+
return resolved;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The branch a lane name maps to. The derived result is validated too: a prefix that is fine on its
|
|
125
|
+
* own can still combine with a lane name into something git refuses.
|
|
126
|
+
*/
|
|
127
|
+
export function laneNameToBranch(laneName: string, cfg: Required<CiSyncConfig>): string {
|
|
128
|
+
const override = cfg.branches[laneName];
|
|
129
|
+
if (override) return override; // already validated by resolveSyncConfig
|
|
130
|
+
const derived = `${cfg.branchPrefix}${laneName}`;
|
|
131
|
+
assertValidBranchName(derived, `the branch for lane "${laneName}" (sync.branchPrefix + lane name)`);
|
|
132
|
+
return derived;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function branchToLaneName(branch: string, cfg: Required<CiSyncConfig>): string | undefined {
|
|
136
|
+
const override = Object.entries(cfg.branches).find(([, b]) => b === branch);
|
|
137
|
+
if (override) return override[0];
|
|
138
|
+
if (cfg.branchPrefix) {
|
|
139
|
+
return branch.startsWith(cfg.branchPrefix) ? branch.slice(cfg.branchPrefix.length) : undefined;
|
|
140
|
+
}
|
|
141
|
+
return branch;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A lane to sync, split into the scope hosting the lane object and the bare name. The branch mapping
|
|
146
|
+
* uses the NAME only; every bit operation uses the full `hostScope/name`.
|
|
147
|
+
*/
|
|
148
|
+
export type LaneTarget = {
|
|
149
|
+
/** the scope hosting the lane object; the workspace's defaultScope unless the target said otherwise */
|
|
150
|
+
hostScope: string;
|
|
151
|
+
/** the bare lane name — never contains a '/' */
|
|
152
|
+
name: string;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Parse the `[lane]` argument (`my-lane` or `some-org.some-scope/my-lane`) into a {@link LaneTarget}.
|
|
157
|
+
* A lane name cannot contain `/`, so the single `/` is always the scope/name boundary. The dot in a
|
|
158
|
+
* scope id is not required (self-hosted scopes can be bare names), so it is not validated here.
|
|
159
|
+
*/
|
|
160
|
+
export function parseLaneTarget(input: string, defaultScope: string): LaneTarget {
|
|
161
|
+
const trimmed = input.trim();
|
|
162
|
+
if (!trimmed) throw new BitError('bit ci sync: the lane argument is empty');
|
|
163
|
+
if (!trimmed.includes('/')) return { hostScope: defaultScope, name: trimmed };
|
|
164
|
+
const parts = trimmed.split('/');
|
|
165
|
+
const [hostScope, name] = parts;
|
|
166
|
+
if (parts.length !== 2 || !hostScope || !name) {
|
|
167
|
+
throw new BitError(
|
|
168
|
+
`bit ci sync: "${input}" is not a valid lane target. Use either a lane name ("my-lane", resolved ` +
|
|
169
|
+
`against the workspace's defaultScope "${defaultScope}") or a scope-qualified lane id ` +
|
|
170
|
+
`("some-org.some-scope/my-lane")`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return { hostScope, name };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The lane name a branch maps to, only if that name could actually be a lane AND maps back to a branch
|
|
178
|
+
* name usable in a git invocation. `branchToLaneName` alone is a string transform (the identity under
|
|
179
|
+
* the default empty prefix), so it would report `feature/foo` as a "lane" — a name bit forbids. The
|
|
180
|
+
* round trip matters because the lane grammar permits `-`: a developer branch called `-x` is a valid
|
|
181
|
+
* lane name, so enumeration would adopt it as a target and the branch mapping would then throw, turning
|
|
182
|
+
* one oddly-named branch into a halt that fails the whole `--all` run. Such a branch is not lane-mapped;
|
|
183
|
+
* skip it like any other.
|
|
184
|
+
*/
|
|
185
|
+
export function syncableLaneNameForBranch(branch: string, cfg: Required<CiSyncConfig>): string | undefined {
|
|
186
|
+
const laneName = branchToLaneName(branch, cfg);
|
|
187
|
+
if (!laneName || !isValidLaneName(laneName)) return undefined;
|
|
188
|
+
// `laneNameToBranch` asserts rather than returns, so the round trip is checked here: a configured
|
|
189
|
+
// override is already validated, and a derived name must pass the same branch-name rules.
|
|
190
|
+
if (cfg.branches[laneName]) return laneName;
|
|
191
|
+
return validateBranchName(`${cfg.branchPrefix}${laneName}`) === undefined ? laneName : undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** bit's own maximum lane-name length (`create-lane.ts`'s `MAX_LANE_NAME_LENGTH`). */
|
|
195
|
+
const MAX_LANE_NAME_LENGTH = 800;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Could this string be a bit lane name at all? Mirrors the module-private `isValidLaneName` in
|
|
199
|
+
* `create-lane.ts`; drift errs toward skipping a branch, and `sync-config.spec.ts` pins the rule.
|
|
200
|
+
*/
|
|
201
|
+
export function isValidLaneName(name: string): boolean {
|
|
202
|
+
if (!name || name.length > MAX_LANE_NAME_LENGTH) return false;
|
|
203
|
+
return /^[$\-_!a-z0-9]+$/.test(name);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** minimal glob: '*' wildcard only (matches the lanes list use-case; avoids a new dep) */
|
|
207
|
+
function globToRegExp(glob: string): RegExp {
|
|
208
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
209
|
+
return new RegExp(`^${escaped}$`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function shouldSyncLane(laneName: string, cfg: Required<CiSyncConfig>): boolean {
|
|
213
|
+
return cfg.lanes.some((pattern) => globToRegExp(pattern).test(laneName));
|
|
214
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { SyncOrchestrator, assertCleanForDryRun } from './sync-orchestrator';
|
|
3
|
+
import { branchToLaneName, resolveSyncConfig, syncableLaneNameForBranch } from './sync-config';
|
|
4
|
+
|
|
5
|
+
// The deps are never reached: every conflict case throws before the first `this.deps` access.
|
|
6
|
+
function orchestrator(): SyncOrchestrator {
|
|
7
|
+
return new SyncOrchestrator({} as any);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Each row must name every selector it refused, and which one would otherwise have silently won. */
|
|
11
|
+
const conflicts: Array<[Record<string, unknown>, string[]]> = [
|
|
12
|
+
[{ all: true, branch: 'my-branch' }, ['--all cannot be combined with', '--branch ("my-branch")']],
|
|
13
|
+
[{ all: true, lane: 'my-lane' }, ['--all cannot be combined with', 'a lane argument ("my-lane")']],
|
|
14
|
+
[{ all: true, main: true }, ['--all cannot be combined with', '--main']],
|
|
15
|
+
[
|
|
16
|
+
{ lane: 'my-lane', branch: 'my-branch' },
|
|
17
|
+
[
|
|
18
|
+
'a lane argument ("my-lane") cannot be combined with --branch ("my-branch")',
|
|
19
|
+
'only --branch ("my-branch") would have run',
|
|
20
|
+
],
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
{ lane: 'my-lane', main: true },
|
|
24
|
+
['a lane argument ("my-lane") cannot be combined with --main', 'only --main would have run'],
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
{ branch: 'my-branch', main: true },
|
|
28
|
+
['--branch ("my-branch") cannot be combined with --main', 'only --main would have run'],
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
{ lane: 'my-lane', branch: 'my-branch', main: true },
|
|
32
|
+
[
|
|
33
|
+
'a lane argument ("my-lane") cannot be combined with --branch ("my-branch") or with --main',
|
|
34
|
+
'only --main would have run',
|
|
35
|
+
],
|
|
36
|
+
],
|
|
37
|
+
// --init only scaffolds, so pairing it with a target could not mean anything.
|
|
38
|
+
[{ init: true, branch: 'x' }, ['--init cannot be combined']],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
describe('SyncOrchestrator target guards', () => {
|
|
42
|
+
it('refuses every combination of selectors, naming the ones it saw', async () => {
|
|
43
|
+
for (const [options, expected] of conflicts) {
|
|
44
|
+
const label = JSON.stringify(options);
|
|
45
|
+
let message = '';
|
|
46
|
+
await orchestrator()
|
|
47
|
+
.sync(options as any)
|
|
48
|
+
.catch((err) => {
|
|
49
|
+
message = err.message;
|
|
50
|
+
});
|
|
51
|
+
expected.forEach((fragment) => expect(message, label).to.contain(fragment));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('reports a branch whose mapped name is not a valid lane name, instead of syncing it', async () => {
|
|
56
|
+
const deps = {
|
|
57
|
+
config: { sync: {} },
|
|
58
|
+
workspace: { defaultScope: 'org.scope' },
|
|
59
|
+
ci: { getDefaultBranchName: async () => 'main', listGitHostProviders: () => [] },
|
|
60
|
+
lanes: { getDefaultLaneId: () => ({ name: 'main' }) },
|
|
61
|
+
logger: { console() {}, consoleWarning() {}, debug() {} },
|
|
62
|
+
};
|
|
63
|
+
const result = await new SyncOrchestrator(deps as any).sync({ branch: 'feature/foo' });
|
|
64
|
+
expect(result).to.equal('branch feature/foo does not map to a valid lane name; nothing to do');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// A dry run computes the main-scope plan by force-checking-out the sync branch, so a dirty tree would be
|
|
69
|
+
// discarded by a run that promised to write nothing.
|
|
70
|
+
describe('assertCleanForDryRun', () => {
|
|
71
|
+
it('refuses a dirty tree, naming the paths at stake and how to keep them', () => {
|
|
72
|
+
let message = '';
|
|
73
|
+
try {
|
|
74
|
+
assertCleanForDryRun(['comp1/index.js', 'notes.txt']);
|
|
75
|
+
} catch (e: any) {
|
|
76
|
+
message = e.message;
|
|
77
|
+
}
|
|
78
|
+
expect(message).to.contain('--dry-run refuses to run');
|
|
79
|
+
expect(message).to.contain('2 uncommitted change(s)');
|
|
80
|
+
expect(message).to.contain('comp1/index.js, notes.txt');
|
|
81
|
+
expect(message).to.contain('Commit or stash them first');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('caps the named paths so a large diff cannot produce an unreadable error', () => {
|
|
85
|
+
const paths = Array.from({ length: 12 }, (_, index) => `file-${index}.js`);
|
|
86
|
+
let message = '';
|
|
87
|
+
try {
|
|
88
|
+
assertCleanForDryRun(paths);
|
|
89
|
+
} catch (e: any) {
|
|
90
|
+
message = e.message;
|
|
91
|
+
}
|
|
92
|
+
expect(message).to.contain('12 uncommitted change(s)');
|
|
93
|
+
expect(message).to.contain('file-9.js');
|
|
94
|
+
expect(message).to.not.contain('file-10.js');
|
|
95
|
+
expect(message).to.contain('…');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('permits a clean tree: the write-then-restore is only acceptable when nothing can be lost', () => {
|
|
99
|
+
expect(() => assertCleanForDryRun([])).to.not.throw();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('branch -> lane enumeration filter', () => {
|
|
104
|
+
it('queues only the branches whose mapped name could actually be a lane', () => {
|
|
105
|
+
const cfg = resolveSyncConfig({});
|
|
106
|
+
// non-vacuity: the default mapping really is the identity, so the filter is what excludes anything
|
|
107
|
+
expect(branchToLaneName('feature/foo', cfg)).to.equal('feature/foo');
|
|
108
|
+
expect(
|
|
109
|
+
['feature/foo', 'FEATURE', 'ok-lane'].map((b) => syncableLaneNameForBranch(b, cfg)).filter(Boolean)
|
|
110
|
+
).to.deep.equal(['ok-lane']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('honours the prefix (stripped before the name is checked) and an explicit branches override', () => {
|
|
114
|
+
const prefixed = resolveSyncConfig({ branchPrefix: 'lane/' });
|
|
115
|
+
expect(syncableLaneNameForBranch('lane/my-lane', prefixed)).to.equal('my-lane');
|
|
116
|
+
expect(syncableLaneNameForBranch('feature/foo', prefixed)).to.equal(undefined);
|
|
117
|
+
const overridden = resolveSyncConfig({ branches: { 'my-lane': 'custom/branch' } });
|
|
118
|
+
expect(syncableLaneNameForBranch('custom/branch', overridden)).to.equal('my-lane');
|
|
119
|
+
});
|
|
120
|
+
});
|