just-git 1.3.2 → 1.3.4

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.
@@ -87,6 +87,13 @@ interface Storage {
87
87
  * `prefix` is at least 4 hex characters.
88
88
  */
89
89
  findObjectsByPrefix(repoId: string, prefix: string): MaybeAsync<string[]>;
90
+ /** Return all object hashes stored for a repo. */
91
+ listObjectHashes(repoId: string): MaybeAsync<string[]>;
92
+ /**
93
+ * Delete specific objects by hash.
94
+ * Returns the number of objects actually deleted.
95
+ */
96
+ deleteObjects(repoId: string, hashes: ReadonlyArray<string>): MaybeAsync<number>;
90
97
  /**
91
98
  * Read a single ref. Returns the stored {@link Ref} value (direct hash
92
99
  * or symbolic pointer) without following symrefs — the adapter handles
@@ -112,6 +119,21 @@ interface Storage {
112
119
  atomicRefUpdate<T>(repoId: string, fn: (ops: RefOps) => MaybeAsync<T>): MaybeAsync<T>;
113
120
  }
114
121
 
122
+ /** Options for {@link gcRepo}. */
123
+ interface GcOptions {
124
+ /** Report what would be deleted without actually deleting. Default: false. */
125
+ dryRun?: boolean;
126
+ }
127
+ /** Result of a {@link gcRepo} call. */
128
+ interface GcResult {
129
+ /** Number of unreachable objects deleted (or that would be deleted in dry-run mode). */
130
+ deleted: number;
131
+ /** Number of reachable objects retained. */
132
+ retained: number;
133
+ /** True if GC was aborted because refs changed during the walk (concurrent modification detected). */
134
+ aborted?: boolean;
135
+ }
136
+
115
137
  /**
116
138
  * Default session type, produced by the built-in session builder when
117
139
  * no custom `session` config is provided to `createServer`.
@@ -130,10 +152,14 @@ interface Session {
130
152
  * User-provided session builder that transforms raw transport input
131
153
  * into a typed session object threaded through all hooks.
132
154
  *
133
- * TypeScript infers `S` from the return types of the builder functions,
134
- * so hooks receive the custom type without explicit generic annotations.
155
+ * Both properties are optional provide only the transports you use.
156
+ * TypeScript infers `S` from whichever builders are present.
157
+ *
158
+ * If a transport is used at runtime but its builder is missing, the
159
+ * server returns an error (HTTP 501 / SSH exit 128).
135
160
  *
136
161
  * ```ts
162
+ * // HTTP-only — no need to provide ssh
137
163
  * const server = createServer({
138
164
  * storage: new BunSqliteStorage(db),
139
165
  * session: {
@@ -141,10 +167,6 @@ interface Session {
141
167
  * userId: parseJwt(req).sub,
142
168
  * roles: parseJwt(req).roles,
143
169
  * }),
144
- * ssh: (info) => ({
145
- * userId: info.username ?? "anonymous",
146
- * roles: (info.metadata?.roles as string[]) ?? [],
147
- * }),
148
170
  * },
149
171
  * hooks: {
150
172
  * preReceive: ({ session }) => {
@@ -163,10 +185,17 @@ interface SessionBuilder<S> {
163
185
  * Return `S` to proceed, or return a `Response` to short-circuit
164
186
  * the request (e.g. 401 with `WWW-Authenticate` header). This is
165
187
  * the primary mechanism for HTTP auth — no separate middleware needed.
188
+ *
189
+ * When omitted, HTTP requests receive a 501 response.
190
+ */
191
+ http?: (request: Request) => S | Response | Promise<S | Response>;
192
+ /**
193
+ * Build a session from SSH session info.
194
+ *
195
+ * When omitted, SSH sessions receive exit code 128 with a
196
+ * diagnostic message.
166
197
  */
167
- http: (request: Request) => S | Response | Promise<S | Response>;
168
- /** Build a session from SSH session info. */
169
- ssh: (info: SshSessionInfo) => S | Promise<S>;
198
+ ssh?: (info: SshSessionInfo) => S | Promise<S>;
170
199
  }
171
200
  /** Information about the SSH session passed to `handleSession`. */
172
201
  interface SshSessionInfo {
@@ -264,11 +293,12 @@ interface GitServerConfig<S = Session> {
264
293
  */
265
294
  policy?: ServerPolicy;
266
295
  /**
267
- * Custom session builder. When provided, the server calls
268
- * `session.http(request)` for HTTP and `session.ssh(info)` for SSH
269
- * to produce the session object threaded through all hooks.
296
+ * Custom session builder. Provide `http`, `ssh`, or both —
297
+ * the server calls whichever is present for that transport.
298
+ * If a transport is used but its builder is missing, the server
299
+ * returns an error (HTTP 501 / SSH exit 128).
270
300
  *
271
- * When omitted, the built-in `Session` type is used.
301
+ * When omitted entirely, the built-in `Session` type is used.
272
302
  */
273
303
  session?: SessionBuilder<S>;
274
304
  /** Base path prefix to strip from HTTP URLs (e.g. "/git"). */
@@ -304,7 +334,29 @@ interface GitServerConfig<S = Session> {
304
334
  */
305
335
  onError?: false | ((err: unknown, session?: S) => void);
306
336
  }
307
- interface GitServer {
337
+ /**
338
+ * A ref update request for {@link GitServer.updateRefs}.
339
+ *
340
+ * In-process equivalent of a push command — updates a ref with CAS
341
+ * protection and hook enforcement, without transport overhead.
342
+ */
343
+ interface RefUpdateRequest {
344
+ /** Full ref name (e.g. `"refs/heads/main"`, `"refs/tags/v1.0"`). */
345
+ ref: string;
346
+ /** New commit hash, or `null` to delete the ref. */
347
+ newHash: string | null;
348
+ /**
349
+ * Expected current hash for compare-and-swap.
350
+ *
351
+ * - `undefined` (default) — the server reads the current ref state
352
+ * automatically. Still CAS-protected against concurrent updates.
353
+ * - `null` — assert the ref does not exist (create-only).
354
+ * - `string` — explicit CAS: the update fails if the ref's current
355
+ * hash doesn't match.
356
+ */
357
+ oldHash?: string | null;
358
+ }
359
+ interface GitServer<S = Session> {
308
360
  /** Standard fetch-API handler for HTTP: (Request) => Response */
309
361
  fetch(request: Request): Promise<Response>;
310
362
  /**
@@ -340,6 +392,32 @@ interface GitServer {
340
392
  * ```
341
393
  */
342
394
  handleSession(command: string, channel: SshChannel, session?: SshSessionInfo): Promise<number>;
395
+ /**
396
+ * Update refs in-process with hook enforcement and CAS protection.
397
+ *
398
+ * Equivalent to a push, but without transport overhead — no pack
399
+ * negotiation, no object transfer. Objects must already exist in
400
+ * the repo's object store (e.g. via `createCommit` from `just-git/repo`).
401
+ *
402
+ * Runs the full hook lifecycle: `preReceive` → per-ref `update` →
403
+ * CAS application → `postReceive`. Returns per-ref results.
404
+ *
405
+ * ```ts
406
+ * import { createCommit, writeBlob, writeTree } from "just-git/repo";
407
+ *
408
+ * const repo = await server.repo("my-repo");
409
+ * const blob = await writeBlob(repo, "content");
410
+ * const tree = await writeTree(repo, [{ name: "file.txt", hash: blob }]);
411
+ * const commit = await createCommit(repo, { tree, parents: [head], ... });
412
+ *
413
+ * await server.updateRefs("my-repo", [
414
+ * { ref: "refs/heads/auto-fix", newHash: commit },
415
+ * ]);
416
+ * ```
417
+ *
418
+ * @throws If the repo does not exist or the server is shutting down.
419
+ */
420
+ updateRefs(repoId: string, refs: RefUpdateRequest[], session?: S): Promise<RefUpdateResult>;
343
421
  /**
344
422
  * Node.js `http.createServer` compatible handler.
345
423
  *
@@ -355,6 +433,19 @@ interface GitServer {
355
433
  repo(id: string): Promise<GitRepo | null>;
356
434
  /** Delete a repo and all its data. */
357
435
  deleteRepo(id: string): Promise<void>;
436
+ /**
437
+ * Remove unreachable objects from a repo's storage.
438
+ *
439
+ * Walks all objects reachable from the repo's refs, compares against
440
+ * the full set of stored objects, and deletes the difference.
441
+ *
442
+ * If refs change during the walk (e.g. a concurrent push completes),
443
+ * GC aborts and returns `{ aborted: true }` to prevent deleting
444
+ * newly-reachable objects. Callers can retry.
445
+ *
446
+ * @throws If the repo does not exist or the server is shutting down.
447
+ */
448
+ gc(repoId: string, options?: GcOptions): Promise<GcResult>;
358
449
  /**
359
450
  * Graceful shutdown. After calling, new HTTP requests receive 503
360
451
  * and new SSH sessions get exit 128. Resolves when all in-flight
@@ -470,6 +561,17 @@ interface RefAdvertisement {
470
561
  name: string;
471
562
  hash: string;
472
563
  }
564
+ /** Per-ref result from a push or {@link GitServer.updateRefs} call. */
565
+ interface RefResult {
566
+ ref: string;
567
+ ok: boolean;
568
+ error?: string;
569
+ }
570
+ /** Result of a push or {@link GitServer.updateRefs} call. */
571
+ interface RefUpdateResult {
572
+ refResults: RefResult[];
573
+ applied: RefUpdate[];
574
+ }
473
575
 
474
576
  /**
475
577
  * Unified Git server: Smart HTTP + SSH session handling.
@@ -508,7 +610,7 @@ interface RefAdvertisement {
508
610
  * server.handleSession(command, channel, { username });
509
611
  * ```
510
612
  */
511
- declare function createServer<S = Session>(config: GitServerConfig<S>): GitServer;
613
+ declare function createServer<S = Session>(config: GitServerConfig<S>): GitServer<S>;
512
614
  /**
513
615
  * Compose multiple hook sets into a single `ServerHooks` object.
514
616
  *
@@ -674,15 +776,6 @@ interface ApplyReceivePackOptions<S = unknown> {
674
776
  /** Session info threaded through to hooks. */
675
777
  session?: S;
676
778
  }
677
- interface RefResult {
678
- ref: string;
679
- ok: boolean;
680
- error?: string;
681
- }
682
- interface ApplyReceivePackResult {
683
- refResults: RefResult[];
684
- applied: RefUpdate[];
685
- }
686
779
  /**
687
780
  * Run the full receive-pack lifecycle: preReceive hook, per-ref update
688
781
  * hook with ref format validation, CAS ref application, and postReceive
@@ -692,7 +785,14 @@ interface ApplyReceivePackResult {
692
785
  * Does NOT handle unpack failures — the caller should check
693
786
  * `ingestResult.unpackOk` and short-circuit before calling this.
694
787
  */
695
- declare function applyReceivePack<S = unknown>(options: ApplyReceivePackOptions<S>): Promise<ApplyReceivePackResult>;
788
+ declare function applyReceivePack<S = unknown>(options: ApplyReceivePackOptions<S>): Promise<RefUpdateResult>;
789
+ /**
790
+ * Resolve `RefUpdateRequest[]` into fully computed `RefUpdate[]`.
791
+ *
792
+ * Reads current ref state when `oldHash` is not provided, and computes
793
+ * `isFF`/`isCreate`/`isDelete` for each entry.
794
+ */
795
+ declare function resolveRefUpdates(repo: GitRepo, requests: RefUpdateRequest[]): Promise<RefUpdate[]>;
696
796
 
697
797
  /**
698
798
  * In-memory storage backend with multi-repo support.
@@ -723,6 +823,8 @@ declare class MemoryStorage implements Storage {
723
823
  }>): void;
724
824
  hasObject(repoId: string, hash: string): boolean;
725
825
  findObjectsByPrefix(repoId: string, prefix: string): string[];
826
+ listObjectHashes(repoId: string): string[];
827
+ deleteObjects(repoId: string, hashes: ReadonlyArray<string>): number;
726
828
  getRef(repoId: string, name: string): Ref | null;
727
829
  putRef(repoId: string, name: string, ref: Ref): void;
728
830
  removeRef(repoId: string, name: string): void;
@@ -757,6 +859,7 @@ declare class BunSqliteStorage implements Storage {
757
859
  private db;
758
860
  private stmts;
759
861
  private batchInsertTx;
862
+ private batchDeleteTx;
760
863
  constructor(db: BunSqliteDatabase);
761
864
  hasRepo(repoId: string): boolean;
762
865
  insertRepo(repoId: string): void;
@@ -770,6 +873,8 @@ declare class BunSqliteStorage implements Storage {
770
873
  }>): void;
771
874
  hasObject(repoId: string, hash: string): boolean;
772
875
  findObjectsByPrefix(repoId: string, prefix: string): string[];
876
+ listObjectHashes(repoId: string): string[];
877
+ deleteObjects(repoId: string, hashes: ReadonlyArray<string>): number;
773
878
  getRef(repoId: string, name: string): Ref | null;
774
879
  putRef(repoId: string, name: string, ref: Ref): void;
775
880
  removeRef(repoId: string, name: string): void;
@@ -801,6 +906,7 @@ declare class BetterSqlite3Storage implements Storage {
801
906
  private db;
802
907
  private stmts;
803
908
  private batchInsertTx;
909
+ private batchDeleteTx;
804
910
  constructor(db: BetterSqlite3Database);
805
911
  hasRepo(repoId: string): boolean;
806
912
  insertRepo(repoId: string): void;
@@ -814,6 +920,8 @@ declare class BetterSqlite3Storage implements Storage {
814
920
  }>): void;
815
921
  hasObject(repoId: string, hash: string): boolean;
816
922
  findObjectsByPrefix(repoId: string, prefix: string): string[];
923
+ listObjectHashes(repoId: string): string[];
924
+ deleteObjects(repoId: string, hashes: ReadonlyArray<string>): number;
817
925
  getRef(repoId: string, name: string): Ref | null;
818
926
  putRef(repoId: string, name: string, ref: Ref): void;
819
927
  removeRef(repoId: string, name: string): void;
@@ -821,54 +929,35 @@ declare class BetterSqlite3Storage implements Storage {
821
929
  atomicRefUpdate<T>(repoId: string, fn: (ops: RefOps) => T): T;
822
930
  }
823
931
 
824
- /** Minimal database interface for PostgreSQL. Use {@link wrapPgPool} to adapt a `pg` Pool. */
825
- interface PgDatabase {
826
- query<T = any>(text: string, values?: any[]): Promise<{
827
- rows: T[];
828
- }>;
829
- transaction<R>(fn: (tx: PgDatabase) => Promise<R>): Promise<R>;
830
- }
831
932
  /** Minimal pool interface matching the `pg` package's `Pool` class. */
832
933
  interface PgPool {
833
- query(text: string, values?: any[]): Promise<{
834
- rows: any[];
934
+ query<T = any>(text: string, values?: any[]): Promise<{
935
+ rows: T[];
835
936
  }>;
836
937
  connect(): Promise<PgPoolClient>;
837
938
  }
838
- /** Minimal pool client interface matching the `pg` package's `PoolClient`. */
839
939
  interface PgPoolClient {
840
- query(text: string, values?: any[]): Promise<{
841
- rows: any[];
940
+ query<T = any>(text: string, values?: any[]): Promise<{
941
+ rows: T[];
842
942
  }>;
843
943
  release(): void;
844
944
  }
845
945
  /**
846
- * Wrap a `pg`-style pool into a `PgDatabase`.
847
- *
848
- * Handles `BEGIN`/`COMMIT`/`ROLLBACK` and client release automatically.
849
- *
850
- * ```ts
851
- * import { Pool } from "pg";
852
- * const pool = new Pool({ connectionString: "..." });
853
- * const db = wrapPgPool(pool);
854
- * ```
855
- */
856
- declare function wrapPgPool(pool: PgPool): PgDatabase;
857
- /**
858
- * PostgreSQL-backed storage.
946
+ * PostgreSQL-backed storage. Accepts a `pg`-style pool directly.
859
947
  *
860
948
  * Use the static `create` factory (schema setup is async):
861
949
  *
862
950
  * ```ts
863
951
  * import { Pool } from "pg";
864
952
  * const pool = new Pool({ connectionString: "..." });
865
- * const storage = await PgStorage.create(wrapPgPool(pool));
953
+ * const storage = await PgStorage.create(pool);
866
954
  * ```
867
955
  */
868
956
  declare class PgStorage implements Storage {
869
- private db;
957
+ private pool;
870
958
  private constructor();
871
- static create(db: PgDatabase): Promise<PgStorage>;
959
+ static create(pool: PgPool): Promise<PgStorage>;
960
+ private transaction;
872
961
  hasRepo(repoId: string): Promise<boolean>;
873
962
  insertRepo(repoId: string): Promise<void>;
874
963
  deleteRepo(repoId: string): Promise<void>;
@@ -881,6 +970,8 @@ declare class PgStorage implements Storage {
881
970
  }>): Promise<void>;
882
971
  hasObject(repoId: string, hash: string): Promise<boolean>;
883
972
  findObjectsByPrefix(repoId: string, prefix: string): Promise<string[]>;
973
+ listObjectHashes(repoId: string): Promise<string[]>;
974
+ deleteObjects(repoId: string, hashes: ReadonlyArray<string>): Promise<number>;
884
975
  getRef(repoId: string, name: string): Promise<Ref | null>;
885
976
  putRef(repoId: string, name: string, ref: Ref): Promise<void>;
886
977
  removeRef(repoId: string, name: string): Promise<void>;
@@ -888,4 +979,4 @@ declare class PgStorage implements Storage {
888
979
  atomicRefUpdate<T>(repoId: string, fn: (ops: RefOps) => Promise<T> | T): Promise<T>;
889
980
  }
890
981
 
891
- export { type AdvertiseRefsEvent, type AdvertiseResult, type ApplyReceivePackOptions, type ApplyReceivePackResult, type BetterSqlite3Database, BetterSqlite3Storage, type BunSqliteDatabase, BunSqliteStorage, type CreateRepoOptions, GitRepo, type GitServer, type GitServerConfig, type MaybeAsync, MemoryStorage, type NodeHttpRequest, type NodeHttpResponse, type PgDatabase, type PgPool, type PgPoolClient, PgStorage, type PostReceiveEvent, type PreReceiveEvent, type PushCommand, RawObject, type RawRefEntry, type ReceivePackResult, Ref, type RefAdvertisement, type RefOps, type RefResult, type RefUpdate, Rejection, type ServerHooks, type ServerPolicy, type Session, type SessionBuilder, type SshChannel, type SshSessionInfo, type Storage, type UpdateEvent, advertiseRefsWithHooks, applyReceivePack, buildRefAdvertisementBytes, buildRefListBytes, buildRefListPktLines, collectRefs, composeHooks, createServer, handleUploadPack, ingestReceivePack, ingestReceivePackFromStream, wrapPgPool };
982
+ export { type AdvertiseRefsEvent, type AdvertiseResult, type ApplyReceivePackOptions, type BetterSqlite3Database, BetterSqlite3Storage, type BunSqliteDatabase, BunSqliteStorage, type CreateRepoOptions, type GcOptions, type GcResult, GitRepo, type GitServer, type GitServerConfig, type MaybeAsync, MemoryStorage, type NodeHttpRequest, type NodeHttpResponse, type PgPool, PgStorage, type PostReceiveEvent, type PreReceiveEvent, type PushCommand, RawObject, type RawRefEntry, type ReceivePackResult, Ref, type RefAdvertisement, type RefOps, type RefResult, type RefUpdate, type RefUpdateRequest, type RefUpdateResult, Rejection, type ServerHooks, type ServerPolicy, type Session, type SessionBuilder, type SshChannel, type SshSessionInfo, type Storage, type UpdateEvent, advertiseRefsWithHooks, applyReceivePack, buildRefAdvertisementBytes, buildRefListBytes, buildRefListPktLines, collectRefs, composeHooks, createServer, handleUploadPack, ingestReceivePack, ingestReceivePackFromStream, resolveRefUpdates };