@teambit/ci 1.0.495 → 1.0.500
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 +39 -9
- package/dist/ci.docs.mdx +39 -9
- package/dist/ci.main.runtime.d.ts +6 -0
- package/dist/ci.main.runtime.js +24 -3
- package/dist/ci.main.runtime.js.map +1 -1
- package/dist/{preview-1786461108763.js → preview-1786731350011.js} +1 -1
- package/dist/sync/lane-sync-executor.d.ts +56 -14
- package/dist/sync/lane-sync-executor.js +196 -37
- package/dist/sync/lane-sync-executor.js.map +1 -1
- package/dist/sync/lane-sync-executor.spec.js +127 -3
- package/dist/sync/lane-sync-executor.spec.js.map +1 -1
- package/dist/sync/sync-planner.d.ts +10 -1
- package/dist/sync/sync-planner.js +37 -13
- package/dist/sync/sync-planner.js.map +1 -1
- package/dist/sync/sync-planner.spec.js +44 -2
- package/dist/sync/sync-planner.spec.js.map +1 -1
- package/dist/sync/sync-state.d.ts +33 -1
- package/dist/sync/sync-state.js +60 -3
- package/dist/sync/sync-state.js.map +1 -1
- package/dist/sync/sync-state.spec.js +28 -0
- package/dist/sync/sync-state.spec.js.map +1 -1
- package/package.json +21 -21
- package/sync/lane-sync-executor.spec.ts +110 -3
- package/sync/lane-sync-executor.ts +234 -32
- package/sync/sync-planner.spec.ts +36 -1
- package/sync/sync-planner.ts +36 -16
- package/sync/sync-state.spec.ts +33 -0
- package/sync/sync-state.ts +62 -1
package/sync/sync-planner.ts
CHANGED
|
@@ -29,7 +29,14 @@ export type LaneSyncInput = {
|
|
|
29
29
|
* with `isSyncAuthoredMessage`, not the loop guard's substring match.
|
|
30
30
|
*/
|
|
31
31
|
tipIsSyncCommit: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The branch had real (non-bit) commits before this reconciler touched it — it was adopted, not
|
|
34
|
+
* manufactured. Can only withhold a delete, never authorize one.
|
|
35
|
+
*/
|
|
36
|
+
hasIndependentHistory?: boolean;
|
|
32
37
|
conflictLabelPresent: boolean;
|
|
38
|
+
/** The branch's committed `.bitmap` names a DIFFERENT lane — another lane's claim, never adopted. */
|
|
39
|
+
branchNamesDifferentLane?: boolean;
|
|
33
40
|
/** @see LaneOwnershipEvidence — consulted only on the "lane is gone" path. */
|
|
34
41
|
ownership: LaneOwnershipEvidence;
|
|
35
42
|
};
|
|
@@ -38,13 +45,18 @@ export type LaneSyncInput = {
|
|
|
38
45
|
* Why a `close-pr` is keeping the branch rather than deleting it. `tip-advanced-during-run` is the one
|
|
39
46
|
* the planner never emits: it is discovered when the executor re-reads the branch just before deleting.
|
|
40
47
|
*/
|
|
41
|
-
export type BranchKeepReason =
|
|
48
|
+
export type BranchKeepReason =
|
|
49
|
+
| 'unmerged-commits'
|
|
50
|
+
| 'tip-not-a-sync-commit'
|
|
51
|
+
| 'tip-advanced-during-run'
|
|
52
|
+
| 'adopted-branch';
|
|
42
53
|
|
|
43
54
|
export type LaneSyncAction =
|
|
44
55
|
| { type: 'noop'; reason: string }
|
|
45
56
|
| { type: 'import-lane' }
|
|
46
57
|
| { type: 'export-branch' }
|
|
47
58
|
| { type: 'merge-diverged' }
|
|
59
|
+
| { type: 'adopt-branch' }
|
|
48
60
|
// Two variants rather than an optional `keepReason` so the type enforces that a keep always says why.
|
|
49
61
|
| { type: 'close-pr'; deleteBranch: true }
|
|
50
62
|
| { type: 'close-pr'; deleteBranch: false; keepReason: BranchKeepReason }
|
|
@@ -52,7 +64,7 @@ export type LaneSyncAction =
|
|
|
52
64
|
|
|
53
65
|
export function planLaneSync(input: LaneSyncInput): LaneSyncAction {
|
|
54
66
|
const { laneHead, branchExists, lastSyncedHead, hasDevCommits, conflictLabelPresent, ownership } = input;
|
|
55
|
-
const { tipIsSyncCommit } = input;
|
|
67
|
+
const { tipIsSyncCommit, hasIndependentHistory, branchNamesDifferentLane } = input;
|
|
56
68
|
if (conflictLabelPresent) {
|
|
57
69
|
return { type: 'noop', reason: 'PR is labeled bit-sync-conflict; resolve and remove the label to resume' };
|
|
58
70
|
}
|
|
@@ -60,16 +72,20 @@ export function planLaneSync(input: LaneSyncInput): LaneSyncAction {
|
|
|
60
72
|
if (!branchExists) return { type: 'noop', reason: 'lane and branch both absent' };
|
|
61
73
|
switch (ownership) {
|
|
62
74
|
case 'own-live':
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
75
|
+
// Each check is a separate reason to withhold a delete: dev commits are work that exists
|
|
76
|
+
// nowhere else; a non-sync tip may be a developer's own `.bitmap` write; an adopted branch's
|
|
77
|
+
// pre-existing history was never proven disposable. A false keep is harmless, a false delete
|
|
78
|
+
// destroys the only copy.
|
|
79
|
+
if (hasDevCommits) {
|
|
80
|
+
return { type: 'close-pr', deleteBranch: false, keepReason: 'unmerged-commits' };
|
|
81
|
+
}
|
|
82
|
+
if (!tipIsSyncCommit) {
|
|
83
|
+
return { type: 'close-pr', deleteBranch: false, keepReason: 'tip-not-a-sync-commit' };
|
|
84
|
+
}
|
|
85
|
+
if (hasIndependentHistory) {
|
|
86
|
+
return { type: 'close-pr', deleteBranch: false, keepReason: 'adopted-branch' };
|
|
87
|
+
}
|
|
88
|
+
return { type: 'close-pr', deleteBranch: true };
|
|
73
89
|
case 'own-merged':
|
|
74
90
|
// No marker conjunction needed: the tip being an ancestor of the default branch is unforgeable
|
|
75
91
|
// proof that deleting loses nothing.
|
|
@@ -84,10 +100,14 @@ export function planLaneSync(input: LaneSyncInput): LaneSyncAction {
|
|
|
84
100
|
if (!branchExists) return { type: 'import-lane' };
|
|
85
101
|
if (!lastSyncedHead) {
|
|
86
102
|
if (hasDevCommits) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
if (branchNamesDifferentLane) {
|
|
104
|
+
return {
|
|
105
|
+
type: 'halt',
|
|
106
|
+
reason: 'branch has commits but its .bitmap names a different lane; cannot tell which side is newer',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// The lane exists but the pair has no sync history: first contact, not a conflict.
|
|
110
|
+
return { type: 'adopt-branch' };
|
|
91
111
|
}
|
|
92
112
|
return { type: 'import-lane' };
|
|
93
113
|
}
|
package/sync/sync-state.spec.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
branchStateFingerprint,
|
|
7
7
|
buildSyncCommitMessage,
|
|
8
8
|
fingerprintIdVersions,
|
|
9
|
+
oldestCommitIsNonSync,
|
|
9
10
|
hasSyncMarker,
|
|
10
11
|
isSyncAuthoredMessage,
|
|
11
12
|
parseBranchBitmap,
|
|
@@ -207,6 +208,38 @@ describe('parseDevCommitCount', () => {
|
|
|
207
208
|
});
|
|
208
209
|
});
|
|
209
210
|
|
|
211
|
+
describe('oldestCommitIsNonSync', () => {
|
|
212
|
+
it('is false for an empty range — no commits at all', () => {
|
|
213
|
+
expect(oldestCommitIsNonSync('')).to.equal(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('is false when the oldest (first, with --reverse) record is bit-authored', () => {
|
|
217
|
+
// A normal bit-created branch: its own first ledger commit, then a later human dev commit and a
|
|
218
|
+
// second ledger commit from export-branch — non-sync work, but never the OLDEST record.
|
|
219
|
+
const log = [LANE_SYNC, 'feat: a later, normal dev commit\n', MAIN_SYNC].join('\x1e');
|
|
220
|
+
expect(oldestCommitIsNonSync(log)).to.equal(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('is true when the oldest (first, with --reverse) record is a genuine, non-sync commit', () => {
|
|
224
|
+
// Adoption's exact shape: the branch's own first commit was a human's, before bit ever touched it.
|
|
225
|
+
const log = ['feat: a human created this branch before adoption\n', LANE_SYNC].join('\x1e');
|
|
226
|
+
expect(oldestCommitIsNonSync(log)).to.equal(true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('ignores a trailing separator with nothing after it', () => {
|
|
230
|
+
const log = `${LANE_SYNC}\x1e`;
|
|
231
|
+
expect(oldestCommitIsNonSync(log)).to.equal(false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// A ledger commit needs the marker AND the Bit-Lane-Head trailer; a human message that merely
|
|
235
|
+
// quotes [bit-sync] on its own line must still read as independent history (a keep, never a delete).
|
|
236
|
+
it('is true when the oldest human commit quotes the [bit-sync] marker but has no ledger trailer', () => {
|
|
237
|
+
const spoof = 'feat: mention our tooling\n\n[bit-sync]\n';
|
|
238
|
+
const log = [spoof, LANE_SYNC].join('\x1e');
|
|
239
|
+
expect(oldestCommitIsNonSync(log)).to.equal(true);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
210
243
|
describe('fingerprintIdVersions', () => {
|
|
211
244
|
it('is a single 40-hex token, and stable under reordering', () => {
|
|
212
245
|
// A single token survives being written into a commit trailer; order-stability keeps neither
|
package/sync/sync-state.ts
CHANGED
|
@@ -68,6 +68,12 @@ export function branchStateFingerprint(state: BranchBitmapState, laneComponentId
|
|
|
68
68
|
return fingerprintIdVersions(laneComponentIds.map((id) => `${id}@${state.versions[id] ?? ABSENT_ON_BRANCH}`));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* `snapPrCommit`'s return value when there was nothing to snap — its only signal either way. Lives in
|
|
73
|
+
* this leaf module so `ci.main.runtime.ts` and `lane-sync-executor.ts` share it without a cycle.
|
|
74
|
+
*/
|
|
75
|
+
export const NO_CHANGES_TO_SNAP = 'No changes detected, nothing to snap';
|
|
76
|
+
|
|
71
77
|
export const LANE_HEAD_TRAILER = 'Bit-Lane-Head';
|
|
72
78
|
/**
|
|
73
79
|
* Marks a commit as machine-generated. Duplicated in the `bit-git-sync` action repo's event router
|
|
@@ -94,15 +100,49 @@ export function isSyncAuthoredMessage(message: string): boolean {
|
|
|
94
100
|
return new RegExp(`^${SYNC_COMMIT_MARKER.replace(/[[\]]/g, '\\$&')}\\r?$`, 'm').test(message);
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
/** The record separator `hasIndependentHistoryBelowStateCommit`'s `git log --format=%B%x1e` uses. */
|
|
104
|
+
const COMMIT_MESSAGE_RECORD_SEPARATOR = '\x1e';
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether the OLDEST record of a `git log --reverse --format=%B%x1e` run is NOT bit-authored — i.e. a
|
|
108
|
+
* human created the branch before this reconciler touched it. Oldest only, not "any": ordinary lane
|
|
109
|
+
* branches carry real dev commits between ledger commits too, and "any" would misclassify them all.
|
|
110
|
+
*/
|
|
111
|
+
export function oldestCommitIsNonSync(rawLog: string): boolean {
|
|
112
|
+
const messages = rawLog
|
|
113
|
+
.split(COMMIT_MESSAGE_RECORD_SEPARATOR)
|
|
114
|
+
.map((entry) => entry.trim())
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
const oldest = messages[0];
|
|
117
|
+
return oldest !== undefined && !isLedgerCommitMessage(oldest);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Strict probe for "this is one of our LEDGER commits": the sync marker on its own line AND the
|
|
122
|
+
* `Bit-Lane-Head` trailer — `buildSyncCommitMessage` always writes both. The deletion guard reads
|
|
123
|
+
* this instead of `isSyncAuthoredMessage` so a human commit merely quoting `[bit-sync]` still counts
|
|
124
|
+
* as independent history (a false "human" only ever keeps a branch).
|
|
125
|
+
*/
|
|
126
|
+
export function isLedgerCommitMessage(message: string): boolean {
|
|
127
|
+
return isSyncAuthoredMessage(message) && new RegExp(`^${LANE_HEAD_TRAILER}: `, 'm').test(message);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Marks `adopt-branch`'s ledger commit. Audit-only — the deletion guard reads the branch's ancestry
|
|
132
|
+
* instead, since a trailer marks only the one commit it is on and later ledger commits carry none.
|
|
133
|
+
*/
|
|
134
|
+
export const ADOPTION_TRAILER = 'Bit-Adopted';
|
|
135
|
+
|
|
97
136
|
/**
|
|
98
137
|
* The sync commit's message. Every part is an annotation (audit trail + loop-guard marker); nothing reads
|
|
99
138
|
* it as state — messages are forgeable and rewritten on squash-merge. State comes from `.bitmap`.
|
|
100
139
|
*/
|
|
101
|
-
export function buildSyncCommitMessage(laneIdStr: string, laneHead: string): string {
|
|
140
|
+
export function buildSyncCommitMessage(laneIdStr: string, laneHead: string, opts: { adopted?: boolean } = {}): string {
|
|
102
141
|
return [
|
|
103
142
|
`chore(bit-sync): sync lane ${laneIdStr} @ ${laneHead.slice(0, 9)}`,
|
|
104
143
|
'',
|
|
105
144
|
`${LANE_HEAD_TRAILER}: ${laneHead}`,
|
|
145
|
+
...(opts.adopted ? [`${ADOPTION_TRAILER}: true`] : []),
|
|
106
146
|
SYNC_COMMIT_MARKER,
|
|
107
147
|
].join('\n');
|
|
108
148
|
}
|
|
@@ -163,6 +203,27 @@ export async function readBranchSyncState(
|
|
|
163
203
|
return { stateCommit, bitmap, hasDevCommits: parseDevCommitCount(count), tipMessage, tipSha };
|
|
164
204
|
}
|
|
165
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Whether `branch`'s committed `.bitmap` is byte-identical (by blob sha) to the one at its merge-base
|
|
208
|
+
* with the default branch — i.e. the branch never asserted a `.bitmap` change of its own, so whatever
|
|
209
|
+
* pointer it carries is INHERITED. Compared at the fork point, not the default branch's current tip,
|
|
210
|
+
* which moves on and would false-block. False on any git error: unreadable must not license adoption.
|
|
211
|
+
*/
|
|
212
|
+
export async function branchBitmapUnchangedSinceFork(branch: string, defaultBranch: string): Promise<boolean> {
|
|
213
|
+
try {
|
|
214
|
+
const base = (await git.raw(['merge-base', `origin/${defaultBranch}`, `origin/${branch}`])).trim();
|
|
215
|
+
if (!base) return false;
|
|
216
|
+
const [branchBlob, baseBlob] = await Promise.all([
|
|
217
|
+
git.raw(['rev-parse', `origin/${branch}:./${BIT_MAP}`]),
|
|
218
|
+
git.raw(['rev-parse', `${base}:./${BIT_MAP}`]),
|
|
219
|
+
]);
|
|
220
|
+
const branchSha = branchBlob.trim();
|
|
221
|
+
return Boolean(branchSha) && branchSha === baseBlob.trim();
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
166
227
|
/**
|
|
167
228
|
* A file's content at a git ref, or undefined when it isn't there. simple-git's `raw` can resolve with
|
|
168
229
|
* empty output instead of rejecting on a missing path; `:./` keeps the path cwd-relative in case the
|