amalgm 0.1.218 → 0.1.219

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.218",
3
+ "version": "0.1.219",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -685,6 +685,51 @@ test('a local commit made after the verified card snapshot is never parked or ov
685
685
  );
686
686
  });
687
687
 
688
+ test('a pending descendant commit catches up without becoming a losing commit', async () => {
689
+ const { repo, oid: baseOid } = await makeRepo('pending-descendant-card');
690
+ const firstPatch = await patchFor(repo, { 'a.txt': 'before commit\n' });
691
+ const secondPatch = await patchFor(repo, {
692
+ 'a.txt': 'before commit\n',
693
+ 'b.txt': 'after commit\n',
694
+ });
695
+ const winningCard = card(repo, baseOid, 1);
696
+ const before = {
697
+ repoKey: 'root:test',
698
+ card: winningCard,
699
+ transitions: [transition('machine-peer', 2, baseOid, '', firstPatch)],
700
+ parked: [],
701
+ };
702
+
703
+ await materializeRepo(winningCard, before, 2);
704
+ await git(repo, 'add', '.');
705
+ await git(repo, 'commit', '-m', 'local pending card');
706
+ const pendingOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
707
+
708
+ const pendingCard = card(repo, pendingOid, null);
709
+ const caughtUp = await materializeRepo(pendingCard, {
710
+ ...before,
711
+ transitions: [
712
+ ...before.transitions,
713
+ transition('machine-peer', 3, baseOid, firstPatch, secondPatch),
714
+ ],
715
+ }, 3);
716
+
717
+ assert.equal(caughtUp.state, 'in-sync');
718
+ assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), pendingOid);
719
+ assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'before commit\n');
720
+ assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'after commit\n');
721
+ const staged = (await git(repo, 'diff', '--cached', '--name-only')).trim();
722
+ const status = (await git(repo, 'status', '--short')).trim();
723
+ assert.equal(staged, 'b.txt', status);
724
+ await assert.rejects(
725
+ git(repo, 'rev-parse', `refs/amalgm/parked/${pendingOid}`),
726
+ );
727
+ const state = readRepoState('root:test');
728
+ assert.equal(state.baseOid, pendingOid);
729
+ assert.equal(state.appliedThrough, 3);
730
+ assert.equal(state.cardVersion, null);
731
+ });
732
+
688
733
  test('an empty index advance cannot overwrite a commit that just finished', async () => {
689
734
  const { repo } = await makeRepo('index-commit-race');
690
735
  const observedIndex = (await git(repo, 'write-tree')).trim();
@@ -148,6 +148,15 @@ test('only a value different from both the materialized baseline and the rail is
148
148
  branches: [{ name: 'main', oid: oidC }],
149
149
  };
150
150
  assert.equal(hasLocalContribution(local, remoteAdvance, baseline), false);
151
+ assert.equal(hasLocalContribution({
152
+ ...local,
153
+ head: { branch: 'main', oid: oidB, detached: false },
154
+ refs: [{ name: 'main', oid: oidB }],
155
+ }, remoteAdvance, {
156
+ ...baseline,
157
+ baseOid: oidB,
158
+ pendingCard: true,
159
+ }), true);
151
160
  assert.equal(hasLocalContribution(local, remoteAdvance, baseline, {
152
161
  checkpoint: 'repo-checkpoint@8',
153
162
  machine: 'machine-a',
@@ -288,6 +288,14 @@ async function moveToWinningCard(localCard, targetCard) {
288
288
  && current.head.branch === targetCard.head?.branch
289
289
  ) return { state: 'ready', moved: false };
290
290
 
291
+ const pendingCard = current.head.oid
292
+ && targetCard.head?.oid
293
+ && current.head.branch === targetCard.head?.branch
294
+ && await isAncestor(localCard.path, targetCard.head.oid, current.head.oid);
295
+ if (pendingCard) {
296
+ return { state: 'ready', moved: false, pendingCard: true, conflicts: [] };
297
+ }
298
+
291
299
  const objects = await obtainCardObjects(localCard, targetCard);
292
300
  if (objects.state !== 'ready') return { state: 'waiting', reason: objects.state };
293
301
 
@@ -594,38 +602,87 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
594
602
  const reduced = await reduceCheckpoint(localCard, plan);
595
603
  if (reduced.state !== 'ready') return reduced;
596
604
 
597
- const baseTree = await merge.treeOf(localCard.path, plan.card.head.oid);
598
- const captured = await captureCheckpoint(localCard.path);
599
- const represented = captureIsOnRail(plan, captured);
600
- const capturedIndex = represented
601
- ? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
602
- : null;
603
- const capturedWorktree = represented
604
- ? await merge.treeOfPatch(localCard.path, capturedIndex, captured.worktreeDiff)
605
- : null;
606
- const heldWorktree = capturedWorktree || (
605
+ const sharedBaseTree = await merge.treeOf(localCard.path, plan.card.head.oid);
606
+ const previousHeldWorktree = (
607
607
  previous?.baseOid === plan.card.head.oid
608
608
  && previous.heldTree
609
609
  && await merge.hasTree(localCard.path, previous.heldTree)
610
610
  ? previous.heldTree
611
- : baseTree
611
+ : sharedBaseTree
612
612
  );
613
- const heldIndex = capturedIndex || (
613
+ const previousHeldIndex = (
614
614
  previous?.baseOid === plan.card.head.oid
615
615
  && previous.heldIndexTree
616
616
  && await merge.hasTree(localCard.path, previous.heldIndexTree)
617
617
  ? previous.heldIndexTree
618
- : baseTree
618
+ : sharedBaseTree
619
619
  );
620
+ let stateBaseOid = plan.card.head.oid;
621
+ let stateBranch = plan.card.head.branch;
622
+ let stateCardVersion = plan.card.positionVersion;
623
+ let stateCardMutationId = plan.card.positionMutationId;
624
+ let stateIndexDiff = reduced.indexDiff;
625
+ let stateWorktreeDiff = reduced.worktreeDiff;
626
+ let heldWorktree;
627
+ let heldIndex;
628
+ let targetWorktree;
629
+ let targetIndex;
630
+ const pendingConflicts = [];
631
+
632
+ if (cardResult.pendingCard) {
633
+ const pendingBaseTree = await merge.treeOf(localCard.path, localCard.head.oid);
634
+ const rebasedIndex = await merge.reduceTree(
635
+ localCard.path,
636
+ pendingBaseTree,
637
+ previousHeldIndex,
638
+ reduced.indexTree,
639
+ { label: `pending-card-index-${appliedThrough ?? 'local'}` },
640
+ );
641
+ const rebasedWorktree = await merge.reduceTree(
642
+ localCard.path,
643
+ pendingBaseTree,
644
+ previousHeldWorktree,
645
+ reduced.worktreeTree,
646
+ { label: `pending-card-worktree-${appliedThrough ?? 'local'}` },
647
+ );
648
+ heldIndex = pendingBaseTree;
649
+ heldWorktree = pendingBaseTree;
650
+ targetIndex = rebasedIndex.tree;
651
+ targetWorktree = rebasedWorktree.tree;
652
+ stateBaseOid = localCard.head.oid;
653
+ stateBranch = localCard.head.branch;
654
+ stateCardVersion = null;
655
+ stateCardMutationId = null;
656
+ stateIndexDiff = await merge.diffOfTree(localCard.path, stateBaseOid, targetIndex);
657
+ stateWorktreeDiff = await merge.diffOfTree(localCard.path, targetIndex, targetWorktree);
658
+ pendingConflicts.push(
659
+ ...rebasedIndex.conflicts.map((item) => ({ ...item, layer: 'pending-card-index' })),
660
+ ...rebasedWorktree.conflicts.map((item) => ({ ...item, layer: 'pending-card-worktree' })),
661
+ );
662
+ } else {
663
+ const captured = await captureCheckpoint(localCard.path);
664
+ const represented = captureIsOnRail(plan, captured);
665
+ const capturedIndex = represented
666
+ ? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
667
+ : null;
668
+ const capturedWorktree = represented
669
+ ? await merge.treeOfPatch(localCard.path, capturedIndex, captured.worktreeDiff)
670
+ : null;
671
+ heldWorktree = capturedWorktree || previousHeldWorktree;
672
+ heldIndex = capturedIndex || previousHeldIndex;
673
+ targetWorktree = reduced.worktreeTree;
674
+ targetIndex = reduced.indexTree;
675
+ }
676
+
620
677
  const currentIndex = await merge.indexTree(localCard.path);
621
678
  const reducedLocalIndex = await merge.reduceTree(
622
679
  localCard.path,
623
680
  currentIndex,
624
681
  heldIndex,
625
- reduced.indexTree,
682
+ targetIndex,
626
683
  { label: `index-${reduced.checkpointRevision || 'clean'}` },
627
684
  );
628
- const applied = await merge.advance(localCard.path, heldWorktree, reduced.worktreeTree, {
685
+ const applied = await merge.advance(localCard.path, heldWorktree, targetWorktree, {
629
686
  label: `checkpoint-${reduced.checkpointRevision || 'clean'}`,
630
687
  });
631
688
  const indexResult = await advanceIndex(localCard.path, currentIndex, reducedLocalIndex.tree);
@@ -633,28 +690,30 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
633
690
  const state = writeRepoState(plan.repoKey, {
634
691
  path: localCard.path,
635
692
  replica: localCard.replica,
636
- branch: plan.card.head.branch,
637
- baseOid: plan.card.head.oid,
638
- cardVersion: plan.card.positionVersion,
639
- cardMutationId: plan.card.positionMutationId,
693
+ branch: stateBranch,
694
+ baseOid: stateBaseOid,
695
+ cardVersion: stateCardVersion,
696
+ cardMutationId: stateCardMutationId,
697
+ pendingCard: Boolean(cardResult.pendingCard),
640
698
  appliedThrough,
641
- checkpointRevision: reduced.checkpointRevision,
642
- indexDiff: reduced.indexDiff,
643
- worktreeDiff: reduced.worktreeDiff,
699
+ checkpointRevision: revisionOf(stateBaseOid, stateIndexDiff, stateWorktreeDiff),
700
+ indexDiff: stateIndexDiff,
701
+ worktreeDiff: stateWorktreeDiff,
644
702
  checkpointVersions: plan.transitions.map((entry) => entry.version),
645
703
  branches: plan.branches || [],
646
704
  heldTree: applied.heldTree,
647
- heldIndexTree: reduced.indexTree,
705
+ heldIndexTree: targetIndex,
648
706
  parked,
649
707
  parkedWaiting: [...parkedWaiting, ...branchResult.waiting],
650
708
  conflicts: [
651
709
  ...(cardResult.conflicts || []),
652
710
  ...reduced.conflicts,
711
+ ...pendingConflicts,
653
712
  ...reducedLocalIndex.conflicts.map((item) => ({ ...item, layer: 'index-materialize' })),
654
713
  ...applied.conflicts,
655
714
  ],
656
- materialized: applied.heldTree === reduced.worktreeTree
657
- && reducedLocalIndex.tree === reduced.indexTree,
715
+ materialized: applied.heldTree === targetWorktree
716
+ && reducedLocalIndex.tree === targetIndex,
658
717
  });
659
718
  appendStateEvent({
660
719
  resource: 'workspace-reducer',
@@ -252,7 +252,8 @@ function hasLocalContribution(card, plan, baseline, checkpoint = null, previousC
252
252
  const onRail = samePosition(card, plan.card);
253
253
  const atBaseline = baseline
254
254
  && card.head?.branch === baseline.branch
255
- && card.head?.oid === baseline.baseOid;
255
+ && card.head?.oid === baseline.baseOid
256
+ && !baseline.pendingCard;
256
257
  const alreadyPublished = validVersion(previousCard?.positionVersion)
257
258
  && samePosition(card, previousCard);
258
259
  const reversesPublishedSharedPosition = previousCard