@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.
@@ -1,5 +1,5 @@
1
1
  ;
2
- import * as overview_0 from '/home/circleci/Library/Caches/Bit/capsules/8891be5ad/teambit.git_ci@1.0.495/dist/ci.docs.mdx';
2
+ import * as overview_0 from '/home/circleci/Library/Caches/Bit/capsules/8891be5ad/teambit.git_ci@1.0.500/dist/ci.docs.mdx';
3
3
 
4
4
  export const compositions = [];
5
5
  export const overview = [overview_0];
@@ -26,6 +26,12 @@ export declare const REFUSED_SUMMARY_PREFIX = "REFUSED";
26
26
  * summary just because the ledger push raced.
27
27
  */
28
28
  export declare function racedLedgerPushSummary(laneName: string, policyClause?: string): string;
29
+ /**
30
+ * `executeAdoptBranch`'s ledger-commit push lost the race. Unlike the export paths, adoption never
31
+ * snaps or exports — the LANE is untouched; only the branch's pointer commit failed to land. The next
32
+ * run re-plans first contact against the winner's tip.
33
+ */
34
+ export declare function racedAdoptionPushSummary(laneName: string): string;
29
35
  export type LaneSyncDeps = {
30
36
  lanes: LanesMain;
31
37
  /** for snapPrCommit + getDefaultBranchName + switchToLaneForSync */
@@ -131,6 +137,16 @@ export declare class LaneSyncExecutor {
131
137
  * resulting `.bitmap` back onto the branch so the next run sees the two sides as converged.
132
138
  */
133
139
  private executeExportBranch;
140
+ /**
141
+ * First contact: the lane exists, the branch maps to it by name, but the branch's committed
142
+ * `.bitmap` has no state for it (typically `bit ci pr --keep-lane` created the lane from this very
143
+ * branch without recording the pointer in git). Adoption only ever RECORDS a pointer, never resolves
144
+ * content: switch onto the lane keeping the branch's files, then ask `bit status` whether anything
145
+ * differs. Nothing → ledger commit; something → halt, lane untouched. Deliberately a status READ,
146
+ * never a snap: `snapping.snap()` writes an unexported Version object even under `dryRun`, and that
147
+ * residue can trip a later lane in the same `--all` run.
148
+ */
149
+ private executeAdoptBranch;
134
150
  /**
135
151
  * Both sides moved since the last sync. Order is load-bearing: (1) merge the lane into the branch's
136
152
  * working tree (`bit checkout head`; conflicts halt or auto-resolve per `sync.onConflict`), (2) snap
@@ -141,13 +157,12 @@ export declare class LaneSyncExecutor {
141
157
  */
142
158
  private executeMergeDiverged;
143
159
  /**
144
- * Snap the workspace's current tree onto the lane and export it; returns the error so the caller can
145
- * halt. Snaps WHATEVER is in the workspace (the switch uses `forceOurs` and never merges files), so
146
- * a diverged tree must already hold the merged content. `keepLane` preserves the lane's history,
147
- * `skipCleanup` keeps the snap's `.bitmap` for the caller to commit, `noDestructiveRecovery` turns
148
- * stale-lane recovery (delete + re-fork the remote lane) into a throw. The lane object must be
149
- * imported BEFORE delegating: a switch onto the lane the workspace is already on no-ops before any
150
- * fetch, so it never warms a cold scope.
160
+ * Snap the workspace's current tree onto the lane and export it. Snaps WHATEVER is in the workspace
161
+ * (the switch uses `forceOurs` and never merges files), so a diverged tree must already hold the
162
+ * merged content. `keepLane` preserves the lane's history, `skipCleanup` keeps the snap's `.bitmap`
163
+ * for the caller to commit, `noDestructiveRecovery` turns stale-lane recovery (delete + re-fork the
164
+ * remote lane) into a throw. The lane object must be imported BEFORE delegating: a switch onto the
165
+ * lane the workspace is already on no-ops before any fetch, so it never warms a cold scope.
151
166
  */
152
167
  private snapAndExportOntoLane;
153
168
  /**
@@ -155,7 +170,7 @@ export declare class LaneSyncExecutor {
155
170
  * so any earlier fingerprint is stale), commit — crucially the `.bitmap` the snap rewrote — and push.
156
171
  * `unreadable` means the lane could no longer be read back, in which case the caller halts;
157
172
  * `raced` means a concurrent run's push already got there first, in which case the caller reports
158
- * a benign noop instead.
173
+ * a benign noop instead. `adopted` adds the audit-only `ADOPTION_TRAILER`.
159
174
  */
160
175
  private recordLaneHeadOnBranch;
161
176
  /**
@@ -181,6 +196,30 @@ export declare class LaneSyncExecutor {
181
196
  * direction: left alone, never retired.
182
197
  */
183
198
  private assessBranchOwnership;
199
+ /**
200
+ * Gates `hasIndependentHistoryBelowStateCommit`'s git call to the one case it can change the outcome
201
+ * for: an `own-live` claim (the only ownership that would otherwise license an immediate delete) with
202
+ * a reachable state commit to inspect. Extracted out of `reconcileLane` to keep the branching there.
203
+ */
204
+ private computeHasIndependentHistory;
205
+ /**
206
+ * Whether the OLDEST commit unique to the branch is NOT bit-authored — durable, ancestry-based proof
207
+ * a human created the branch before this reconciler touched it (a trailer would not survive later
208
+ * ledger commits; see `oldestCommitIsNonSync` for the oldest-only rationale). Unreadable answers
209
+ * `true`: a deletion guard errs toward keeping.
210
+ */
211
+ private hasIndependentHistoryBelowStateCommit;
212
+ /** Gates `differentLaneStillClaims` to the planner's first-contact path, the only one that reads it. */
213
+ private computeBranchNamesDifferentLane;
214
+ /**
215
+ * Whether adoption must refuse `branch` because its `.bitmap` names a DIFFERENT lane's still-live
216
+ * claim. Blob equality with the fork-point `.bitmap`, not commit reachability: an `own-superseded`
217
+ * branch is reachable yet still carries live work; only a `.bitmap` the branch never changed proves
218
+ * the pointer is inherited. Unreadable stays conservative and blocks adoption.
219
+ */
220
+ private differentLaneStillClaims;
221
+ /** Seam over `branchBitmapUnchangedSinceFork`, so `differentLaneStillClaims` is unit-testable without a real remote. */
222
+ private branchInheritsBitmapFromDefault;
184
223
  /**
185
224
  * The lane is gone from bit.cloud — close its PR, and retire the branch if the claim on it allows.
186
225
  * Deleting the branch is the one irreversible thing this command does; `keepReason` distinguishes
@@ -202,12 +241,15 @@ export declare class LaneSyncExecutor {
202
241
  */
203
242
  private executeHalt;
204
243
  /**
205
- * Drive the workspace's filesystem to the remote lane's content; returns the error so the caller can
206
- * halt. Two traps make a plain `switchToLane` insufficient: (1) it defaults to `forceOurs`, which
207
- * never touches the filesystem — the commit would claim the branch mirrors the lane over the default
208
- * branch's files — so `forceOurs` is cleared and `forceTheirs` set; (2) switching onto the lane the
209
- * workspace already sits on throws "already checked out", which `switchToLane` swallows as success
210
- * and nothing materializes — so step off to main first.
244
+ * Drive the workspace's filesystem onto the remote lane; returns the error so the caller can halt.
245
+ * Two traps make a plain `switchToLane` insufficient: (1) its default `forceOurs` never touches the
246
+ * filesystem, so the caller's commit would misrepresent whichever side it claims mirrors the other;
247
+ * (2) switching onto the lane the workspace already sits on throws "already checked out", which
248
+ * `switchToLane` swallows as success and nothing materializes — so step off to main first.
249
+ *
250
+ * `theirsWins` picks the direction: `true` (import-lane) checks the lane's files out over the disk;
251
+ * `false` (adopt-branch) keeps the workspace's files — the branch's content is what the lane is
252
+ * being asked to reflect.
211
253
  */
212
254
  private materializeLane;
213
255
  private fetchOnce;
@@ -17,6 +17,7 @@ exports.haltCommentBody = haltCommentBody;
17
17
  exports.isProtectedBranch = isProtectedBranch;
18
18
  exports.laneHeadFingerprint = laneHeadFingerprint;
19
19
  exports.laneSyncPrBody = laneSyncPrBody;
20
+ exports.racedAdoptionPushSummary = racedAdoptionPushSummary;
20
21
  exports.racedLedgerPushSummary = racedLedgerPushSummary;
21
22
  function _chalk() {
22
23
  const data = _interopRequireDefault(require("chalk"));
@@ -137,6 +138,15 @@ function racedBranchPushSummary(laneName) {
137
138
  function racedLedgerPushSummary(laneName, policyClause = '') {
138
139
  return racedSummary(laneName, `${policyClause}lane updated; branch ledger commit lost the push race — next run re-plans`);
139
140
  }
141
+
142
+ /**
143
+ * `executeAdoptBranch`'s ledger-commit push lost the race. Unlike the export paths, adoption never
144
+ * snaps or exports — the LANE is untouched; only the branch's pointer commit failed to land. The next
145
+ * run re-plans first contact against the winner's tip.
146
+ */
147
+ function racedAdoptionPushSummary(laneName) {
148
+ return racedSummary(laneName, 'lane untouched; adoption ledger commit lost the push race — next run re-plans');
149
+ }
140
150
  /**
141
151
  * Fingerprint of a lane's content — same primitive as `branchStateFingerprint`, so "equal" means
142
152
  * exactly "the branch records every lane component at the lane's head".
@@ -261,7 +271,8 @@ function dryRunSummaryLine(laneName, action) {
261
271
  const KEPT_BECAUSE = exports.KEPT_BECAUSE = {
262
272
  'unmerged-commits': `it carries commits that are not in the default branch`,
263
273
  'tip-not-a-sync-commit': `its tip is not one of this reconciler's own commits, so the work in it may exist nowhere else`,
264
- 'tip-advanced-during-run': `its tip advanced after the ownership evidence was read, so it may carry work this run never saw`
274
+ 'tip-advanced-during-run': `its tip advanced after the ownership evidence was read, so it may carry work this run never saw`,
275
+ 'adopted-branch': `it was adopted from a pre-existing branch, and only merging (not deleting) proves its history is safe to retire`
265
276
  };
266
277
  class LaneSyncExecutor {
267
278
  constructor(deps) {
@@ -438,13 +449,17 @@ class LaneSyncExecutor {
438
449
  // `isSyncAuthoredMessage`, NOT `hasSyncMarker`: this feeds a branch deletion, and a message that
439
450
  // merely quotes the marker must not read as one we wrote.
440
451
  const tipIsSyncCommit = (0, _syncState().isSyncAuthoredMessage)(branchState.tipMessage);
452
+ const hasIndependentHistory = await this.computeHasIndependentHistory(ownership, branchState.stateCommit, defaultBranch);
453
+ const branchNamesDifferentLane = await this.computeBranchNamesDifferentLane(lastSyncedHead, branchState.hasDevCommits, mirroredLane, laneIdStr, branch, defaultBranch);
441
454
  const action = (0, _syncPlanner().planLaneSync)({
442
455
  laneHead,
443
456
  branchExists,
444
457
  lastSyncedHead,
445
458
  hasDevCommits: branchState.hasDevCommits,
446
459
  tipIsSyncCommit,
460
+ hasIndependentHistory,
447
461
  conflictLabelPresent,
462
+ branchNamesDifferentLane,
448
463
  ownership
449
464
  });
450
465
  logger.console(_chalk().default.blue(`${laneName} -> ${action.type} (branch: ${branch}, lane head: ${laneHead?.slice(0, 9) ?? 'none'}, ` + `branch state: ${lastSyncedHead?.slice(0, 9) ?? 'none'}, ` + `dev commits: ${branchState.hasDevCommits}${laneIsGone ? `, branch claim: ${ownership}` : ''})`));
@@ -501,6 +516,14 @@ class LaneSyncExecutor {
501
516
  branch,
502
517
  defaultBranch
503
518
  });
519
+ case 'adopt-branch':
520
+ return this.executeAdoptBranch({
521
+ target,
522
+ laneIdStr,
523
+ branch,
524
+ defaultBranch,
525
+ pr
526
+ });
504
527
  case 'close-pr':
505
528
  return this.executeClosePr({
506
529
  laneName,
@@ -513,13 +536,18 @@ class LaneSyncExecutor {
513
536
  expectedTipSha: branchState.tipSha
514
537
  });
515
538
  case 'halt':
516
- return this.executeHalt({
517
- laneName,
518
- laneIdStr,
519
- branch,
520
- reason: action.reason,
521
- pr
522
- });
539
+ {
540
+ // The planner only has the boolean; the different-lane id and its PR advice live here.
541
+ const commentNote = branchNamesDifferentLane && mirroredLane ? branchMirrorsOtherLaneNote(mirroredLane, laneIdStr) : undefined;
542
+ return this.executeHalt({
543
+ laneName,
544
+ laneIdStr,
545
+ branch,
546
+ reason: action.reason,
547
+ pr,
548
+ commentNote
549
+ });
550
+ }
523
551
  default:
524
552
  {
525
553
  // exhaustiveness guard
@@ -660,14 +688,14 @@ class LaneSyncExecutor {
660
688
  const message = await this.lastNonSyncCommitMessage(branch, defaultBranch);
661
689
  await this.checkoutFromRemote(branch, `origin/${branch}`);
662
690
  try {
663
- const exportErr = await this.snapAndExportOntoLane(laneIdStr, message);
664
- if (exportErr) {
691
+ const exported = await this.snapAndExportOntoLane(laneIdStr, message);
692
+ if (exported.status === 'error') {
665
693
  // Halt rather than propagate: one lane's failed snap/export must not abort the lanes after it.
666
694
  return await this.executeHalt({
667
695
  laneName,
668
696
  laneIdStr,
669
697
  branch,
670
- reason: `failed to snap and export branch ${branch} onto lane ${laneIdStr}: ${exportErr.message}`,
698
+ reason: `failed to snap and export branch ${branch} onto lane ${laneIdStr}: ${exported.error.message}`,
671
699
  pr: await this.findPr(branch)
672
700
  });
673
701
  }
@@ -688,6 +716,71 @@ class LaneSyncExecutor {
688
716
  }
689
717
  }
690
718
 
719
+ /**
720
+ * First contact: the lane exists, the branch maps to it by name, but the branch's committed
721
+ * `.bitmap` has no state for it (typically `bit ci pr --keep-lane` created the lane from this very
722
+ * branch without recording the pointer in git). Adoption only ever RECORDS a pointer, never resolves
723
+ * content: switch onto the lane keeping the branch's files, then ask `bit status` whether anything
724
+ * differs. Nothing → ledger commit; something → halt, lane untouched. Deliberately a status READ,
725
+ * never a snap: `snapping.snap()` writes an unexported Version object even under `dryRun`, and that
726
+ * residue can trip a later lane in the same `--all` run.
727
+ */
728
+ async executeAdoptBranch({
729
+ target,
730
+ laneIdStr,
731
+ branch,
732
+ defaultBranch,
733
+ pr
734
+ }) {
735
+ const {
736
+ logger
737
+ } = this.deps;
738
+ const laneName = target.name;
739
+ logger.console((0, _cli().formatWarningSummary)(`Adopting branch ${branch}: lane ${laneIdStr} exists, but the branch's committed .bitmap has no state for it (first contact)`));
740
+ await this.checkoutFromRemote(branch, `origin/${branch}`);
741
+ try {
742
+ const switchErr = await this.materializeLane(laneIdStr, false);
743
+ if (switchErr) {
744
+ return await this.executeHalt({
745
+ laneName,
746
+ laneIdStr,
747
+ branch,
748
+ reason: `failed to check branch ${branch} against lane ${laneIdStr} for adoption: ${switchErr.message}`,
749
+ pr: pr ?? (await this.findPr(branch))
750
+ });
751
+ }
752
+ const wouldChangeLane = await this.deps.ci.hasUnsyncedWorkChanges();
753
+ if (wouldChangeLane) {
754
+ // Nothing was snapped or exported — the lane is untouched.
755
+ return await this.executeHalt({
756
+ laneName,
757
+ laneIdStr,
758
+ branch,
759
+ reason: 'branch has commits but its .bitmap records no state for this lane; cannot tell which side is ' + `newer (adopting would change lane ${laneIdStr}'s content, not just record a pointer)`,
760
+ pr: pr ?? (await this.findPr(branch))
761
+ });
762
+ }
763
+
764
+ // Branch content already matches the lane — the ledger commit is all that is missing.
765
+ const recorded = await this.recordLaneHeadOnBranch(target, laneIdStr, branch, {
766
+ adopted: true
767
+ });
768
+ if (recorded.status === 'raced') return racedAdoptionPushSummary(laneName);
769
+ if (recorded.status === 'unreadable') {
770
+ return await this.executeHalt({
771
+ laneName,
772
+ laneIdStr,
773
+ branch,
774
+ reason: `lane ${laneIdStr} could not be read back from the remote after adopting branch ${branch}`,
775
+ pr: pr ?? (await this.findPr(branch))
776
+ });
777
+ }
778
+ return `${laneName} -> adopt-branch (branch ${branch} had no lane state; adopted onto lane ${laneIdStr} @ ` + `${recorded.laneHead.slice(0, 9)})`;
779
+ } finally {
780
+ await this.restoreWorkspace(defaultBranch);
781
+ }
782
+ }
783
+
691
784
  /**
692
785
  * Both sides moved since the last sync. Order is load-bearing: (1) merge the lane into the branch's
693
786
  * working tree (`bit checkout head`; conflicts halt or auto-resolve per `sync.onConflict`), (2) snap
@@ -756,9 +849,9 @@ class LaneSyncExecutor {
756
849
  }
757
850
 
758
851
  // ---- step 2: snap + export the merged tree onto the lane ---------------------------------
759
- const exportErr = await this.snapAndExportOntoLane(laneIdStr, `merge remote lane ${laneIdStr} into ${branch} ${_syncState().SYNC_COMMIT_MARKER}`);
760
- if (exportErr) {
761
- return await halt(`merged lane ${laneIdStr} into branch ${branch} cleanly, but snapping/exporting the merged ` + `result failed: ${exportErr.message}`);
852
+ const exported = await this.snapAndExportOntoLane(laneIdStr, `merge remote lane ${laneIdStr} into ${branch} ${_syncState().SYNC_COMMIT_MARKER}`);
853
+ if (exported.status === 'error') {
854
+ return await halt(`merged lane ${laneIdStr} into branch ${branch} cleanly, but snapping/exporting the merged ` + `result failed: ${exported.error.message}`);
762
855
  }
763
856
 
764
857
  // ---- step 3: record the new lane head on the branch --------------------------------------
@@ -777,18 +870,17 @@ class LaneSyncExecutor {
777
870
  }
778
871
 
779
872
  /**
780
- * Snap the workspace's current tree onto the lane and export it; returns the error so the caller can
781
- * halt. Snaps WHATEVER is in the workspace (the switch uses `forceOurs` and never merges files), so
782
- * a diverged tree must already hold the merged content. `keepLane` preserves the lane's history,
783
- * `skipCleanup` keeps the snap's `.bitmap` for the caller to commit, `noDestructiveRecovery` turns
784
- * stale-lane recovery (delete + re-fork the remote lane) into a throw. The lane object must be
785
- * imported BEFORE delegating: a switch onto the lane the workspace is already on no-ops before any
786
- * fetch, so it never warms a cold scope.
873
+ * Snap the workspace's current tree onto the lane and export it. Snaps WHATEVER is in the workspace
874
+ * (the switch uses `forceOurs` and never merges files), so a diverged tree must already hold the
875
+ * merged content. `keepLane` preserves the lane's history, `skipCleanup` keeps the snap's `.bitmap`
876
+ * for the caller to commit, `noDestructiveRecovery` turns stale-lane recovery (delete + re-fork the
877
+ * remote lane) into a throw. The lane object must be imported BEFORE delegating: a switch onto the
878
+ * lane the workspace is already on no-ops before any fetch, so it never warms a cold scope.
787
879
  */
788
880
  async snapAndExportOntoLane(laneIdStr, message) {
789
881
  try {
790
882
  await (0, _workspaceLane().ensureCurrentLaneObject)(this.deps.lanes);
791
- await this.deps.ci.snapPrCommit({
883
+ const result = await this.deps.ci.snapPrCommit({
792
884
  laneIdStr,
793
885
  message,
794
886
  build: undefined,
@@ -797,9 +889,17 @@ class LaneSyncExecutor {
797
889
  skipCleanup: true,
798
890
  noDestructiveRecovery: true
799
891
  });
800
- return undefined;
892
+ if (result === _syncState().NO_CHANGES_TO_SNAP) return {
893
+ status: 'noop'
894
+ };
895
+ return {
896
+ status: 'exported'
897
+ };
801
898
  } catch (e) {
802
- return e instanceof Error ? e : new Error(String(e?.message ?? e));
899
+ return {
900
+ status: 'error',
901
+ error: e instanceof Error ? e : new Error(String(e?.message ?? e))
902
+ };
803
903
  }
804
904
  }
805
905
 
@@ -808,15 +908,15 @@ class LaneSyncExecutor {
808
908
  * so any earlier fingerprint is stale), commit — crucially the `.bitmap` the snap rewrote — and push.
809
909
  * `unreadable` means the lane could no longer be read back, in which case the caller halts;
810
910
  * `raced` means a concurrent run's push already got there first, in which case the caller reports
811
- * a benign noop instead.
911
+ * a benign noop instead. `adopted` adds the audit-only `ADOPTION_TRAILER`.
812
912
  */
813
- async recordLaneHeadOnBranch(target, laneIdStr, branch) {
913
+ async recordLaneHeadOnBranch(target, laneIdStr, branch, opts = {}) {
814
914
  const remoteLane = await this.getRemoteLane(target);
815
915
  if (!remoteLane) return {
816
916
  status: 'unreadable'
817
917
  };
818
918
  const laneHead = laneHeadFingerprint(remoteLane.components);
819
- const pushResult = await this.commitAllAndPush(branch, (0, _syncState().buildSyncCommitMessage)(laneIdStr, laneHead));
919
+ const pushResult = await this.commitAllAndPush(branch, (0, _syncState().buildSyncCommitMessage)(laneIdStr, laneHead, opts));
820
920
  if (pushResult.raced) return {
821
921
  status: 'raced'
822
922
  };
@@ -925,6 +1025,55 @@ class LaneSyncExecutor {
925
1025
  }
926
1026
  }
927
1027
 
1028
+ /**
1029
+ * Gates `hasIndependentHistoryBelowStateCommit`'s git call to the one case it can change the outcome
1030
+ * for: an `own-live` claim (the only ownership that would otherwise license an immediate delete) with
1031
+ * a reachable state commit to inspect. Extracted out of `reconcileLane` to keep the branching there.
1032
+ */
1033
+ async computeHasIndependentHistory(ownership, stateCommit, defaultBranch) {
1034
+ if (ownership !== 'own-live' || !stateCommit) return false;
1035
+ return this.hasIndependentHistoryBelowStateCommit(stateCommit, defaultBranch);
1036
+ }
1037
+
1038
+ /**
1039
+ * Whether the OLDEST commit unique to the branch is NOT bit-authored — durable, ancestry-based proof
1040
+ * a human created the branch before this reconciler touched it (a trailer would not survive later
1041
+ * ledger commits; see `oldestCommitIsNonSync` for the oldest-only rationale). Unreadable answers
1042
+ * `true`: a deletion guard errs toward keeping.
1043
+ */
1044
+ async hasIndependentHistoryBelowStateCommit(stateCommit, defaultBranch) {
1045
+ try {
1046
+ const log = await _git().git.raw(['log', `origin/${defaultBranch}..${stateCommit}`, '--first-parent', '--reverse', '--format=%B%x1e']);
1047
+ return (0, _syncState().oldestCommitIsNonSync)(log);
1048
+ } catch (e) {
1049
+ this.deps.logger.consoleWarning(`Could not read ${stateCommit}'s history against ${defaultBranch} to check for independent, ` + `pre-existing branch history; treating it as present (the branch is kept, not deleted): ${e?.message || e}`);
1050
+ return true;
1051
+ }
1052
+ }
1053
+
1054
+ /** Gates `differentLaneStillClaims` to the planner's first-contact path, the only one that reads it. */
1055
+ async computeBranchNamesDifferentLane(lastSyncedHead, hasDevCommits, mirroredLane, laneIdStr, branch, defaultBranch) {
1056
+ if (lastSyncedHead || !hasDevCommits) return false;
1057
+ return this.differentLaneStillClaims(mirroredLane, laneIdStr, branch, defaultBranch);
1058
+ }
1059
+
1060
+ /**
1061
+ * Whether adoption must refuse `branch` because its `.bitmap` names a DIFFERENT lane's still-live
1062
+ * claim. Blob equality with the fork-point `.bitmap`, not commit reachability: an `own-superseded`
1063
+ * branch is reachable yet still carries live work; only a `.bitmap` the branch never changed proves
1064
+ * the pointer is inherited. Unreadable stays conservative and blocks adoption.
1065
+ */
1066
+ async differentLaneStillClaims(mirroredLane, laneIdStr, branch, defaultBranch) {
1067
+ if (!mirroredLane || mirroredLane === laneIdStr) return false;
1068
+ const inherited = await this.branchInheritsBitmapFromDefault(branch, defaultBranch);
1069
+ return !inherited;
1070
+ }
1071
+
1072
+ /** Seam over `branchBitmapUnchangedSinceFork`, so `differentLaneStillClaims` is unit-testable without a real remote. */
1073
+ branchInheritsBitmapFromDefault(branch, defaultBranch) {
1074
+ return (0, _syncState().branchBitmapUnchangedSinceFork)(branch, defaultBranch);
1075
+ }
1076
+
928
1077
  /**
929
1078
  * The lane is gone from bit.cloud — close its PR, and retire the branch if the claim on it allows.
930
1079
  * Deleting the branch is the one irreversible thing this command does; `keepReason` distinguishes
@@ -1084,14 +1233,17 @@ class LaneSyncExecutor {
1084
1233
  }
1085
1234
 
1086
1235
  /**
1087
- * Drive the workspace's filesystem to the remote lane's content; returns the error so the caller can
1088
- * halt. Two traps make a plain `switchToLane` insufficient: (1) it defaults to `forceOurs`, which
1089
- * never touches the filesystem — the commit would claim the branch mirrors the lane over the default
1090
- * branch's files — so `forceOurs` is cleared and `forceTheirs` set; (2) switching onto the lane the
1091
- * workspace already sits on throws "already checked out", which `switchToLane` swallows as success
1092
- * and nothing materializes — so step off to main first.
1236
+ * Drive the workspace's filesystem onto the remote lane; returns the error so the caller can halt.
1237
+ * Two traps make a plain `switchToLane` insufficient: (1) its default `forceOurs` never touches the
1238
+ * filesystem, so the caller's commit would misrepresent whichever side it claims mirrors the other;
1239
+ * (2) switching onto the lane the workspace already sits on throws "already checked out", which
1240
+ * `switchToLane` swallows as success and nothing materializes — so step off to main first.
1241
+ *
1242
+ * `theirsWins` picks the direction: `true` (import-lane) checks the lane's files out over the disk;
1243
+ * `false` (adopt-branch) keeps the workspace's files — the branch's content is what the lane is
1244
+ * being asked to reflect.
1093
1245
  */
1094
- async materializeLane(laneIdStr) {
1246
+ async materializeLane(laneIdStr, theirsWins = true) {
1095
1247
  const {
1096
1248
  logger
1097
1249
  } = this.deps;
@@ -1108,9 +1260,16 @@ class LaneSyncExecutor {
1108
1260
  }
1109
1261
  }
1110
1262
  const switchErr = await this.deps.ci.switchToLaneForSync(laneIdStr, {
1111
- forceOurs: false,
1112
- forceTheirs: true,
1113
- writeAdoptedFiles: true
1263
+ forceOurs: !theirsWins,
1264
+ forceTheirs: theirsWins,
1265
+ // The whole lane has to land in the workspace here, including components it has that this
1266
+ // workspace doesn't track yet - materializing the lane is the point of this step. That is the
1267
+ // opposite of `bit ci pr`, where the git checkout is the source of truth and `switchToLane`'s
1268
+ // `workspaceOnly` default keeps the switch from writing lane-only components.
1269
+ workspaceOnly: false,
1270
+ // Follows the direction: in adopt mode the missing-component retry must not write the lane's
1271
+ // files over the branch's — the ledger commit would then push the lane's content onto the branch.
1272
+ writeAdoptedFiles: theirsWins
1114
1273
  });
1115
1274
  if (switchErr) return switchErr;
1116
1275