@amalgm/live 0.2.81-main.40832913201 → 0.2.81-main.40972490558

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.
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * Files: an artifact carries only the blobs its parent's state lacks, so a
6
6
  * file the receiver does not already hold is found by walking back to the
7
- * artifact that first carried it.
7
+ * artifact that first carried it, each artifact read once and only as far
8
+ * back as a file asked for first travelled.
8
9
  *
9
10
  * Objects: a pack is thin against its excluded roots, so it installs once
10
11
  * everything they reach stands here; otherwise its parent's pack comes first,
@@ -35,9 +36,10 @@ export interface GitChainPacks {
35
36
  export interface GitChainOwnerWait {
36
37
  readonly kind: 'owner';
37
38
  }
38
- /** The bytes of every wanted blob: the target's files the receiver does not
39
- * hold. */
40
- export declare function gitChainBlobs(target: string, artifact: GitArtifactReader, wants: ReadonlySet<string>): Promise<Checked<ReadonlyMap<string, Uint8Array>>>;
39
+ /** Reads blobs from the chain back from the target: each artifact read
40
+ * once, only as far back as a blob asked for first travelled. Null for a
41
+ * blob no artifact on the chain carries. */
42
+ export declare function gitChainReader(target: string, artifact: GitArtifactReader): (oid: string) => Promise<Uint8Array | null>;
41
43
  /** The packs that bring the target's objects here. */
42
44
  export declare function gitChainPacks(target: string, artifact: GitArtifactReader, holds: GitHolds): Promise<Checked<GitChainPacks | GitChainOwnerWait>>;
43
45
  //# sourceMappingURL=chain.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chain.d.ts","sourceRoot":"","sources":["../../src/git/chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,sDAAsD;AACtD,MAAM,MAAM,iBAAiB,GAAG,CAAC,gBAAgB,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;AAEnF,wEAAwE;AACxE,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAErE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;CACvC;AAED;mBACmB;AACnB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;WACW;AACX,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAC7E,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAS/E;AAED,sDAAsD;AACtD,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAC7E,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,GAAG,iBAAiB,CAAC,CAAC,CAUtE"}
1
+ {"version":3,"file":"chain.d.ts","sourceRoot":"","sources":["../../src/git/chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,sDAAsD;AACtD,MAAM,MAAM,iBAAiB,GAAG,CAAC,gBAAgB,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;AAEnF,wEAAwE;AACxE,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAErE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;CACvC;AAED;mBACmB;AACnB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;4CAE4C;AAC5C,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAWvH;AAED,sDAAsD;AACtD,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAC7E,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,GAAG,iBAAiB,CAAC,CAAC,CAUtE"}
package/dist/git/chain.js CHANGED
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * Files: an artifact carries only the blobs its parent's state lacks, so a
6
6
  * file the receiver does not already hold is found by walking back to the
7
- * artifact that first carried it.
7
+ * artifact that first carried it, each artifact read once and only as far
8
+ * back as a file asked for first travelled.
8
9
  *
9
10
  * Objects: a pack is thin against its excluded roots, so it installs once
10
11
  * everything they reach stands here; otherwise its parent's pack comes first,
@@ -20,20 +21,22 @@
20
21
  * then asks its Git which packs it lacks.
21
22
  */
22
23
  import { fail, pass } from '../contracts/validate.js';
23
- /** The bytes of every wanted blob: the target's files the receiver does not
24
- * hold. */
25
- export async function gitChainBlobs(target, artifact, wants) {
26
- const blobs = new Map();
27
- for (let next = target; blobs.size < wants.size;) {
28
- if (next === null)
29
- return fail(`the chain from ${target} carries no bytes for ${wants.size - blobs.size} wanted files`);
30
- const read = await artifact(next);
31
- for (const [oid, bytes] of read.blobs)
32
- if (wants.has(oid))
33
- blobs.set(oid, bytes);
34
- next = read.parent;
35
- }
36
- return pass(blobs);
24
+ /** Reads blobs from the chain back from the target: each artifact read
25
+ * once, only as far back as a blob asked for first travelled. Null for a
26
+ * blob no artifact on the chain carries. */
27
+ export function gitChainReader(target, artifact) {
28
+ const found = new Map();
29
+ let next = target;
30
+ return async (oid) => {
31
+ while (!found.has(oid) && next !== null) {
32
+ const read = await artifact(next);
33
+ for (const [blob, bytes] of read.blobs)
34
+ if (!found.has(blob))
35
+ found.set(blob, bytes);
36
+ next = read.parent;
37
+ }
38
+ return found.get(oid) ?? null;
39
+ };
37
40
  }
38
41
  /** The packs that bring the target's objects here. */
39
42
  export async function gitChainPacks(target, artifact, holds) {
@@ -1 +1 @@
1
- {"version":3,"file":"chain.js","sourceRoot":"","sources":["../../src/git/chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,IAAI,EAAE,IAAI,EAAgB,MAAM,0BAA0B,CAAC;AAqBpE;WACW;AACX,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,QAA2B,EAC7E,KAA0B;IAC1B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC5C,KAAK,IAAI,IAAI,GAAkB,MAAM,EAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC;QAChE,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,kBAAkB,MAAM,yBAAyB,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,eAAe,CAAC,CAAC;QACxH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;QAClC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACjF,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;AACrB,CAAC;AAED,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,QAA2B,EAC7E,KAAe;IACf,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,KAAK,IAAI,IAAI,GAAkB,MAAM,EAAE,IAAI,KAAK,IAAI,GAAG,CAAC;QACtD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACxF,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACrH,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC,kBAAkB,MAAM,iDAAiD,CAAC,CAAC;AACzF,CAAC"}
1
+ {"version":3,"file":"chain.js","sourceRoot":"","sources":["../../src/git/chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,IAAI,EAAE,IAAI,EAAgB,MAAM,0BAA0B,CAAC;AAqBpE;;4CAE4C;AAC5C,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,QAA2B;IACxE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC5C,IAAI,IAAI,GAAkB,MAAM,CAAC;IACjC,OAAO,KAAK,EAAC,GAAG,EAAC,EAAE;QACjB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,GAAgB,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC/C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK;gBAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;oBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACrF,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;QACrB,CAAC;QACD,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IAChC,CAAC,CAAC;AACJ,CAAC;AAED,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,QAA2B,EAC7E,KAAe;IACf,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,KAAK,IAAI,IAAI,GAAkB,MAAM,EAAE,IAAI,KAAK,IAAI,GAAG,CAAC;QACtD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACxF,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACrH,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC,kBAAkB,MAAM,iDAAiD,CAAC,CAAC;AACzF,CAAC"}
@@ -4,4 +4,5 @@ export { compareGitText } from './text.js';
4
4
  export * from './artifact.js';
5
5
  export * from './chain.js';
6
6
  export * from './plan.js';
7
+ export * from './merge.js';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/git/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/git/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
package/dist/git/index.js CHANGED
@@ -4,4 +4,5 @@ export { compareGitText } from './text.js';
4
4
  export * from './artifact.js';
5
5
  export * from './chain.js';
6
6
  export * from './plan.js';
7
+ export * from './merge.js';
7
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/git/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/git/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Keep both: two writers on one disk who each commit keep both commits, and
3
+ * so do two machines that change one repository at once.
4
+ *
5
+ * A repository's records stand in one order, each a whole state. A record
6
+ * that follows the state this machine last recorded or applied replaces it.
7
+ * One that does not was made beside it: both moved on from the state where
8
+ * their chains of records meet, the base, and Apply joins the two here
9
+ * instead of letting the later replace the earlier. The machine whose record
10
+ * the cloud accepted first joins: on the other, its own later record protects
11
+ * its ground from the first (src/apply/local-work.ts). The join stands here
12
+ * as a person's change to theirs, so this machine records it after theirs and
13
+ * the other machine applies it plainly. Nothing in a join names the machine
14
+ * making it, so a third machine that joins the same sides makes the same
15
+ * state.
16
+ *
17
+ * - A Git-directory file takes the side that changed it; theirs when both did.
18
+ * - A ref, loose or packed, is decided by name: the side that moved it, or
19
+ * the later of two on one line of history. A branch both moved apart gets
20
+ * a merge commit when Git merges them with no file changed differently;
21
+ * otherwise theirs stays and ours is kept beside it, as `git fetch` keeps
22
+ * what it will not overwrite, at `<name>-<commit>`; so is a ref ours made
23
+ * where theirs's need a folder, or the reverse. A remote-tracking ref
24
+ * takes theirs, which the next fetch refreshes.
25
+ * - A reflog both changed keeps both sides' lines, the side its ref ends at
26
+ * last; the stash keeps both sides' entries, ours's on top.
27
+ * - The index follows HEAD: the index of the side whose commit HEAD names,
28
+ * or ours carried onto a merge commit as `git read-tree -m` carries it.
29
+ *
30
+ * Where the merge has no answer, theirs replaces ours (`gitMergeable`): refs
31
+ * kept in reftable, a linked worktree, whose branches its main checkout's
32
+ * state holds, and sides that differ in format, home or borrowed storage.
33
+ * Everything Git computes is the host's, through `GitMergeGit`; the rest is
34
+ * decided here, so every host joins alike and the same sides always make the
35
+ * same state.
36
+ */
37
+ import type { Sha256Hex } from '../entities/types.js';
38
+ import { type GitState } from './state.js';
39
+ export interface GitMergeSides {
40
+ /** Where the two chains of records meet. */
41
+ readonly base: GitState;
42
+ /** The state this machine last recorded or applied. */
43
+ readonly ours: GitState;
44
+ /** The state of the record being applied. */
45
+ readonly theirs: GitState;
46
+ }
47
+ /** Who writes the merge commits and their reflog lines. */
48
+ export declare const GIT_MERGE_IDENTITY: {
49
+ readonly name: "Amalgm";
50
+ readonly email: "amalgm@localhost";
51
+ };
52
+ export interface GitJoin {
53
+ readonly commit: string;
54
+ /** Its committer time in seconds, as its reflog line states it. */
55
+ readonly time: number;
56
+ }
57
+ /** What the merge asks Git, in the repository holding both sides' objects. */
58
+ export interface GitMergeGit {
59
+ /** Whether `ancestor` is `descendant` or reached from it. */
60
+ isAncestor(ancestor: string, descendant: string): Promise<boolean>;
61
+ /** A merge commit of `theirs` and `ours`, in that order, by
62
+ * GIT_MERGE_IDENTITY at the later of their committer times, when Git
63
+ * merges them cleanly and no file changed differently on the two sides;
64
+ * null otherwise.
65
+ * The same two commits always make the same commit. */
66
+ join(theirs: string, ours: string, message: string): Promise<GitJoin | null>;
67
+ /** What an index becomes when HEAD moves from commit `from` to `to`,
68
+ * keeping what it stages (`git read-tree -m`): its bytes and the meaning
69
+ * a GitIndex states; null when Git cannot carry it, or it skips files a
70
+ * sparse checkout leaves out. */
71
+ carryIndex(index: Uint8Array, from: string, to: string): Promise<{
72
+ readonly meaning: string;
73
+ readonly bytes: Uint8Array;
74
+ } | null>;
75
+ }
76
+ export interface GitMerge {
77
+ readonly state: GitState;
78
+ /** The bytes of the files the merge wrote, by blob ID. */
79
+ readonly made: ReadonlyMap<string, Uint8Array>;
80
+ /** The merge commits it made. */
81
+ readonly joins: readonly string[];
82
+ }
83
+ /** Whether the sides can be joined; otherwise theirs replaces ours. */
84
+ export declare function gitMergeable({ base, ours, theirs }: GitMergeSides): boolean;
85
+ /** Where two chains of records meet: the nearest version both reach through
86
+ * their parents, or null when they never do. The chains step back in turn,
87
+ * so the walk costs twice the longer way back. */
88
+ export declare function gitMergeBase(ours: string, theirs: string, parentOf: (version: string) => Promise<string | null>): Promise<string | null>;
89
+ /** The sides joined by the file rule alone, refs as files and the index
90
+ * theirs's: what a receiver's Git acts under while it merges, its
91
+ * configuration and borrowed storage. */
92
+ export declare function gitMergeFiles(sides: GitMergeSides): GitState;
93
+ /** The sides joined. `bytes` reads any blob a side's files name. */
94
+ export declare function gitMergeStates(sides: GitMergeSides, bytes: (oid: string) => Promise<Uint8Array>, git: GitMergeGit, sha256: Sha256Hex): Promise<GitMerge>;
95
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/git/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAOtD,OAAO,EAA2D,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGpG,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;CAC3B;AAED,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB;;;CAAyD,CAAC;AAEzF,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,6DAA6D;IAC7D,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE;;;;2DAIuD;IACvD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC7E;;;qCAGiC;IACjC,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CACnI;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC/C,iCAAiC;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,aAAa,GAAG,OAAO,CAM3E;AAED;;kDAEkD;AAClD,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAC7D,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAe/E;AA+CD;;yCAEyC;AACzC,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,CAI5D;AAyCD,oEAAoE;AACpE,wBAAsB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,EACpG,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuLxD"}
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Keep both: two writers on one disk who each commit keep both commits, and
3
+ * so do two machines that change one repository at once.
4
+ *
5
+ * A repository's records stand in one order, each a whole state. A record
6
+ * that follows the state this machine last recorded or applied replaces it.
7
+ * One that does not was made beside it: both moved on from the state where
8
+ * their chains of records meet, the base, and Apply joins the two here
9
+ * instead of letting the later replace the earlier. The machine whose record
10
+ * the cloud accepted first joins: on the other, its own later record protects
11
+ * its ground from the first (src/apply/local-work.ts). The join stands here
12
+ * as a person's change to theirs, so this machine records it after theirs and
13
+ * the other machine applies it plainly. Nothing in a join names the machine
14
+ * making it, so a third machine that joins the same sides makes the same
15
+ * state.
16
+ *
17
+ * - A Git-directory file takes the side that changed it; theirs when both did.
18
+ * - A ref, loose or packed, is decided by name: the side that moved it, or
19
+ * the later of two on one line of history. A branch both moved apart gets
20
+ * a merge commit when Git merges them with no file changed differently;
21
+ * otherwise theirs stays and ours is kept beside it, as `git fetch` keeps
22
+ * what it will not overwrite, at `<name>-<commit>`; so is a ref ours made
23
+ * where theirs's need a folder, or the reverse. A remote-tracking ref
24
+ * takes theirs, which the next fetch refreshes.
25
+ * - A reflog both changed keeps both sides' lines, the side its ref ends at
26
+ * last; the stash keeps both sides' entries, ours's on top.
27
+ * - The index follows HEAD: the index of the side whose commit HEAD names,
28
+ * or ours carried onto a merge commit as `git read-tree -m` carries it.
29
+ *
30
+ * Where the merge has no answer, theirs replaces ours (`gitMergeable`): refs
31
+ * kept in reftable, a linked worktree, whose branches its main checkout's
32
+ * state holds, and sides that differ in format, home or borrowed storage.
33
+ * Everything Git computes is the host's, through `GitMergeGit`; the rest is
34
+ * decided here, so every host joins alike and the same sides always make the
35
+ * same state.
36
+ */
37
+ import { utf8Decode, utf8Encode } from '../entities/bytes.js';
38
+ import { GIT_ALTERNATES_PATH } from './alternates.js';
39
+ import { gitBlobId } from './artifact.js';
40
+ import { isGitLockPath } from './plan.js';
41
+ import { gitChainReflog, gitDropPackedRefs, gitLooseRef, gitMergeReflog, gitPackedRefs, gitReflogLine, gitReflogTop, } from './refs.js';
42
+ import { gitSharesObjects, isGitOid } from './state.js';
43
+ import { compareGitText } from './text.js';
44
+ /** Who writes the merge commits and their reflog lines. */
45
+ export const GIT_MERGE_IDENTITY = { name: 'Amalgm', email: 'amalgm@localhost' };
46
+ /** Whether the sides can be joined; otherwise theirs replaces ours. */
47
+ export function gitMergeable({ base, ours, theirs }) {
48
+ const borrowed = (state) => state.files.find(file => file.path === GIT_ALTERNATES_PATH)?.oid ?? null;
49
+ const reftable = (state) => state.files.some(file => file.path === 'reftable');
50
+ return base.format === ours.format && ours.format === theirs.format && !gitSharesObjects(ours)
51
+ && ours.home?.entity === theirs.home?.entity && ours.home?.path === theirs.home?.path
52
+ && borrowed(ours) === borrowed(theirs) && ![base, ours, theirs].some(reftable);
53
+ }
54
+ /** Where two chains of records meet: the nearest version both reach through
55
+ * their parents, or null when they never do. The chains step back in turn,
56
+ * so the walk costs twice the longer way back. */
57
+ export async function gitMergeBase(ours, theirs, parentOf) {
58
+ if (ours === theirs)
59
+ return ours;
60
+ const at = [ours, theirs];
61
+ const seen = [new Set([ours]), new Set([theirs])];
62
+ while (at[0] !== null || at[1] !== null) {
63
+ for (const side of [0, 1]) {
64
+ const from = at[side];
65
+ if (from === null || from === undefined)
66
+ continue;
67
+ const parent = at[side] = await parentOf(from);
68
+ if (parent === null)
69
+ continue;
70
+ if (seen[1 - side].has(parent))
71
+ return parent;
72
+ seen[side].add(parent);
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ const FOLDER = '040000';
78
+ const byPath = (files) => new Map(files.map(file => [file.path, file]));
79
+ const same = (left, right) => left === undefined ? right === undefined : right !== undefined && left.mode === right.mode && left.oid === right.oid;
80
+ /** The side that changed an entry from the base; theirs when both did. */
81
+ const changedSide = (base, ours, theirs) => same(ours, theirs) || same(theirs, base) ? ours : theirs;
82
+ function fileRule(base, ours, theirs) {
83
+ const merged = new Map();
84
+ for (const path of new Set([...base.keys(), ...ours.keys(), ...theirs.keys()])) {
85
+ const file = changedSide(base.get(path), ours.get(path), theirs.get(path));
86
+ if (file !== undefined)
87
+ merged.set(path, file);
88
+ }
89
+ return merged;
90
+ }
91
+ const parents = (path) => {
92
+ const found = [];
93
+ for (let at = path.indexOf('/'); at > 0; at = path.indexOf('/', at + 1))
94
+ found.push(path.slice(0, at));
95
+ return found;
96
+ };
97
+ /** A tree again, sorted: where an entry stands beneath a file, theirs's
98
+ * whole subtree there replaces both sides', and every parent folder stands. */
99
+ function asTree(merged, theirs) {
100
+ const clashes = new Set();
101
+ for (const path of merged.keys()) {
102
+ const clash = parents(path).find(parent => merged.has(parent) && merged.get(parent).mode !== FOLDER);
103
+ if (clash !== undefined)
104
+ clashes.add(clash);
105
+ }
106
+ const clashed = (path) => clashes.has(path) || parents(path).some(parent => clashes.has(parent));
107
+ if (clashes.size > 0) {
108
+ for (const path of [...merged.keys()])
109
+ if (clashed(path))
110
+ merged.delete(path);
111
+ for (const [path, file] of theirs)
112
+ if (clashed(path))
113
+ merged.set(path, file);
114
+ }
115
+ for (const path of [...merged.keys()]) {
116
+ for (const parent of parents(path))
117
+ if (!merged.has(parent))
118
+ merged.set(parent, { path: parent, mode: FOLDER, oid: null });
119
+ }
120
+ return [...merged.values()].sort((left, right) => compareGitText(left.path, right.path));
121
+ }
122
+ /** The sides joined by the file rule alone, refs as files and the index
123
+ * theirs's: what a receiver's Git acts under while it merges, its
124
+ * configuration and borrowed storage. */
125
+ export function gitMergeFiles(sides) {
126
+ const theirs = byPath(sides.theirs.files);
127
+ const files = asTree(fileRule(byPath(sides.base.files), byPath(sides.ours.files), theirs), theirs);
128
+ return { format: sides.theirs.format, home: sides.theirs.home, files, index: sides.theirs.index };
129
+ }
130
+ const SIDES = ['base', 'ours', 'theirs'];
131
+ const isLooseRef = (file) => file.mode !== FOLDER
132
+ && (file.path === 'HEAD' || file.path.startsWith('refs/') && !isGitLockPath(file.path));
133
+ const isRefFile = (file) => isLooseRef(file) || file.path === 'packed-refs';
134
+ const isReflogPath = (path) => path.startsWith('logs/') && !isGitLockPath(path);
135
+ /** Where ours is kept beside a ref that ends elsewhere, a detached HEAD's
136
+ * as a branch: the ref and the commit; flattened beneath its namespace when
137
+ * the ref's own place is taken, then with the whole commit ID. They name
138
+ * nothing of the machine joining, so every machine that joins the same sides
139
+ * names it alike. */
140
+ const besideNames = (name, commit) => {
141
+ const ref = name === 'HEAD' ? 'refs/heads/detached' : name;
142
+ const space = /^refs\/[^/]+\//.exec(ref)?.[0] ?? 'refs/';
143
+ const flat = space + ref.slice(space.length).replace(/\//g, '-');
144
+ return [`${ref}-${commit.slice(0, 7)}`, `${flat}-${commit.slice(0, 7)}`, `${flat}-${commit}`];
145
+ };
146
+ /** Ref names as Git allows them: none stands where another needs a folder. */
147
+ class RefNames {
148
+ #names = new Set();
149
+ #folders = new Set();
150
+ constructor(names) { for (const name of names)
151
+ this.add(name); }
152
+ has(name) { return this.#names.has(name); }
153
+ add(name) { this.#names.add(name); for (const folder of parents(name))
154
+ this.#folders.add(folder); }
155
+ /** Whether a new name would stand beneath one, or above others. */
156
+ clashes(name) { return this.#folders.has(name) || parents(name).some(folder => this.#names.has(folder)); }
157
+ }
158
+ const STASH_LOG = 'logs/refs/stash';
159
+ /** The sides joined. `bytes` reads any blob a side's files name. */
160
+ export async function gitMergeStates(sides, bytes, git, sha256) {
161
+ const files = {
162
+ base: byPath(sides.base.files), ours: byPath(sides.ours.files), theirs: byPath(sides.theirs.files),
163
+ };
164
+ const entries = (path) => SIDES.map(side => files[side].get(path));
165
+ const made = new Map();
166
+ const make = (path, text) => {
167
+ const content = typeof text === 'string' ? utf8Encode(text) : text;
168
+ const oid = gitBlobId(content, sha256);
169
+ made.set(oid, content);
170
+ return { path, mode: '100644', oid };
171
+ };
172
+ const read = async (file) => file?.oid ? utf8Decode(made.get(file.oid) ?? await bytes(file.oid)) : '';
173
+ const packed = new Map();
174
+ const packedOf = (side) => {
175
+ if (!packed.has(side))
176
+ packed.set(side, read(files[side].get('packed-refs')).then(gitPackedRefs));
177
+ return packed.get(side);
178
+ };
179
+ const values = new Map();
180
+ const value = (side, name) => {
181
+ const key = `${side}\0${name}`;
182
+ if (!values.has(key)) {
183
+ const loose = files[side].get(name);
184
+ values.set(key, loose !== undefined && isLooseRef(loose) ? read(loose).then(gitLooseRef)
185
+ : name === 'HEAD' ? Promise.resolve(null) : packedOf(side).then(refs => refs.get(name) ?? null));
186
+ }
187
+ return values.get(key);
188
+ };
189
+ const merged = fileRule(files.base, files.ours, files.theirs);
190
+ const paths = new Set(SIDES.flatMap(side => [...files[side].keys()]));
191
+ // A reflog both sides changed keeps both sides' lines, the side its ref
192
+ // ends at last, so `@{0}` names where it stands; the HEAD's and the
193
+ // stash's whatever else either did, and one side's dropped ref keeps the
194
+ // other's moved one. The stash is a stack: ours's entries go on top.
195
+ const mergeLog = async (path, oursLast) => {
196
+ const [base, ours, theirs] = entries(path);
197
+ if (same(ours, theirs) || same(ours, base) || same(theirs, base))
198
+ return;
199
+ if ([base, ours, theirs].some(file => file?.mode === FOLDER))
200
+ return;
201
+ if (path !== 'logs/HEAD' && path !== STASH_LOG && (ours === undefined || theirs === undefined)) {
202
+ merged.set(path, (ours ?? theirs));
203
+ return;
204
+ }
205
+ const [was, own, their] = [await read(base), await read(ours), await read(theirs)];
206
+ const text = oursLast ? gitMergeReflog(was, own, their) : gitMergeReflog(was, their, own);
207
+ if (text === '')
208
+ merged.delete(path);
209
+ else
210
+ merged.set(path, make(path, path === STASH_LOG ? gitChainReflog(text) : text));
211
+ };
212
+ await mergeLog(STASH_LOG, true);
213
+ // Every ref some side holds differently: the loose files that differ, and
214
+ // every packed ref when packed-refs does.
215
+ const names = new Set();
216
+ for (const path of paths) {
217
+ const [base, ours, theirs] = entries(path);
218
+ if (same(base, ours) && same(ours, theirs))
219
+ continue;
220
+ if (path === 'packed-refs')
221
+ for (const side of SIDES)
222
+ for (const name of (await packedOf(side)).keys())
223
+ names.add(name);
224
+ else if ([base, ours, theirs].some(file => file !== undefined && isLooseRef(file)))
225
+ names.add(path);
226
+ }
227
+ const finals = new Map();
228
+ const kept = [];
229
+ const joined = new Map();
230
+ const keepBeside = (name, commit) => kept.push({ name, commit });
231
+ for (const name of [...names].sort(compareGitText)) {
232
+ const [base, ours, theirs] = await Promise.all(SIDES.map(side => value(side, name)));
233
+ if (ours === theirs || ours === base)
234
+ continue;
235
+ if (theirs === base) {
236
+ finals.set(name, ours);
237
+ continue;
238
+ }
239
+ if (name === 'refs/stash') {
240
+ finals.set(name, gitReflogTop(await read(merged.get(STASH_LOG))));
241
+ continue;
242
+ }
243
+ if (theirs === null || ours === null) {
244
+ if (theirs === null)
245
+ finals.set(name, ours);
246
+ continue;
247
+ }
248
+ if (!isGitOid(sides.theirs.format, ours) || !isGitOid(sides.theirs.format, theirs))
249
+ continue;
250
+ if (await git.isAncestor(theirs, ours)) {
251
+ finals.set(name, ours);
252
+ continue;
253
+ }
254
+ if (await git.isAncestor(ours, theirs) || name.startsWith('refs/remotes/'))
255
+ continue;
256
+ const message = `Merge concurrent commits on ${name.slice('refs/heads/'.length)}`;
257
+ const join = name.startsWith('refs/heads/') ? await git.join(theirs, ours, message) : null;
258
+ if (join !== null) {
259
+ finals.set(name, join.commit);
260
+ joined.set(name, { ...join, theirs, ours, message });
261
+ }
262
+ else
263
+ keepBeside(name, ours);
264
+ }
265
+ // A ref ours made where theirs's need a folder, or the reverse, is kept
266
+ // beside: Git holds no two such names.
267
+ const theirsNames = [...(await packedOf('theirs')).keys(),
268
+ ...sides.theirs.files.filter(file => isLooseRef(file) && file.path !== 'HEAD').map(file => file.path)];
269
+ const standing = new RefNames(theirsNames.filter(name => !finals.has(name) || finals.get(name) !== null));
270
+ for (const [name, target] of [...finals]) {
271
+ if (target === null || standing.has(name) || name === 'refs/stash')
272
+ continue;
273
+ if (standing.clashes(name)) {
274
+ finals.delete(name);
275
+ keepBeside(name, target);
276
+ }
277
+ }
278
+ // The index follows the commit HEAD names. A merge commit's carries ours's
279
+ // staged work; one Git cannot carry is undone, and ours kept beside.
280
+ const final = (name) => finals.has(name) ? Promise.resolve(finals.get(name)) : value('theirs', name);
281
+ const commitOf = async (head, valueOf) => head?.startsWith('ref: ') ? valueOf(head.slice('ref: '.length)) : head;
282
+ const oursHead = await commitOf(await value('ours', 'HEAD'), name => value('ours', name));
283
+ const theirsHead = await commitOf(await value('theirs', 'HEAD'), name => value('theirs', name));
284
+ let headCommit = await commitOf(await final('HEAD'), final);
285
+ let index;
286
+ const carried = [...joined].find(([, join]) => join.commit === headCommit);
287
+ if (carried !== undefined) {
288
+ const [name, join] = carried;
289
+ const own = sides.ours.index;
290
+ const moved = own?.files.length === 1 && oursHead !== null
291
+ ? await git.carryIndex(await bytes(own.files[0].oid), oursHead, join.commit) : null;
292
+ if (moved !== null)
293
+ index = { meaning: moved.meaning, files: [make('index', moved.bytes)] };
294
+ else {
295
+ finals.set(name, join.theirs);
296
+ joined.delete(name);
297
+ keepBeside(name, join.ours);
298
+ headCommit = join.theirs;
299
+ }
300
+ }
301
+ if (index === undefined) {
302
+ const { base, ours, theirs } = { base: sides.base.index, ours: sides.ours.index, theirs: sides.theirs.index };
303
+ index = headCommit === oursHead && headCommit === theirsHead
304
+ ? theirs?.meaning === base?.meaning && ours?.meaning !== base?.meaning ? ours : theirs
305
+ : headCommit === oursHead ? ours : theirs;
306
+ }
307
+ // A merge commit's line follows theirs's, its first parent, as `git
308
+ // merge` writes it.
309
+ const endsOurs = (end, ours, theirs) => end === ours && end !== theirs;
310
+ for (const path of paths) {
311
+ if (!isReflogPath(path) || path === STASH_LOG)
312
+ continue;
313
+ const name = path.slice('logs/'.length);
314
+ await mergeLog(path, name === 'HEAD' ? endsOurs(headCommit, oursHead, theirsHead)
315
+ : !joined.has(name) && endsOurs(await final(name), await value('ours', name), await value('theirs', name)));
316
+ }
317
+ // Theirs's ref files, with each ref that ends elsewhere written loose or
318
+ // dropped, and ours kept beside.
319
+ for (const [path, file] of merged)
320
+ if (isRefFile(file))
321
+ merged.delete(path);
322
+ for (const file of sides.theirs.files)
323
+ if (isRefFile(file))
324
+ merged.set(file.path, file);
325
+ const dropped = new Set();
326
+ for (const [name, target] of finals) {
327
+ if (target === await value('theirs', name))
328
+ continue;
329
+ if (target !== null)
330
+ merged.set(name, make(name, `${target}\n`));
331
+ else {
332
+ if (merged.has(name) && isLooseRef(merged.get(name)))
333
+ merged.delete(name);
334
+ dropped.add(name);
335
+ }
336
+ }
337
+ const packedRefs = files.theirs.get('packed-refs');
338
+ if (dropped.size > 0 && packedRefs !== undefined) {
339
+ const text = await read(packedRefs), rest = gitDropPackedRefs(text, dropped);
340
+ if (rest !== text)
341
+ merged.set('packed-refs', make('packed-refs', rest));
342
+ }
343
+ for (const [name, target] of finals)
344
+ if (target !== null)
345
+ standing.add(name);
346
+ const beside = new Map();
347
+ for (const { name, commit } of kept) {
348
+ const names = besideNames(name, commit);
349
+ let side = names.at(-1);
350
+ for (const candidate of names) {
351
+ const holds = beside.get(candidate) ?? (standing.has(candidate) ? await final(candidate) : null);
352
+ if ((holds === null || holds === commit) && !standing.clashes(candidate)) {
353
+ side = candidate;
354
+ break;
355
+ }
356
+ }
357
+ standing.add(side);
358
+ beside.set(side, commit);
359
+ merged.set(side, make(side, `${commit}\n`));
360
+ const ours = files.ours.get(`logs/${name}`), theirs = files.theirs.get(`logs/${name}`);
361
+ if (ours !== undefined)
362
+ merged.set(`logs/${side}`, { ...ours, path: `logs/${side}` });
363
+ if (name === 'HEAD')
364
+ continue;
365
+ if (theirs !== undefined)
366
+ merged.set(`logs/${name}`, theirs);
367
+ else
368
+ merged.delete(`logs/${name}`);
369
+ }
370
+ const head = await final('HEAD');
371
+ for (const [name, join] of joined) {
372
+ const line = gitReflogLine(join.theirs, join.commit, GIT_MERGE_IDENTITY, join.time, `commit (merge): ${join.message}`);
373
+ for (const log of [`logs/${name}`, ...head === `ref: ${name}` ? ['logs/HEAD'] : []]) {
374
+ const file = merged.get(log);
375
+ if (file !== undefined && file.mode !== FOLDER)
376
+ merged.set(log, make(log, await read(file) + line));
377
+ }
378
+ }
379
+ return {
380
+ state: { format: sides.theirs.format, home: sides.theirs.home, files: asTree(merged, files.theirs), index },
381
+ made,
382
+ joins: [...joined.values()].map(join => join.commit),
383
+ };
384
+ }
385
+ //# sourceMappingURL=merge.js.map