@scolladon/tsgit 1.0.0 → 1.2.0

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.
Files changed (144) hide show
  1. package/README.md +29 -137
  2. package/dist/cjs/adapters/browser/index.cjs +1 -1
  3. package/dist/cjs/adapters/browser/index.cjs.map +1 -1
  4. package/dist/cjs/adapters/memory/index.cjs +1 -1
  5. package/dist/cjs/adapters/memory/index.cjs.map +1 -1
  6. package/dist/cjs/adapters/node/index.cjs +1 -1
  7. package/dist/cjs/adapters/node/index.cjs.map +1 -1
  8. package/dist/cjs/chunks/browser-http-transport-D4NH-8oJ.cjs +2 -0
  9. package/dist/cjs/chunks/browser-http-transport-D4NH-8oJ.cjs.map +1 -0
  10. package/dist/cjs/chunks/context-BcoAzPuU.cjs.map +1 -1
  11. package/dist/cjs/chunks/error-CDLOBdNU.cjs +2 -0
  12. package/dist/cjs/chunks/error-CDLOBdNU.cjs.map +1 -0
  13. package/dist/cjs/chunks/index-BVArBKuk.cjs +2 -0
  14. package/dist/cjs/chunks/index-BVArBKuk.cjs.map +1 -0
  15. package/dist/cjs/chunks/index-DE9HSiWf.cjs +2 -0
  16. package/dist/cjs/chunks/index-DE9HSiWf.cjs.map +1 -0
  17. package/dist/cjs/chunks/{logger-Cz9r6yt5.cjs → logger-CVG0zcPH.cjs} +2 -2
  18. package/dist/cjs/chunks/{logger-Cz9r6yt5.cjs.map → logger-CVG0zcPH.cjs.map} +1 -1
  19. package/dist/cjs/chunks/memory-http-transport-DXwDGKtR.cjs +2 -0
  20. package/dist/cjs/chunks/memory-http-transport-DXwDGKtR.cjs.map +1 -0
  21. package/dist/cjs/chunks/node-http-transport-vrbnNEDb.cjs +2 -0
  22. package/dist/cjs/chunks/node-http-transport-vrbnNEDb.cjs.map +1 -0
  23. package/dist/cjs/chunks/{progress-CK7CT9vU.cjs → progress-Bj2w-90A.cjs} +2 -2
  24. package/dist/cjs/chunks/{progress-CK7CT9vU.cjs.map → progress-Bj2w-90A.cjs.map} +1 -1
  25. package/dist/cjs/chunks/readable-stream-MTA6D0Zh.cjs +2 -0
  26. package/dist/cjs/chunks/readable-stream-MTA6D0Zh.cjs.map +1 -0
  27. package/dist/cjs/chunks/repository-BJwTSiGf.cjs +2 -0
  28. package/dist/cjs/chunks/repository-BJwTSiGf.cjs.map +1 -0
  29. package/dist/cjs/commands/index.cjs +1 -1
  30. package/dist/cjs/index.browser.cjs +1 -1
  31. package/dist/cjs/index.browser.cjs.map +1 -1
  32. package/dist/cjs/index.cjs +1 -1
  33. package/dist/cjs/index.default.cjs +1 -1
  34. package/dist/cjs/index.default.cjs.map +1 -1
  35. package/dist/cjs/index.node.cjs +1 -1
  36. package/dist/cjs/index.node.cjs.map +1 -1
  37. package/dist/cjs/operators/index.cjs +1 -1
  38. package/dist/cjs/operators/index.cjs.map +1 -1
  39. package/dist/cjs/primitives/index.cjs +1 -1
  40. package/dist/cjs/primitives/index.cjs.map +1 -1
  41. package/dist/cjs/transport/index.cjs.map +1 -1
  42. package/dist/esm/adapters/browser/index.js +1 -1
  43. package/dist/esm/adapters/browser/index.js.map +1 -1
  44. package/dist/esm/adapters/memory/index.js +1 -1
  45. package/dist/esm/adapters/memory/index.js.map +1 -1
  46. package/dist/esm/adapters/node/index.js +1 -1
  47. package/dist/esm/adapters/node/index.js.map +1 -1
  48. package/dist/esm/chunks/browser-http-transport-DL8bkNdg.js +2 -0
  49. package/dist/esm/chunks/browser-http-transport-DL8bkNdg.js.map +1 -0
  50. package/dist/esm/chunks/context-CumKOV7K.js.map +1 -1
  51. package/dist/esm/chunks/error-MOmSzp9Z.js +2 -0
  52. package/dist/esm/chunks/error-MOmSzp9Z.js.map +1 -0
  53. package/dist/esm/chunks/index-BjmofbKE.js +2 -0
  54. package/dist/esm/chunks/index-BjmofbKE.js.map +1 -0
  55. package/dist/esm/chunks/index-Jh5R-UmU.js +2 -0
  56. package/dist/esm/chunks/index-Jh5R-UmU.js.map +1 -0
  57. package/dist/esm/chunks/{logger-84ixEPbQ.js → logger-DP2cCpvL.js} +2 -2
  58. package/dist/esm/chunks/{logger-84ixEPbQ.js.map → logger-DP2cCpvL.js.map} +1 -1
  59. package/dist/esm/chunks/memory-http-transport-COJU6VOL.js +2 -0
  60. package/dist/esm/chunks/memory-http-transport-COJU6VOL.js.map +1 -0
  61. package/dist/esm/chunks/node-http-transport-DYCFIJbc.js +2 -0
  62. package/dist/esm/chunks/node-http-transport-DYCFIJbc.js.map +1 -0
  63. package/dist/esm/chunks/{progress-OTDhgmPO.js → progress-NePjO3Kd.js} +2 -2
  64. package/dist/esm/chunks/{progress-OTDhgmPO.js.map → progress-NePjO3Kd.js.map} +1 -1
  65. package/dist/esm/chunks/readable-stream-CGuf8k1J.js +2 -0
  66. package/dist/esm/chunks/readable-stream-CGuf8k1J.js.map +1 -0
  67. package/dist/esm/chunks/repository-Cs-dbHqO.js +2 -0
  68. package/dist/esm/chunks/repository-Cs-dbHqO.js.map +1 -0
  69. package/dist/esm/commands/index.js +1 -1
  70. package/dist/esm/index.browser.js +1 -1
  71. package/dist/esm/index.browser.js.map +1 -1
  72. package/dist/esm/index.default.js +1 -1
  73. package/dist/esm/index.default.js.map +1 -1
  74. package/dist/esm/index.js +1 -1
  75. package/dist/esm/index.node.js +1 -1
  76. package/dist/esm/index.node.js.map +1 -1
  77. package/dist/esm/operators/index.js +1 -1
  78. package/dist/esm/operators/index.js.map +1 -1
  79. package/dist/esm/primitives/index.js +1 -1
  80. package/dist/esm/primitives/index.js.map +1 -1
  81. package/dist/esm/transport/index.js.map +1 -1
  82. package/dist/types/adapters/browser/index.d.cts +2 -1
  83. package/dist/types/adapters/browser/index.d.ts +2 -1
  84. package/dist/types/adapters/memory/index.d.cts +23 -2
  85. package/dist/types/adapters/memory/index.d.ts +23 -2
  86. package/dist/types/adapters/node/index.d.cts +178 -3
  87. package/dist/types/adapters/node/index.d.ts +178 -3
  88. package/dist/types/chunks/{context-CTaXSPiP.d.ts → context-BORy7yXb.d.ts} +133 -20
  89. package/dist/types/chunks/{context-d36639-i.d.cts → context-CMrHCVwK.d.cts} +133 -20
  90. package/dist/types/chunks/reflog-entry-BIf-zt__.d.cts +331 -0
  91. package/dist/types/chunks/reflog-entry-D9P2hCgv.d.ts +331 -0
  92. package/dist/types/chunks/{repository-CFT9j9H6.d.cts → repository-BqibvTOu.d.cts} +23 -4
  93. package/dist/types/chunks/{repository-ntg7eXb2.d.ts → repository-C4rN09Am.d.ts} +23 -4
  94. package/dist/types/chunks/write-tree-B8G6kgTy.d.cts +154 -0
  95. package/dist/types/chunks/write-tree-BXCDChvv.d.ts +154 -0
  96. package/dist/types/commands/index.d.cts +229 -37
  97. package/dist/types/commands/index.d.ts +229 -37
  98. package/dist/types/index.browser.d.cts +4 -4
  99. package/dist/types/index.browser.d.ts +4 -4
  100. package/dist/types/index.d.cts +4 -4
  101. package/dist/types/index.d.ts +4 -4
  102. package/dist/types/index.default.d.cts +4 -4
  103. package/dist/types/index.default.d.ts +4 -4
  104. package/dist/types/index.node.d.cts +4 -4
  105. package/dist/types/index.node.d.ts +4 -4
  106. package/dist/types/operators/index.d.cts +17 -1
  107. package/dist/types/operators/index.d.ts +17 -1
  108. package/dist/types/primitives/index.d.cts +393 -171
  109. package/dist/types/primitives/index.d.ts +393 -171
  110. package/package.json +76 -10
  111. package/dist/cjs/chunks/browser-http-transport-BBF8uw-f.cjs +0 -2
  112. package/dist/cjs/chunks/browser-http-transport-BBF8uw-f.cjs.map +0 -1
  113. package/dist/cjs/chunks/error-DL4SHCBJ.cjs +0 -2
  114. package/dist/cjs/chunks/error-DL4SHCBJ.cjs.map +0 -1
  115. package/dist/cjs/chunks/error-DN8Vnwr4.cjs +0 -2
  116. package/dist/cjs/chunks/error-DN8Vnwr4.cjs.map +0 -1
  117. package/dist/cjs/chunks/index-iUd-bwwm.cjs +0 -2
  118. package/dist/cjs/chunks/index-iUd-bwwm.cjs.map +0 -1
  119. package/dist/cjs/chunks/memory-http-transport-DGll7Af4.cjs +0 -2
  120. package/dist/cjs/chunks/memory-http-transport-DGll7Af4.cjs.map +0 -1
  121. package/dist/cjs/chunks/merge-base-DlGWnkxP.cjs +0 -2
  122. package/dist/cjs/chunks/merge-base-DlGWnkxP.cjs.map +0 -1
  123. package/dist/cjs/chunks/node-http-transport-CuOgJlws.cjs +0 -2
  124. package/dist/cjs/chunks/node-http-transport-CuOgJlws.cjs.map +0 -1
  125. package/dist/cjs/chunks/repository-Cfo6Bj8T.cjs +0 -2
  126. package/dist/cjs/chunks/repository-Cfo6Bj8T.cjs.map +0 -1
  127. package/dist/esm/chunks/browser-http-transport-mZQKkInJ.js +0 -2
  128. package/dist/esm/chunks/browser-http-transport-mZQKkInJ.js.map +0 -1
  129. package/dist/esm/chunks/error-CnIcr6IG.js +0 -2
  130. package/dist/esm/chunks/error-CnIcr6IG.js.map +0 -1
  131. package/dist/esm/chunks/error-DTEP18A3.js +0 -2
  132. package/dist/esm/chunks/error-DTEP18A3.js.map +0 -1
  133. package/dist/esm/chunks/index-CJc-SKMj.js +0 -2
  134. package/dist/esm/chunks/index-CJc-SKMj.js.map +0 -1
  135. package/dist/esm/chunks/memory-http-transport-BmHjaEWj.js +0 -2
  136. package/dist/esm/chunks/memory-http-transport-BmHjaEWj.js.map +0 -1
  137. package/dist/esm/chunks/merge-base-DmuOYxfP.js +0 -2
  138. package/dist/esm/chunks/merge-base-DmuOYxfP.js.map +0 -1
  139. package/dist/esm/chunks/node-http-transport-Bz3noIS3.js +0 -2
  140. package/dist/esm/chunks/node-http-transport-Bz3noIS3.js.map +0 -1
  141. package/dist/esm/chunks/repository-qcX3-LkP.js +0 -2
  142. package/dist/esm/chunks/repository-qcX3-LkP.js.map +0 -1
  143. package/dist/types/chunks/diff-change-B09vxnxy.d.cts +0 -59
  144. package/dist/types/chunks/diff-change-D7xSeCn9.d.ts +0 -59
@@ -1,4 +1,4 @@
1
- import { b as Context, C as Compressor, I as InflateStreamResult, f as FileSystem, e as FileStat, D as DirEntry, F as FileHandle, H as HashService, g as Hasher } from '../../chunks/context-CTaXSPiP.js';
1
+ import { b as Context, C as Compressor, I as InflateStreamResult, f as FileSystem, e as FileStat, D as DirEntry, F as FileHandle, H as HashService, g as Hasher, k as HookRunner, i as HookRequest, j as HookResult } from '../../chunks/context-BORy7yXb.js';
2
2
  import { b as HttpTransport, H as HttpRequest, a as HttpResponse } from '../../chunks/http-transport-DirKfK2S.js';
3
3
 
4
4
  interface NodeAdapterOptions {
@@ -9,6 +9,12 @@ interface NodeAdapterOptions {
9
9
  readonly signal?: AbortSignal;
10
10
  readonly deltaCacheMaxBytes?: number;
11
11
  readonly deltaCacheMaxEntries?: number;
12
+ /**
13
+ * Wire the git-hook runner (default true). Pass `false` to disable hooks.
14
+ * A wired runner spawns `.git/hooks/*` scripts that inherit the full
15
+ * `process.env` — disable it for repositories you do not trust.
16
+ */
17
+ readonly hooks?: boolean;
12
18
  }
13
19
  declare function createNodeContext(options: NodeAdapterOptions): Context;
14
20
 
@@ -25,15 +31,128 @@ declare class NodeCompressor implements Compressor {
25
31
  createInflateStream: () => TransformStream<Uint8Array, Uint8Array>;
26
32
  }
27
33
 
34
+ /**
35
+ * Injectable surface for the Node `fs/promises` calls that `NodeFileSystem`
36
+ * needs. Production code uses `realFsOps`; tests inject a partial fake.
37
+ *
38
+ * Why this exists:
39
+ * - `vi.mock('node:fs/promises')` is file-scoped and patches the module
40
+ * system. It dumps every test that "needs to mock fs" into one bucket
41
+ * (the previous `node-file-system-containment.test.ts` smell).
42
+ * - Dependency injection at the adapter constructor makes the dependency
43
+ * explicit, scoped per-instance, and cross-platform by construction —
44
+ * tests don't depend on Vitest's mock machinery to swap the fs surface.
45
+ * - The interface is a `Pick` of `fsPromises` so production code can just
46
+ * pass the real module without writing any glue. Tests pass a fake
47
+ * object that satisfies the subset they exercise.
48
+ *
49
+ * @internal — not re-exported from `src/adapters/node/index.ts`.
50
+ */
51
+ type FsOperations = Pick<typeof fsPromises, 'appendFile' | 'chmod' | 'lstat' | 'mkdir' | 'open' | 'readdir' | 'readFile' | 'readlink' | 'realpath' | 'rename' | 'rm' | 'rmdir' | 'stat' | 'symlink' | 'writeFile'>;
52
+
53
+ /**
54
+ * Path-policy abstraction.
55
+ *
56
+ * Encapsulates every platform-aware path operation the Node adapter needs.
57
+ * Production code uses `nativePolicy` (host-matching). Tests inject
58
+ * `windowsPolicy` or `posixPolicy` to simulate either platform on any host,
59
+ * eliminating the "host vs. simulated platform" confusion that was
60
+ * leaking into containment / cache / errno code paths.
61
+ *
62
+ * Design notes:
63
+ * - `sep` is the platform separator string, used for prefix containment.
64
+ * - `caseInsensitive` drives `normalizeForCompare`; Windows + macOS HFS+
65
+ * could share this in theory, but tsgit treats macOS as case-sensitive
66
+ * per Git's `core.ignorecase` default and POSIX convention.
67
+ * - `rootOf` returns the volume/drive prefix produced by `path.parse`.
68
+ * Examples: `/` on POSIX, `'C:\\'` on Windows, `'\\\\server\\share\\'`
69
+ * for UNC paths.
70
+ * - The interface is *only* the subset NodeFileSystem actually needs; we
71
+ * intentionally do not expose all of `nodePath`'s surface so callers
72
+ * can't smuggle host-bound calls back in.
73
+ */
74
+ interface PathPolicy {
75
+ readonly sep: '\\' | '/';
76
+ readonly caseInsensitive: boolean;
77
+ isAbsolute(path: string): boolean;
78
+ resolve(...parts: string[]): string;
79
+ join(...parts: string[]): string;
80
+ dirname(path: string): string;
81
+ basename(path: string): string;
82
+ /**
83
+ * Returns the volume/drive prefix produced by `path.parse(p).root`.
84
+ * POSIX: `/` for absolute, `''` for relative.
85
+ * Windows: `'C:\\'`, `'\\\\server\\share\\'`, or `''` for relative.
86
+ */
87
+ rootOf(path: string): string;
88
+ /** Case-fold on case-insensitive platforms; identity otherwise. */
89
+ normalizeForCompare(path: string): string;
90
+ }
91
+
28
92
  declare class NodeFileSystem implements FileSystem {
29
93
  private readonly rootDir;
30
- constructor(rootDir: string);
94
+ private readonly pathPolicy;
95
+ private readonly fsOps;
96
+ /**
97
+ * Memoised realpath of an *existing* parent directory, keyed by the raw
98
+ * (pre-realpath) parent path. Populated on every cache-miss inside
99
+ * `resolveForCreation` so a clone/checkout that writes N files into the
100
+ * same tree pays the realpath walk-up once per parent rather than once
101
+ * per file.
102
+ *
103
+ * Invariants:
104
+ * - Only EXISTING parents are cached. ENOENT walks fall back to
105
+ * `realpathNearestExisting` and are never recorded.
106
+ * - `rmRecursive` and `rename` clear the cache, which is correct (the
107
+ * parent realpath may have changed) and cheap (the cache holds at
108
+ * most a handful of small strings).
109
+ */
110
+ private readonly creationParentCache;
111
+ /**
112
+ * Lazy long-name canonicalisation of `rootDir` for containment checks.
113
+ * Promise so concurrent first calls share one `realpath`; cleared on
114
+ * rejection so a transient ENOENT can be retried.
115
+ */
116
+ private canonicalRootPromise;
117
+ /**
118
+ * Memoised result of `pathPolicy.normalizeForCompare(rootDir)`. The
119
+ * rootDir is `readonly` for the adapter's lifetime, so a single
120
+ * normalisation is amortised across every containment check.
121
+ */
122
+ private normalizedRootDir;
123
+ /**
124
+ * Memoised result of `pathPolicy.normalizeForCompare(canonicalRoot)`.
125
+ * Tracked alongside the canonical-root promise: when the promise
126
+ * resolves we cache the normalised form once. The promise's
127
+ * rejection-clears-cache rule means a transient ENOENT also clears
128
+ * this field (so the next call re-normalises against the retried
129
+ * canonical root).
130
+ */
131
+ private normalizedCanonicalRoot;
132
+ constructor(rootDir: string, pathPolicy?: PathPolicy, fsOps?: FsOperations);
133
+ private getNormalizedRootDir;
134
+ /**
135
+ * Returns the cached normalised canonical root. Caller must have
136
+ * `await this.getCanonicalRoot()` immediately prior — the cache is
137
+ * populated by the promise's success arm and cleared on rejection, so
138
+ * a successful `await` guarantees the field is set.
139
+ *
140
+ * Kept synchronous (vs `async` + `await this.getCanonicalRoot()`
141
+ * inside) so the hot path doesn't pay an extra microtask suspension
142
+ * per containment check on a settled promise. The `!` is the only
143
+ * machine-readable form of "trust the post-await invariant"; the
144
+ * private `await getCanonicalRoot()` discipline at every call site is
145
+ * what makes it safe.
146
+ */
147
+ private getResolvedNormalizedCanonicalRoot;
148
+ private getCanonicalRoot;
31
149
  read: (path: string) => Promise<Uint8Array>;
32
150
  readSlice: (path: string, offset: number, length: number) => Promise<Uint8Array>;
33
151
  readUtf8: (path: string) => Promise<string>;
34
152
  write: (path: string, data: Uint8Array) => Promise<void>;
35
153
  writeExclusive: (path: string, data: Uint8Array) => Promise<void>;
36
154
  writeUtf8: (path: string, content: string) => Promise<void>;
155
+ appendUtf8: (path: string, content: string) => Promise<void>;
37
156
  exists: (path: string) => Promise<boolean>;
38
157
  stat: (path: string) => Promise<FileStat>;
39
158
  lstat: (path: string) => Promise<FileStat>;
@@ -46,8 +165,10 @@ declare class NodeFileSystem implements FileSystem {
46
165
  chmod: (path: string, mode: number) => Promise<void>;
47
166
  rmRecursive: (path: string) => Promise<void>;
48
167
  openWithNoFollow: (path: string, mode: "read" | "write") => Promise<FileHandle>;
168
+ private isSymlinkLeaf;
49
169
  private removeTree;
50
170
  private resolveForCreation;
171
+ private realpathForCreation;
51
172
  private resolveForMode;
52
173
  private checkContainment;
53
174
  }
@@ -62,6 +183,60 @@ declare class NodeHashService implements HashService {
62
183
  createHasher: () => Hasher;
63
184
  }
64
185
 
186
+ /** Minimal `stat` result `NodeHookRunner` consumes. */
187
+ interface HookStat {
188
+ readonly mode: number;
189
+ isFile(): boolean;
190
+ }
191
+ /** Minimal readable-stream surface — a hook's stdout / stderr. */
192
+ interface HookReadable {
193
+ on(event: 'data', listener: (chunk: Buffer) => void): void;
194
+ }
195
+ /** Minimal writable surface — a hook's stdin. */
196
+ interface HookWritable {
197
+ on(event: 'error', listener: () => void): void;
198
+ end(data: string): void;
199
+ }
200
+ /** Minimal child-process surface `NodeHookRunner` consumes. */
201
+ interface HookChild {
202
+ readonly stdout: HookReadable;
203
+ readonly stderr: HookReadable;
204
+ readonly stdin: HookWritable;
205
+ on(event: 'error', listener: (err: Error) => void): void;
206
+ on(event: 'close', listener: (code: number | null) => void): void;
207
+ kill(): void;
208
+ }
209
+ /**
210
+ * Injectable process / filesystem surface. Production uses the Node builtins;
211
+ * unit tests inject a fake so every branch is exercised deterministically
212
+ * without spawning a real process (mirrors `FsOperations` — ADR-047).
213
+ */
214
+ interface HookRunnerOps {
215
+ readonly stat: (path: string) => Promise<HookStat>;
216
+ readonly spawn: (command: string, args: ReadonlyArray<string>, options: {
217
+ readonly cwd: string;
218
+ readonly env: NodeJS.ProcessEnv;
219
+ }) => HookChild;
220
+ }
221
+ /**
222
+ * Node `HookRunner`: resolves `${hooksDir}/${name}` and, when it exists and is
223
+ * executable, spawns it via `node:child_process`. The hook inherits the
224
+ * process environment plus `GIT_DIR` / `GIT_INDEX_FILE`, runs with `cwd` at the
225
+ * working tree, and its output is captured (bounded). Never rejects for a
226
+ * non-zero exit.
227
+ */
228
+ declare class NodeHookRunner implements HookRunner {
229
+ private readonly isWindows;
230
+ private readonly ops;
231
+ constructor(platform?: NodeJS.Platform, ops?: HookRunnerOps);
232
+ run(request: HookRequest): Promise<HookResult>;
233
+ /**
234
+ * A hook is runnable when it is a regular file that is executable. Windows
235
+ * has no executable bit, so any regular file qualifies (see ADR-068).
236
+ */
237
+ private isRunnable;
238
+ }
239
+
65
240
  interface NodeHttpTransportOptions {
66
241
  readonly allowInsecureHttp?: boolean;
67
242
  }
@@ -71,5 +246,5 @@ declare class NodeHttpTransport implements HttpTransport {
71
246
  request: (req: HttpRequest) => Promise<HttpResponse>;
72
247
  }
73
248
 
74
- export { NodeCompressor, NodeFileSystem, NodeHashService, NodeHttpTransport, createNodeContext };
249
+ export { NodeCompressor, NodeFileSystem, NodeHashService, NodeHookRunner, NodeHttpTransport, createNodeContext };
75
250
  export type { NodeAdapterOptions, NodeHttpTransportOptions };
@@ -83,20 +83,25 @@ interface FileSystem {
83
83
  /**
84
84
  * Write bytes to file. Fails with FILE_EXISTS if the file already exists (exclusive create).
85
85
  *
86
- * Contract obligations (Phase 7 §14.17):
86
+ * Contract obligations:
87
87
  * - **Parent-directory creation:** the adapter MUST ensure parent directories exist before the
88
- * exclusive write. Equivalent to `mkdir -p dirname(path)` before `open(path, O_EXCL)`. If the
89
- * parent is removed between the implicit mkdir and the open (e.g. concurrent `git gc` prunes
90
- * the fanout), the adapter retries once: re-create the parent, re-attempt the open. On a second
91
- * ENOENT the error propagates as FILE_NOT_FOUND.
88
+ * exclusive write. Equivalent to `mkdir -p dirname(path)` before `open(path, O_EXCL)`. If the
89
+ * parent is removed between the implicit mkdir and the open (e.g. concurrent `git gc` prunes
90
+ * the fanout), the adapter retries once: re-create the parent, re-attempt the open. On a second
91
+ * ENOENT the error propagates as FILE_NOT_FOUND.
92
92
  * - **Symlink-safe ancestor check:** the adapter MUST reject writes where any ancestor directory
93
- * of `path` is a symbolic link whose resolved target is outside the containment root. This
94
- * closes the attack where an attacker replaces `objects/xx/` with a symlink pointing elsewhere.
95
- * Implementation: lstat-walk the ancestor chain, or use `openat`-style relative opens.
93
+ * of `path` is a symbolic link whose resolved target is outside the containment root. This
94
+ * closes the attack where an attacker replaces `objects/xx/` with a symlink pointing elsewhere.
95
+ * Implementation: lstat-walk the ancestor chain, or use `openat`-style relative opens.
96
96
  */
97
97
  readonly writeExclusive: (path: string, data: Uint8Array) => Promise<void>;
98
98
  /** Write UTF-8 string to file, creating parent directories as needed. */
99
99
  readonly writeUtf8: (path: string, content: string) => Promise<void>;
100
+ /**
101
+ * Append UTF-8 to a file, creating parent directories and the file as
102
+ * needed. Atomic per-call for line-sized writes (relies on `O_APPEND`).
103
+ */
104
+ readonly appendUtf8: (path: string, content: string) => Promise<void>;
100
105
  /** Check if path exists. */
101
106
  readonly exists: (path: string) => Promise<boolean>;
102
107
  /** Get file/directory metadata. Throws FILE_NOT_FOUND if not found. Follows symlinks. */
@@ -140,7 +145,7 @@ interface FileSystem {
140
145
  * - Node: `fs.open(path, O_NOFOLLOW | (mode === 'write' ? O_WRONLY : O_RDONLY))`.
141
146
  * - Memory: rejects with `PERMISSION_DENIED` when the leaf is a memory symlink entry.
142
147
  * - Browser OPFS: throws `UNSUPPORTED_OPERATION` (OPFS has no symlinks; callers can
143
- * fall back to a plain `read`/`write` because the no-follow guarantee holds vacuously).
148
+ * fall back to a plain `read`/`write` because the no-follow guarantee holds vacuously).
144
149
  *
145
150
  * Throws `FILE_NOT_FOUND` if the leaf does not exist (in `read` mode).
146
151
  * Throws `PERMISSION_DENIED` if the leaf is a symlink.
@@ -206,13 +211,73 @@ interface LruCache<V> {
206
211
  readonly entryCount: number;
207
212
  }
208
213
 
214
+ /**
215
+ * The lifecycle hooks tsgit invokes. Extend the union to add a hook.
216
+ *
217
+ * Kept in the domain layer because both the `HookRunner` port and the
218
+ * `HOOK_FAILED` command error reference it — a port may import domain, but
219
+ * domain may never import a port.
220
+ */
221
+ type HookName = 'pre-commit' | 'commit-msg' | 'pre-push';
222
+
223
+ /**
224
+ * A single hook invocation. The port is stateless — every fact the adapter
225
+ * needs to resolve and spawn the hook travels in this request.
226
+ */
227
+ interface HookRequest {
228
+ /** Hook to run. */
229
+ readonly name: HookName;
230
+ /** Absolute directory holding hook scripts — `core.hooksPath` or `${gitDir}/hooks`. */
231
+ readonly hooksDir: string;
232
+ /** Working directory for the spawned process — the working-tree root. */
233
+ readonly workDir: string;
234
+ /** Absolute `.git` directory — exported to the hook environment as `GIT_DIR`. */
235
+ readonly gitDir: string;
236
+ /** Positional arguments (e.g. the `COMMIT_EDITMSG` path for `commit-msg`). */
237
+ readonly args: ReadonlyArray<string>;
238
+ /** Bytes piped to the hook's stdin. Empty string ⇒ stdin closed empty. */
239
+ readonly stdin: string;
240
+ /** Cancels a running hook — the adapter kills the child when it aborts. */
241
+ readonly signal?: AbortSignal;
242
+ }
243
+ /**
244
+ * Outcome of a hook invocation.
245
+ *
246
+ * `skipped` — the hook file is absent or not executable; nothing ran. Git
247
+ * treats both as "no hook, proceed".
248
+ *
249
+ * `ran` — the hook ran to completion; `exitCode` is authoritative (a non-zero
250
+ * value is the caller's signal to abort).
251
+ */
252
+ type HookResult = {
253
+ readonly kind: 'skipped';
254
+ } | {
255
+ readonly kind: 'ran';
256
+ readonly exitCode: number;
257
+ readonly stdout: string;
258
+ readonly stderr: string;
259
+ };
260
+ /**
261
+ * Runs git lifecycle hooks. Optional on `Context`: when absent, hooks are
262
+ * inert (the browser has no runner; a host may opt out).
263
+ */
264
+ interface HookRunner {
265
+ /**
266
+ * Resolve `${hooksDir}/${name}`; when it exists and is executable, spawn it
267
+ * with `args`, `stdin`, `cwd = workDir`, and `GIT_DIR` in the environment.
268
+ * Resolves with the exit code and captured output. NEVER rejects for a
269
+ * non-zero exit — interpreting the exit code is the caller's policy.
270
+ */
271
+ readonly run: (request: HookRequest) => Promise<HookResult>;
272
+ }
273
+
209
274
  /**
210
275
  * General-purpose level-based logger consumed by the facade and any cross-cutting
211
276
  * concern (dispose, validation, lifecycle). Independent from the transport-tier
212
277
  * `Logger` in `transport/types.ts`, which is event-based and HTTP-shaped.
213
278
  *
214
279
  * The facade wraps user-supplied loggers with sanitization at construction time
215
- * (see Phase 10 design §8.5). Implementations should be tolerant of high call
280
+ * . Implementations should be tolerant of high call
216
281
  * frequency and MUST NOT throw — a throwing logger crashes nothing.
217
282
  */
218
283
  interface Logger {
@@ -225,8 +290,8 @@ interface Logger {
225
290
  declare const noopLogger: Logger;
226
291
  /**
227
292
  * Wrap a user-supplied logger so every `message` + every string value in the
228
- * `context` object passes through `sanitize()` (Phase 9 §4.7) before reaching
229
- * the sink. Per design §8.5 the facade applies this at construction time so no
293
+ * `context` object passes through `sanitize()` before reaching
294
+ * the sink. the facade applies this at construction time so no
230
295
  * downstream caller ever feeds raw control bytes to a user-controlled sink.
231
296
  *
232
297
  * Methods that the user did not supply are absent on the wrapper (preserves
@@ -237,7 +302,7 @@ declare const wrapLoggerSanitizer: (logger: Logger) => Logger;
237
302
 
238
303
  /**
239
304
  * Progress reporter shape consumed by long-running commands. The facade
240
- * (Phase 10) accepts a user-supplied implementation via
305
+ * accepts a user-supplied implementation via
241
306
  * `OpenRepositoryOptions.progress` and plumbs it onto `Context.progress`.
242
307
  *
243
308
  * Reporters are synchronous and fire-and-forget. The facade wraps every call
@@ -260,21 +325,51 @@ interface ProgressReporter {
260
325
  readonly end: (op: string) => void;
261
326
  }
262
327
 
328
+ /** Outcome of a promisor-remote lazy fetch (ADR-081). */
329
+ interface PromisorFetchOutcome {
330
+ /**
331
+ * False when the repository has no promisor remote configured — the caller
332
+ * (`readObject`) then falls through to its normal `OBJECT_NOT_FOUND`.
333
+ */
334
+ readonly attempted: boolean;
335
+ /** Objects the caller asked for. */
336
+ readonly requested: number;
337
+ /** Objects that were missing locally and were fetched from the promisor. */
338
+ readonly fetched: number;
339
+ }
263
340
  /**
264
- * Repository physical layout where the working tree and .git directory live.
265
- * Renamed in Phase 10 from the previous `RepositoryConfig` (port-tier) to free that
341
+ * Capability for fetching objects that a partial clone omitted, from the
342
+ * configured promisor remote. Wired onto `Context` by `openRepository` and
343
+ * consumed by `readObject` on a miss — the dependency-inverting seam that lets
344
+ * a primitive trigger a command-tier fetch without importing upward.
345
+ */
346
+ interface PromisorRemote {
347
+ fetch(oids: ReadonlyArray<ObjectId>): Promise<PromisorFetchOutcome>;
348
+ }
349
+
350
+ /**
351
+ * Repository physical layout — where the working tree and.git directory live.
352
+ * Renamed in from the previous `RepositoryConfig` (port-tier) to free that
266
353
  * name for the facade-tier `RepositoryConfig` shape (auth/parallelism/etc.).
267
354
  */
268
355
  interface RepositoryLayout {
269
356
  /** Absolute path to the repository root (working tree). */
270
357
  readonly workDir: string;
271
- /** Absolute path to the .git directory (usually `${workDir}/.git`, but may differ for bare repos or worktrees). */
358
+ /** Absolute path to the.git directory (usually `${workDir}/.git`, but may differ for bare repos or worktrees). */
272
359
  readonly gitDir: string;
273
360
  /** Whether this is a bare repository. */
274
361
  readonly bare: boolean;
362
+ /**
363
+ * Home directory for `~`-expansion in config-driven paths (e.g.
364
+ * `core.excludesFile = ~/.config/git/ignore`). Populated by the node
365
+ * shim from `os.homedir()`; memory adapter accepts an option; browser
366
+ * leaves it `undefined`. When `undefined`, loaders that need home
367
+ * expansion treat the source as missing.
368
+ */
369
+ readonly homeDir?: string;
275
370
  }
276
371
  /**
277
- * Author / committer identity — Phase 9 §4.7 shape.
372
+ * Author / committer identity shape.
278
373
  */
279
374
  interface AuthorIdentity {
280
375
  readonly name: string;
@@ -292,7 +387,7 @@ type AuthStrategy = {
292
387
  readonly password: string;
293
388
  };
294
389
  /**
295
- * Facade-tier configuration. Phase 10 introduces this shape; it carries the
390
+ * Facade-tier configuration. introduces this shape; it carries the
296
391
  * auth/parallelism/SSRF/network options the facade plumbs into network-pipeline.
297
392
  * All fields are optional — primitives and commands consult only the keys they need.
298
393
  */
@@ -304,7 +399,17 @@ interface RepositoryConfig {
304
399
  readonly upstreamRef?: RefName;
305
400
  readonly allowInsecure?: boolean;
306
401
  readonly allowPrivateNetworks?: boolean;
402
+ /**
403
+ * Hard cap (bytes) on a single pack body buffered in memory by `fetchPack`.
404
+ * Server-controlled byte counts above this raise `PACK_TOO_LARGE`. Default
405
+ * 512 MiB. Lower it for hardened deployments that clone only small repos.
406
+ */
307
407
  readonly maxResponseBytes?: number;
408
+ /**
409
+ * Hard cap on the entry-count field declared in a received pack header.
410
+ * Server-controlled `uint32` values above this raise `PACK_TOO_LARGE` before
411
+ * `fetchPack` allocates per-entry state. Default 50_000_000.
412
+ */
308
413
  readonly maxObjectsPerPack?: number;
309
414
  readonly detectRenames?: boolean;
310
415
  readonly breakStaleLockMs?: number;
@@ -332,6 +437,13 @@ interface Context {
332
437
  readonly logger?: Logger;
333
438
  /** Optional abort signal for cancelling long-running operations. */
334
439
  readonly signal?: AbortSignal;
440
+ /** Optional hook runner. Absent ⇒ hooks are inert (browser, or opted out). */
441
+ readonly hooks?: HookRunner;
442
+ /**
443
+ * Optional promisor-remote capability. Populated by `openRepository`;
444
+ * `readObject` consults it to lazy-fetch an object a partial clone omitted.
445
+ */
446
+ readonly promisor?: PromisorRemote;
335
447
  }
336
448
  interface CreateContextParts {
337
449
  readonly fs: FileSystem;
@@ -346,9 +458,10 @@ interface CreateContextParts {
346
458
  readonly config?: RepositoryConfig;
347
459
  readonly logger?: Logger;
348
460
  readonly signal?: AbortSignal;
461
+ readonly hooks?: HookRunner;
349
462
  }
350
463
  /** Assemble a frozen Context from its constituent ports + layout. */
351
464
  declare function createContext(parts: CreateContextParts): Context;
352
465
 
353
- export { ObjectId as O, RefName as R, FilePath as d, createContext as j, noopLogger as n, wrapLoggerSanitizer as w };
354
- export type { AuthStrategy as A, Compressor as C, DirEntry as D, FileHandle as F, HashService as H, InflateStreamResult as I, Logger as L, ProgressReporter as P, AuthorIdentity as a, Context as b, CreateContextParts as c, FileStat as e, FileSystem as f, Hasher as g, RepositoryConfig as h, RepositoryLayout as i };
466
+ export { ObjectId as O, RefName as R, FilePath as d, createContext as p, noopLogger as q, wrapLoggerSanitizer as w };
467
+ export type { AuthStrategy as A, Compressor as C, DirEntry as D, FileHandle as F, HashService as H, InflateStreamResult as I, Logger as L, ProgressReporter as P, AuthorIdentity as a, Context as b, CreateContextParts as c, FileStat as e, FileSystem as f, Hasher as g, HookName as h, HookRequest as i, HookResult as j, HookRunner as k, PromisorFetchOutcome as l, PromisorRemote as m, RepositoryConfig as n, RepositoryLayout as o };