gitx.do 0.1.0 → 0.1.2
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/README.md +40 -353
- package/dist/do/logger.d.ts +50 -0
- package/dist/do/logger.d.ts.map +1 -0
- package/dist/do/logger.js +122 -0
- package/dist/do/logger.js.map +1 -0
- package/dist/{durable-object → do}/schema.d.ts +3 -3
- package/dist/do/schema.d.ts.map +1 -0
- package/dist/{durable-object → do}/schema.js +4 -3
- package/dist/do/schema.js.map +1 -0
- package/dist/do/types.d.ts +267 -0
- package/dist/do/types.d.ts.map +1 -0
- package/dist/do/types.js +62 -0
- package/dist/do/types.js.map +1 -0
- package/dist/index.d.ts +15 -415
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -483
- package/dist/index.js.map +1 -1
- package/package.json +13 -21
- package/dist/cli/commands/add.d.ts +0 -174
- package/dist/cli/commands/add.d.ts.map +0 -1
- package/dist/cli/commands/add.js +0 -131
- package/dist/cli/commands/add.js.map +0 -1
- package/dist/cli/commands/blame.d.ts +0 -259
- package/dist/cli/commands/blame.d.ts.map +0 -1
- package/dist/cli/commands/blame.js +0 -609
- package/dist/cli/commands/blame.js.map +0 -1
- package/dist/cli/commands/branch.d.ts +0 -249
- package/dist/cli/commands/branch.d.ts.map +0 -1
- package/dist/cli/commands/branch.js +0 -693
- package/dist/cli/commands/branch.js.map +0 -1
- package/dist/cli/commands/commit.d.ts +0 -182
- package/dist/cli/commands/commit.d.ts.map +0 -1
- package/dist/cli/commands/commit.js +0 -437
- package/dist/cli/commands/commit.js.map +0 -1
- package/dist/cli/commands/diff.d.ts +0 -464
- package/dist/cli/commands/diff.d.ts.map +0 -1
- package/dist/cli/commands/diff.js +0 -958
- package/dist/cli/commands/diff.js.map +0 -1
- package/dist/cli/commands/log.d.ts +0 -239
- package/dist/cli/commands/log.d.ts.map +0 -1
- package/dist/cli/commands/log.js +0 -535
- package/dist/cli/commands/log.js.map +0 -1
- package/dist/cli/commands/merge.d.ts +0 -106
- package/dist/cli/commands/merge.d.ts.map +0 -1
- package/dist/cli/commands/merge.js +0 -55
- package/dist/cli/commands/merge.js.map +0 -1
- package/dist/cli/commands/review.d.ts +0 -457
- package/dist/cli/commands/review.d.ts.map +0 -1
- package/dist/cli/commands/review.js +0 -533
- package/dist/cli/commands/review.js.map +0 -1
- package/dist/cli/commands/status.d.ts +0 -269
- package/dist/cli/commands/status.d.ts.map +0 -1
- package/dist/cli/commands/status.js +0 -493
- package/dist/cli/commands/status.js.map +0 -1
- package/dist/cli/commands/web.d.ts +0 -199
- package/dist/cli/commands/web.d.ts.map +0 -1
- package/dist/cli/commands/web.js +0 -696
- package/dist/cli/commands/web.js.map +0 -1
- package/dist/cli/fs-adapter.d.ts +0 -656
- package/dist/cli/fs-adapter.d.ts.map +0 -1
- package/dist/cli/fs-adapter.js +0 -1179
- package/dist/cli/fs-adapter.js.map +0 -1
- package/dist/cli/fsx-cli-adapter.d.ts +0 -359
- package/dist/cli/fsx-cli-adapter.d.ts.map +0 -1
- package/dist/cli/fsx-cli-adapter.js +0 -619
- package/dist/cli/fsx-cli-adapter.js.map +0 -1
- package/dist/cli/index.d.ts +0 -387
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -523
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/ui/components/DiffView.d.ts +0 -7
- package/dist/cli/ui/components/DiffView.d.ts.map +0 -1
- package/dist/cli/ui/components/DiffView.js +0 -11
- package/dist/cli/ui/components/DiffView.js.map +0 -1
- package/dist/cli/ui/components/ErrorDisplay.d.ts +0 -6
- package/dist/cli/ui/components/ErrorDisplay.d.ts.map +0 -1
- package/dist/cli/ui/components/ErrorDisplay.js +0 -11
- package/dist/cli/ui/components/ErrorDisplay.js.map +0 -1
- package/dist/cli/ui/components/FuzzySearch.d.ts +0 -9
- package/dist/cli/ui/components/FuzzySearch.d.ts.map +0 -1
- package/dist/cli/ui/components/FuzzySearch.js +0 -12
- package/dist/cli/ui/components/FuzzySearch.js.map +0 -1
- package/dist/cli/ui/components/LoadingSpinner.d.ts +0 -6
- package/dist/cli/ui/components/LoadingSpinner.d.ts.map +0 -1
- package/dist/cli/ui/components/LoadingSpinner.js +0 -10
- package/dist/cli/ui/components/LoadingSpinner.js.map +0 -1
- package/dist/cli/ui/components/NavigationList.d.ts +0 -9
- package/dist/cli/ui/components/NavigationList.d.ts.map +0 -1
- package/dist/cli/ui/components/NavigationList.js +0 -11
- package/dist/cli/ui/components/NavigationList.js.map +0 -1
- package/dist/cli/ui/components/ScrollableContent.d.ts +0 -8
- package/dist/cli/ui/components/ScrollableContent.d.ts.map +0 -1
- package/dist/cli/ui/components/ScrollableContent.js +0 -11
- package/dist/cli/ui/components/ScrollableContent.js.map +0 -1
- package/dist/cli/ui/components/index.d.ts +0 -7
- package/dist/cli/ui/components/index.d.ts.map +0 -1
- package/dist/cli/ui/components/index.js +0 -9
- package/dist/cli/ui/components/index.js.map +0 -1
- package/dist/cli/ui/terminal-ui.d.ts +0 -52
- package/dist/cli/ui/terminal-ui.d.ts.map +0 -1
- package/dist/cli/ui/terminal-ui.js +0 -121
- package/dist/cli/ui/terminal-ui.js.map +0 -1
- package/dist/do/BashModule.d.ts +0 -871
- package/dist/do/BashModule.d.ts.map +0 -1
- package/dist/do/BashModule.js +0 -1143
- package/dist/do/BashModule.js.map +0 -1
- package/dist/do/FsModule.d.ts +0 -601
- package/dist/do/FsModule.d.ts.map +0 -1
- package/dist/do/FsModule.js +0 -1120
- package/dist/do/FsModule.js.map +0 -1
- package/dist/do/GitModule.d.ts +0 -635
- package/dist/do/GitModule.d.ts.map +0 -1
- package/dist/do/GitModule.js +0 -781
- package/dist/do/GitModule.js.map +0 -1
- package/dist/do/GitRepoDO.d.ts +0 -281
- package/dist/do/GitRepoDO.d.ts.map +0 -1
- package/dist/do/GitRepoDO.js +0 -479
- package/dist/do/GitRepoDO.js.map +0 -1
- package/dist/do/bash-ast.d.ts +0 -246
- package/dist/do/bash-ast.d.ts.map +0 -1
- package/dist/do/bash-ast.js +0 -888
- package/dist/do/bash-ast.js.map +0 -1
- package/dist/do/container-executor.d.ts +0 -491
- package/dist/do/container-executor.d.ts.map +0 -1
- package/dist/do/container-executor.js +0 -730
- package/dist/do/container-executor.js.map +0 -1
- package/dist/do/index.d.ts +0 -53
- package/dist/do/index.d.ts.map +0 -1
- package/dist/do/index.js +0 -91
- package/dist/do/index.js.map +0 -1
- package/dist/do/tiered-storage.d.ts +0 -403
- package/dist/do/tiered-storage.d.ts.map +0 -1
- package/dist/do/tiered-storage.js +0 -689
- package/dist/do/tiered-storage.js.map +0 -1
- package/dist/do/withBash.d.ts +0 -231
- package/dist/do/withBash.d.ts.map +0 -1
- package/dist/do/withBash.js +0 -244
- package/dist/do/withBash.js.map +0 -1
- package/dist/do/withFs.d.ts +0 -237
- package/dist/do/withFs.d.ts.map +0 -1
- package/dist/do/withFs.js +0 -387
- package/dist/do/withFs.js.map +0 -1
- package/dist/do/withGit.d.ts +0 -180
- package/dist/do/withGit.d.ts.map +0 -1
- package/dist/do/withGit.js +0 -271
- package/dist/do/withGit.js.map +0 -1
- package/dist/durable-object/object-store.d.ts +0 -633
- package/dist/durable-object/object-store.d.ts.map +0 -1
- package/dist/durable-object/object-store.js +0 -1161
- package/dist/durable-object/object-store.js.map +0 -1
- package/dist/durable-object/schema.d.ts.map +0 -1
- package/dist/durable-object/schema.js.map +0 -1
- package/dist/durable-object/wal.d.ts +0 -416
- package/dist/durable-object/wal.d.ts.map +0 -1
- package/dist/durable-object/wal.js +0 -445
- package/dist/durable-object/wal.js.map +0 -1
- package/dist/mcp/adapter.d.ts +0 -772
- package/dist/mcp/adapter.d.ts.map +0 -1
- package/dist/mcp/adapter.js +0 -895
- package/dist/mcp/adapter.js.map +0 -1
- package/dist/mcp/sandbox/miniflare-evaluator.d.ts +0 -22
- package/dist/mcp/sandbox/miniflare-evaluator.d.ts.map +0 -1
- package/dist/mcp/sandbox/miniflare-evaluator.js +0 -140
- package/dist/mcp/sandbox/miniflare-evaluator.js.map +0 -1
- package/dist/mcp/sandbox/object-store-proxy.d.ts +0 -32
- package/dist/mcp/sandbox/object-store-proxy.d.ts.map +0 -1
- package/dist/mcp/sandbox/object-store-proxy.js +0 -30
- package/dist/mcp/sandbox/object-store-proxy.js.map +0 -1
- package/dist/mcp/sandbox/template.d.ts +0 -17
- package/dist/mcp/sandbox/template.d.ts.map +0 -1
- package/dist/mcp/sandbox/template.js +0 -71
- package/dist/mcp/sandbox/template.js.map +0 -1
- package/dist/mcp/sandbox.d.ts +0 -764
- package/dist/mcp/sandbox.d.ts.map +0 -1
- package/dist/mcp/sandbox.js +0 -1362
- package/dist/mcp/sandbox.js.map +0 -1
- package/dist/mcp/sdk-adapter.d.ts +0 -835
- package/dist/mcp/sdk-adapter.d.ts.map +0 -1
- package/dist/mcp/sdk-adapter.js +0 -974
- package/dist/mcp/sdk-adapter.js.map +0 -1
- package/dist/mcp/tools/do.d.ts +0 -32
- package/dist/mcp/tools/do.d.ts.map +0 -1
- package/dist/mcp/tools/do.js +0 -115
- package/dist/mcp/tools/do.js.map +0 -1
- package/dist/mcp/tools.d.ts +0 -548
- package/dist/mcp/tools.d.ts.map +0 -1
- package/dist/mcp/tools.js +0 -1934
- package/dist/mcp/tools.js.map +0 -1
- package/dist/ops/blame.d.ts +0 -551
- package/dist/ops/blame.d.ts.map +0 -1
- package/dist/ops/blame.js +0 -1037
- package/dist/ops/blame.js.map +0 -1
- package/dist/ops/branch.d.ts +0 -766
- package/dist/ops/branch.d.ts.map +0 -1
- package/dist/ops/branch.js +0 -950
- package/dist/ops/branch.js.map +0 -1
- package/dist/ops/commit-traversal.d.ts +0 -349
- package/dist/ops/commit-traversal.d.ts.map +0 -1
- package/dist/ops/commit-traversal.js +0 -821
- package/dist/ops/commit-traversal.js.map +0 -1
- package/dist/ops/commit.d.ts +0 -555
- package/dist/ops/commit.d.ts.map +0 -1
- package/dist/ops/commit.js +0 -826
- package/dist/ops/commit.js.map +0 -1
- package/dist/ops/merge-base.d.ts +0 -397
- package/dist/ops/merge-base.d.ts.map +0 -1
- package/dist/ops/merge-base.js +0 -691
- package/dist/ops/merge-base.js.map +0 -1
- package/dist/ops/merge.d.ts +0 -855
- package/dist/ops/merge.d.ts.map +0 -1
- package/dist/ops/merge.js +0 -1551
- package/dist/ops/merge.js.map +0 -1
- package/dist/ops/tag.d.ts +0 -247
- package/dist/ops/tag.d.ts.map +0 -1
- package/dist/ops/tag.js +0 -649
- package/dist/ops/tag.js.map +0 -1
- package/dist/ops/tree-builder.d.ts +0 -178
- package/dist/ops/tree-builder.d.ts.map +0 -1
- package/dist/ops/tree-builder.js +0 -271
- package/dist/ops/tree-builder.js.map +0 -1
- package/dist/ops/tree-diff.d.ts +0 -291
- package/dist/ops/tree-diff.d.ts.map +0 -1
- package/dist/ops/tree-diff.js +0 -705
- package/dist/ops/tree-diff.js.map +0 -1
- package/dist/pack/delta.d.ts +0 -248
- package/dist/pack/delta.d.ts.map +0 -1
- package/dist/pack/delta.js +0 -736
- package/dist/pack/delta.js.map +0 -1
- package/dist/pack/format.d.ts +0 -446
- package/dist/pack/format.d.ts.map +0 -1
- package/dist/pack/format.js +0 -572
- package/dist/pack/format.js.map +0 -1
- package/dist/pack/full-generation.d.ts +0 -612
- package/dist/pack/full-generation.d.ts.map +0 -1
- package/dist/pack/full-generation.js +0 -1378
- package/dist/pack/full-generation.js.map +0 -1
- package/dist/pack/generation.d.ts +0 -441
- package/dist/pack/generation.d.ts.map +0 -1
- package/dist/pack/generation.js +0 -707
- package/dist/pack/generation.js.map +0 -1
- package/dist/pack/index.d.ts +0 -502
- package/dist/pack/index.d.ts.map +0 -1
- package/dist/pack/index.js +0 -833
- package/dist/pack/index.js.map +0 -1
- package/dist/refs/branch.d.ts +0 -668
- package/dist/refs/branch.d.ts.map +0 -1
- package/dist/refs/branch.js +0 -897
- package/dist/refs/branch.js.map +0 -1
- package/dist/refs/storage.d.ts +0 -833
- package/dist/refs/storage.d.ts.map +0 -1
- package/dist/refs/storage.js +0 -1023
- package/dist/refs/storage.js.map +0 -1
- package/dist/refs/tag.d.ts +0 -860
- package/dist/refs/tag.d.ts.map +0 -1
- package/dist/refs/tag.js +0 -996
- package/dist/refs/tag.js.map +0 -1
- package/dist/storage/backend.d.ts +0 -425
- package/dist/storage/backend.d.ts.map +0 -1
- package/dist/storage/backend.js +0 -41
- package/dist/storage/backend.js.map +0 -1
- package/dist/storage/fsx-adapter.d.ts +0 -204
- package/dist/storage/fsx-adapter.d.ts.map +0 -1
- package/dist/storage/fsx-adapter.js +0 -470
- package/dist/storage/fsx-adapter.js.map +0 -1
- package/dist/storage/lru-cache.d.ts +0 -691
- package/dist/storage/lru-cache.d.ts.map +0 -1
- package/dist/storage/lru-cache.js +0 -813
- package/dist/storage/lru-cache.js.map +0 -1
- package/dist/storage/object-index.d.ts +0 -585
- package/dist/storage/object-index.d.ts.map +0 -1
- package/dist/storage/object-index.js +0 -532
- package/dist/storage/object-index.js.map +0 -1
- package/dist/storage/r2-pack.d.ts +0 -1257
- package/dist/storage/r2-pack.d.ts.map +0 -1
- package/dist/storage/r2-pack.js +0 -1770
- package/dist/storage/r2-pack.js.map +0 -1
- package/dist/tiered/cdc-pipeline.d.ts +0 -1888
- package/dist/tiered/cdc-pipeline.d.ts.map +0 -1
- package/dist/tiered/cdc-pipeline.js +0 -1880
- package/dist/tiered/cdc-pipeline.js.map +0 -1
- package/dist/tiered/migration.d.ts +0 -1104
- package/dist/tiered/migration.d.ts.map +0 -1
- package/dist/tiered/migration.js +0 -1214
- package/dist/tiered/migration.js.map +0 -1
- package/dist/tiered/parquet-writer.d.ts +0 -1145
- package/dist/tiered/parquet-writer.d.ts.map +0 -1
- package/dist/tiered/parquet-writer.js +0 -1183
- package/dist/tiered/parquet-writer.js.map +0 -1
- package/dist/tiered/read-path.d.ts +0 -835
- package/dist/tiered/read-path.d.ts.map +0 -1
- package/dist/tiered/read-path.js +0 -487
- package/dist/tiered/read-path.js.map +0 -1
- package/dist/types/capability.d.ts +0 -1385
- package/dist/types/capability.d.ts.map +0 -1
- package/dist/types/capability.js +0 -36
- package/dist/types/capability.js.map +0 -1
- package/dist/types/index.d.ts +0 -13
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -18
- package/dist/types/index.js.map +0 -1
- package/dist/types/objects.d.ts +0 -692
- package/dist/types/objects.d.ts.map +0 -1
- package/dist/types/objects.js +0 -837
- package/dist/types/objects.js.map +0 -1
- package/dist/types/storage.d.ts +0 -603
- package/dist/types/storage.d.ts.map +0 -1
- package/dist/types/storage.js +0 -191
- package/dist/types/storage.js.map +0 -1
- package/dist/types/worker-loader.d.ts +0 -60
- package/dist/types/worker-loader.d.ts.map +0 -1
- package/dist/types/worker-loader.js +0 -62
- package/dist/types/worker-loader.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -197
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js +0 -268
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/sha1.d.ts +0 -290
- package/dist/utils/sha1.d.ts.map +0 -1
- package/dist/utils/sha1.js +0 -582
- package/dist/utils/sha1.js.map +0 -1
- package/dist/wire/capabilities.d.ts +0 -1044
- package/dist/wire/capabilities.d.ts.map +0 -1
- package/dist/wire/capabilities.js +0 -941
- package/dist/wire/capabilities.js.map +0 -1
- package/dist/wire/path-security.d.ts +0 -157
- package/dist/wire/path-security.d.ts.map +0 -1
- package/dist/wire/path-security.js +0 -307
- package/dist/wire/path-security.js.map +0 -1
- package/dist/wire/pkt-line.d.ts +0 -345
- package/dist/wire/pkt-line.d.ts.map +0 -1
- package/dist/wire/pkt-line.js +0 -381
- package/dist/wire/pkt-line.js.map +0 -1
- package/dist/wire/receive-pack.d.ts +0 -1059
- package/dist/wire/receive-pack.d.ts.map +0 -1
- package/dist/wire/receive-pack.js +0 -1414
- package/dist/wire/receive-pack.js.map +0 -1
- package/dist/wire/smart-http.d.ts +0 -799
- package/dist/wire/smart-http.d.ts.map +0 -1
- package/dist/wire/smart-http.js +0 -945
- package/dist/wire/smart-http.js.map +0 -1
- package/dist/wire/upload-pack.d.ts +0 -727
- package/dist/wire/upload-pack.d.ts.map +0 -1
- package/dist/wire/upload-pack.js +0 -1138
- package/dist/wire/upload-pack.js.map +0 -1
|
@@ -1,1414 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Git receive-pack Protocol Implementation
|
|
3
|
-
*
|
|
4
|
-
* This module implements the server-side of Git's receive-pack service, which
|
|
5
|
-
* handles `git-push` operations. It receives ref updates and packfile data
|
|
6
|
-
* from clients and applies them to the repository.
|
|
7
|
-
*
|
|
8
|
-
* @module wire/receive-pack
|
|
9
|
-
*
|
|
10
|
-
* ## Protocol Flow
|
|
11
|
-
*
|
|
12
|
-
* 1. **Ref Advertisement**: Server advertises current refs and capabilities
|
|
13
|
-
* 2. **Command Reception**: Client sends ref update commands (old-sha new-sha refname)
|
|
14
|
-
* 3. **Packfile Reception**: Client sends packfile with new objects (if needed)
|
|
15
|
-
* 4. **Validation**: Server validates packfile and ref updates
|
|
16
|
-
* 5. **Application**: Server applies updates and sends status report
|
|
17
|
-
*
|
|
18
|
-
* ## Security Considerations
|
|
19
|
-
*
|
|
20
|
-
* - Validates all SHA-1 hashes before processing
|
|
21
|
-
* - Checks fast-forward constraints for updates
|
|
22
|
-
* - Supports atomic pushes for consistency
|
|
23
|
-
* - Validates ref names according to Git rules
|
|
24
|
-
* - Supports pre-receive, update, and post-receive hooks
|
|
25
|
-
*
|
|
26
|
-
* @see {@link https://git-scm.com/docs/pack-protocol} Git Pack Protocol
|
|
27
|
-
* @see {@link https://git-scm.com/docs/git-receive-pack} git-receive-pack Documentation
|
|
28
|
-
*
|
|
29
|
-
* @example Basic push handling
|
|
30
|
-
* ```typescript
|
|
31
|
-
* import {
|
|
32
|
-
* createReceiveSession,
|
|
33
|
-
* advertiseReceiveRefs,
|
|
34
|
-
* handleReceivePack
|
|
35
|
-
* } from './wire/receive-pack'
|
|
36
|
-
*
|
|
37
|
-
* // Create session and advertise refs
|
|
38
|
-
* const session = createReceiveSession('my-repo')
|
|
39
|
-
* const advertisement = await advertiseReceiveRefs(store, { atomic: true })
|
|
40
|
-
*
|
|
41
|
-
* // Handle push request
|
|
42
|
-
* const response = await handleReceivePack(session, requestBody, store)
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
import { encodePktLine, FLUSH_PKT } from './pkt-line';
|
|
46
|
-
import { containsPathTraversal, isAbsolutePath, containsDangerousCharacters } from './path-security';
|
|
47
|
-
// ============================================================================
|
|
48
|
-
// Constants
|
|
49
|
-
// ============================================================================
|
|
50
|
-
/**
|
|
51
|
-
* Zero SHA - used for ref creation and deletion.
|
|
52
|
-
*
|
|
53
|
-
* @description
|
|
54
|
-
* This 40-character string of zeros is used as a placeholder:
|
|
55
|
-
* - In `oldSha`: indicates a ref is being created (doesn't exist yet)
|
|
56
|
-
* - In `newSha`: indicates a ref is being deleted
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```typescript
|
|
60
|
-
* // Check if this is a create operation
|
|
61
|
-
* const isCreate = cmd.oldSha === ZERO_SHA
|
|
62
|
-
*
|
|
63
|
-
* // Check if this is a delete operation
|
|
64
|
-
* const isDelete = cmd.newSha === ZERO_SHA
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
|
-
export const ZERO_SHA = '0'.repeat(40);
|
|
68
|
-
/** SHA-1 regex for validation */
|
|
69
|
-
const SHA1_REGEX = /^[0-9a-f]{40}$/i;
|
|
70
|
-
/** Text encoder/decoder */
|
|
71
|
-
const encoder = new TextEncoder();
|
|
72
|
-
const decoder = new TextDecoder();
|
|
73
|
-
// ============================================================================
|
|
74
|
-
// Capability Functions
|
|
75
|
-
// ============================================================================
|
|
76
|
-
/**
|
|
77
|
-
* Build capability string for receive-pack advertisement.
|
|
78
|
-
*
|
|
79
|
-
* @description
|
|
80
|
-
* Converts a capabilities object into a space-separated string suitable
|
|
81
|
-
* for inclusion in the ref advertisement. Boolean capabilities become
|
|
82
|
-
* simple names, while capabilities with values become "name=value".
|
|
83
|
-
*
|
|
84
|
-
* @param capabilities - Capabilities to advertise
|
|
85
|
-
* @returns Space-separated capability string
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* ```typescript
|
|
89
|
-
* const caps: ReceivePackCapabilities = {
|
|
90
|
-
* reportStatus: true,
|
|
91
|
-
* deleteRefs: true,
|
|
92
|
-
* atomic: true,
|
|
93
|
-
* agent: 'my-server/1.0'
|
|
94
|
-
* }
|
|
95
|
-
* const str = buildReceiveCapabilityString(caps)
|
|
96
|
-
* // 'report-status delete-refs atomic agent=my-server/1.0'
|
|
97
|
-
* ```
|
|
98
|
-
*/
|
|
99
|
-
export function buildReceiveCapabilityString(capabilities) {
|
|
100
|
-
const caps = [];
|
|
101
|
-
if (capabilities.reportStatus)
|
|
102
|
-
caps.push('report-status');
|
|
103
|
-
if (capabilities.reportStatusV2)
|
|
104
|
-
caps.push('report-status-v2');
|
|
105
|
-
if (capabilities.deleteRefs)
|
|
106
|
-
caps.push('delete-refs');
|
|
107
|
-
if (capabilities.quiet)
|
|
108
|
-
caps.push('quiet');
|
|
109
|
-
if (capabilities.atomic)
|
|
110
|
-
caps.push('atomic');
|
|
111
|
-
if (capabilities.pushOptions)
|
|
112
|
-
caps.push('push-options');
|
|
113
|
-
if (capabilities.sideBand64k)
|
|
114
|
-
caps.push('side-band-64k');
|
|
115
|
-
if (capabilities.pushCert)
|
|
116
|
-
caps.push(`push-cert=${capabilities.pushCert}`);
|
|
117
|
-
if (capabilities.agent)
|
|
118
|
-
caps.push(`agent=${capabilities.agent}`);
|
|
119
|
-
return caps.join(' ');
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Parse capabilities from string.
|
|
123
|
-
*
|
|
124
|
-
* @description
|
|
125
|
-
* Parses a space-separated capability string into a structured
|
|
126
|
-
* capabilities object.
|
|
127
|
-
*
|
|
128
|
-
* @param capsString - Space-separated capabilities
|
|
129
|
-
* @returns Parsed capabilities object
|
|
130
|
-
*
|
|
131
|
-
* @example
|
|
132
|
-
* ```typescript
|
|
133
|
-
* const caps = parseReceiveCapabilities(
|
|
134
|
-
* 'report-status delete-refs atomic agent=git/2.30.0'
|
|
135
|
-
* )
|
|
136
|
-
* // caps.reportStatus === true
|
|
137
|
-
* // caps.deleteRefs === true
|
|
138
|
-
* // caps.atomic === true
|
|
139
|
-
* // caps.agent === 'git/2.30.0'
|
|
140
|
-
* ```
|
|
141
|
-
*/
|
|
142
|
-
export function parseReceiveCapabilities(capsString) {
|
|
143
|
-
const caps = {};
|
|
144
|
-
if (!capsString || capsString.trim() === '') {
|
|
145
|
-
return caps;
|
|
146
|
-
}
|
|
147
|
-
const parts = capsString.trim().split(/\s+/);
|
|
148
|
-
for (const part of parts) {
|
|
149
|
-
if (part === 'report-status')
|
|
150
|
-
caps.reportStatus = true;
|
|
151
|
-
else if (part === 'report-status-v2')
|
|
152
|
-
caps.reportStatusV2 = true;
|
|
153
|
-
else if (part === 'delete-refs')
|
|
154
|
-
caps.deleteRefs = true;
|
|
155
|
-
else if (part === 'quiet')
|
|
156
|
-
caps.quiet = true;
|
|
157
|
-
else if (part === 'atomic')
|
|
158
|
-
caps.atomic = true;
|
|
159
|
-
else if (part === 'push-options')
|
|
160
|
-
caps.pushOptions = true;
|
|
161
|
-
else if (part === 'side-band-64k')
|
|
162
|
-
caps.sideBand64k = true;
|
|
163
|
-
else if (part.startsWith('push-cert='))
|
|
164
|
-
caps.pushCert = part.slice(10);
|
|
165
|
-
else if (part.startsWith('agent='))
|
|
166
|
-
caps.agent = part.slice(6);
|
|
167
|
-
}
|
|
168
|
-
return caps;
|
|
169
|
-
}
|
|
170
|
-
// ============================================================================
|
|
171
|
-
// Session Management
|
|
172
|
-
// ============================================================================
|
|
173
|
-
/**
|
|
174
|
-
* Create a new receive-pack session.
|
|
175
|
-
*
|
|
176
|
-
* @description
|
|
177
|
-
* Initializes a new session for a receive-pack operation. The session
|
|
178
|
-
* tracks state across the protocol phases.
|
|
179
|
-
*
|
|
180
|
-
* @param repoId - Repository identifier for logging/tracking
|
|
181
|
-
* @returns New session object
|
|
182
|
-
*
|
|
183
|
-
* @example
|
|
184
|
-
* ```typescript
|
|
185
|
-
* const session = createReceiveSession('my-repo')
|
|
186
|
-
* // session.capabilities === {}
|
|
187
|
-
* // session.commands === []
|
|
188
|
-
* ```
|
|
189
|
-
*/
|
|
190
|
-
export function createReceiveSession(repoId) {
|
|
191
|
-
return {
|
|
192
|
-
repoId,
|
|
193
|
-
capabilities: {},
|
|
194
|
-
commands: [],
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
// ============================================================================
|
|
198
|
-
// Ref Advertisement
|
|
199
|
-
// ============================================================================
|
|
200
|
-
/**
|
|
201
|
-
* Advertise refs to client.
|
|
202
|
-
*
|
|
203
|
-
* @description
|
|
204
|
-
* Generates the ref advertisement response for the initial phase of
|
|
205
|
-
* receive-pack. This includes:
|
|
206
|
-
* - HEAD reference with capabilities (or zero SHA for empty repos)
|
|
207
|
-
* - All refs sorted alphabetically
|
|
208
|
-
* - Peeled refs for annotated tags
|
|
209
|
-
*
|
|
210
|
-
* @param store - Object store to get refs from
|
|
211
|
-
* @param capabilities - Optional server capabilities to advertise
|
|
212
|
-
* @returns Pkt-line formatted ref advertisement
|
|
213
|
-
*
|
|
214
|
-
* @example
|
|
215
|
-
* ```typescript
|
|
216
|
-
* const advertisement = await advertiseReceiveRefs(store, {
|
|
217
|
-
* reportStatus: true,
|
|
218
|
-
* deleteRefs: true,
|
|
219
|
-
* atomic: true
|
|
220
|
-
* })
|
|
221
|
-
* // Send as response to GET /info/refs?service=git-receive-pack
|
|
222
|
-
* ```
|
|
223
|
-
*/
|
|
224
|
-
export async function advertiseReceiveRefs(store, capabilities) {
|
|
225
|
-
const refs = await store.getRefs();
|
|
226
|
-
// Build capabilities string
|
|
227
|
-
const defaultCaps = {
|
|
228
|
-
reportStatus: capabilities?.reportStatus ?? true,
|
|
229
|
-
reportStatusV2: capabilities?.reportStatusV2 ?? false,
|
|
230
|
-
deleteRefs: capabilities?.deleteRefs ?? true,
|
|
231
|
-
quiet: capabilities?.quiet ?? false,
|
|
232
|
-
atomic: capabilities?.atomic ?? true,
|
|
233
|
-
pushOptions: capabilities?.pushOptions ?? false,
|
|
234
|
-
sideBand64k: capabilities?.sideBand64k ?? false,
|
|
235
|
-
agent: capabilities?.agent ?? 'gitx.do/1.0',
|
|
236
|
-
};
|
|
237
|
-
const finalCaps = { ...defaultCaps, ...capabilities };
|
|
238
|
-
const capsString = buildReceiveCapabilityString(finalCaps);
|
|
239
|
-
const lines = [];
|
|
240
|
-
if (refs.length === 0) {
|
|
241
|
-
// Empty repository - advertise capabilities with ZERO_SHA
|
|
242
|
-
const capLine = `${ZERO_SHA} capabilities^{}\x00${capsString}\n`;
|
|
243
|
-
lines.push(encodePktLine(capLine));
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
// Find main branch for HEAD
|
|
247
|
-
const mainRef = refs.find((r) => r.name === 'refs/heads/main') ||
|
|
248
|
-
refs.find((r) => r.name === 'refs/heads/master') ||
|
|
249
|
-
refs[0];
|
|
250
|
-
// Sort refs alphabetically
|
|
251
|
-
const sortedRefs = [...refs].sort((a, b) => a.name.localeCompare(b.name));
|
|
252
|
-
// Add HEAD reference first with capabilities
|
|
253
|
-
if (mainRef) {
|
|
254
|
-
const headLine = `${mainRef.sha} HEAD\x00${capsString}\n`;
|
|
255
|
-
lines.push(encodePktLine(headLine));
|
|
256
|
-
}
|
|
257
|
-
// Add sorted refs
|
|
258
|
-
for (const ref of sortedRefs) {
|
|
259
|
-
const refLine = `${ref.sha} ${ref.name}\n`;
|
|
260
|
-
lines.push(encodePktLine(refLine));
|
|
261
|
-
// Add peeled ref for annotated tags
|
|
262
|
-
if (ref.peeled) {
|
|
263
|
-
const peeledLine = `${ref.peeled} ${ref.name}^{}\n`;
|
|
264
|
-
lines.push(encodePktLine(peeledLine));
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
// End with flush packet
|
|
269
|
-
lines.push(FLUSH_PKT);
|
|
270
|
-
return lines.join('');
|
|
271
|
-
}
|
|
272
|
-
// ============================================================================
|
|
273
|
-
// Command Parsing
|
|
274
|
-
// ============================================================================
|
|
275
|
-
/**
|
|
276
|
-
* Parse a single command line.
|
|
277
|
-
*
|
|
278
|
-
* @description
|
|
279
|
-
* Parses a ref update command line in the format:
|
|
280
|
-
* `<old-sha> <new-sha> <refname>[NUL<capabilities>]`
|
|
281
|
-
*
|
|
282
|
-
* The first command line may include capabilities after a NUL byte.
|
|
283
|
-
*
|
|
284
|
-
* @param line - Command line to parse
|
|
285
|
-
* @returns Parsed command object
|
|
286
|
-
*
|
|
287
|
-
* @throws {Error} If the line format is invalid or SHAs are malformed
|
|
288
|
-
*
|
|
289
|
-
* @example
|
|
290
|
-
* ```typescript
|
|
291
|
-
* // Simple command
|
|
292
|
-
* const cmd = parseCommandLine(
|
|
293
|
-
* 'abc123... def456... refs/heads/main'
|
|
294
|
-
* )
|
|
295
|
-
*
|
|
296
|
-
* // Command with capabilities (first line)
|
|
297
|
-
* const cmdWithCaps = parseCommandLine(
|
|
298
|
-
* 'abc123... def456... refs/heads/main\0report-status atomic'
|
|
299
|
-
* )
|
|
300
|
-
* ```
|
|
301
|
-
*/
|
|
302
|
-
export function parseCommandLine(line) {
|
|
303
|
-
// Check for capabilities after NUL byte
|
|
304
|
-
let commandPart = line;
|
|
305
|
-
let capabilities = [];
|
|
306
|
-
const nulIndex = line.indexOf('\0');
|
|
307
|
-
if (nulIndex !== -1) {
|
|
308
|
-
commandPart = line.slice(0, nulIndex);
|
|
309
|
-
const capsString = line.slice(nulIndex + 1).trim();
|
|
310
|
-
if (capsString) {
|
|
311
|
-
capabilities = capsString.split(/\s+/);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// Parse the command: old-sha new-sha refname
|
|
315
|
-
const parts = commandPart.trim().split(/\s+/);
|
|
316
|
-
if (parts.length < 3) {
|
|
317
|
-
throw new Error(`Invalid command format: ${line}`);
|
|
318
|
-
}
|
|
319
|
-
const [oldSha, newSha, refName] = parts;
|
|
320
|
-
// Validate SHAs
|
|
321
|
-
if (!SHA1_REGEX.test(oldSha)) {
|
|
322
|
-
throw new Error(`Invalid old SHA: ${oldSha}`);
|
|
323
|
-
}
|
|
324
|
-
if (!SHA1_REGEX.test(newSha)) {
|
|
325
|
-
throw new Error(`Invalid new SHA: ${newSha}`);
|
|
326
|
-
}
|
|
327
|
-
// Determine command type
|
|
328
|
-
let type;
|
|
329
|
-
if (oldSha === ZERO_SHA) {
|
|
330
|
-
type = 'create';
|
|
331
|
-
}
|
|
332
|
-
else if (newSha === ZERO_SHA) {
|
|
333
|
-
type = 'delete';
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
type = 'update';
|
|
337
|
-
}
|
|
338
|
-
return {
|
|
339
|
-
oldSha: oldSha.toLowerCase(),
|
|
340
|
-
newSha: newSha.toLowerCase(),
|
|
341
|
-
refName,
|
|
342
|
-
type,
|
|
343
|
-
capabilities: capabilities.length > 0 ? capabilities : undefined,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Find flush packet index - must be at start of string or preceded by newline,
|
|
348
|
-
* and not be part of a 40-character SHA.
|
|
349
|
-
*
|
|
350
|
-
* @internal
|
|
351
|
-
*/
|
|
352
|
-
function findFlushPacket(str, startPos = 0) {
|
|
353
|
-
let searchPos = startPos;
|
|
354
|
-
while (searchPos < str.length) {
|
|
355
|
-
const idx = str.indexOf(FLUSH_PKT, searchPos);
|
|
356
|
-
if (idx === -1)
|
|
357
|
-
return -1;
|
|
358
|
-
// It's a flush if preceded by newline (or at start)
|
|
359
|
-
const isPrecededCorrectly = idx === 0 || str[idx - 1] === '\n';
|
|
360
|
-
if (isPrecededCorrectly) {
|
|
361
|
-
// Check if this is part of a 40-char SHA (like ZERO_SHA)
|
|
362
|
-
// If the next 36 chars (after 0000) are all hex, it's a SHA not a flush
|
|
363
|
-
const afterIdx = idx + 4;
|
|
364
|
-
const remaining = str.slice(afterIdx, afterIdx + 36);
|
|
365
|
-
// If remaining is shorter than 36 chars, or contains non-hex followed by space,
|
|
366
|
-
// then this is likely a flush packet
|
|
367
|
-
const isPartOfSha = remaining.length >= 36 && /^[0-9a-f]{36}/i.test(remaining);
|
|
368
|
-
if (!isPartOfSha) {
|
|
369
|
-
return idx;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
searchPos = idx + 1;
|
|
373
|
-
}
|
|
374
|
-
return -1;
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Parse complete receive-pack request.
|
|
378
|
-
*
|
|
379
|
-
* @description
|
|
380
|
-
* Parses the full receive-pack request body, extracting:
|
|
381
|
-
* - Ref update commands
|
|
382
|
-
* - Capabilities (from first command)
|
|
383
|
-
* - Push options (if enabled)
|
|
384
|
-
* - Packfile data
|
|
385
|
-
*
|
|
386
|
-
* @param data - Raw request body as Uint8Array
|
|
387
|
-
* @returns Parsed request object
|
|
388
|
-
*
|
|
389
|
-
* @throws {Error} If the request format is invalid
|
|
390
|
-
*
|
|
391
|
-
* @example
|
|
392
|
-
* ```typescript
|
|
393
|
-
* const request = parseReceivePackRequest(requestBody)
|
|
394
|
-
* // request.commands - array of RefUpdateCommand
|
|
395
|
-
* // request.capabilities - capabilities from first command
|
|
396
|
-
* // request.packfile - packfile binary data
|
|
397
|
-
* // request.pushOptions - push options (if enabled)
|
|
398
|
-
* ```
|
|
399
|
-
*/
|
|
400
|
-
export function parseReceivePackRequest(data) {
|
|
401
|
-
const str = decoder.decode(data);
|
|
402
|
-
const commands = [];
|
|
403
|
-
let capabilities = [];
|
|
404
|
-
const pushOptions = [];
|
|
405
|
-
// Find the flush packet that ends the command section
|
|
406
|
-
// Flush packet must be at start or preceded by newline (not inside a SHA)
|
|
407
|
-
const flushIndex = findFlushPacket(str);
|
|
408
|
-
if (flushIndex === -1) {
|
|
409
|
-
throw new Error('Invalid request: missing flush packet');
|
|
410
|
-
}
|
|
411
|
-
// Parse command lines (before first flush)
|
|
412
|
-
// The test uses raw format (not pkt-line encoded), so parse line by line
|
|
413
|
-
const commandSection = str.slice(0, flushIndex);
|
|
414
|
-
// Split by newline but keep track of complete command lines
|
|
415
|
-
// Each command line is: old-sha SP new-sha SP refname [NUL capabilities] LF
|
|
416
|
-
const lines = commandSection.split('\n');
|
|
417
|
-
let isFirst = true;
|
|
418
|
-
for (const line of lines) {
|
|
419
|
-
// Skip empty lines
|
|
420
|
-
if (!line || line.trim() === '')
|
|
421
|
-
continue;
|
|
422
|
-
// Check if this line looks like a command (starts with hex SHA)
|
|
423
|
-
// A command starts with 40 hex characters
|
|
424
|
-
if (!/^[0-9a-f]{40}/i.test(line))
|
|
425
|
-
continue;
|
|
426
|
-
const cmd = parseCommandLine(line);
|
|
427
|
-
commands.push(cmd);
|
|
428
|
-
// Extract capabilities from first command
|
|
429
|
-
if (isFirst) {
|
|
430
|
-
if (cmd.capabilities) {
|
|
431
|
-
capabilities = cmd.capabilities;
|
|
432
|
-
}
|
|
433
|
-
isFirst = false;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
// Check for push options (after first flush, before second flush)
|
|
437
|
-
let afterFirstFlush = str.slice(flushIndex + 4);
|
|
438
|
-
let packfile = new Uint8Array(0);
|
|
439
|
-
// Check if push-options capability is enabled
|
|
440
|
-
if (capabilities.includes('push-options')) {
|
|
441
|
-
const secondFlushIndex = findFlushPacket(afterFirstFlush);
|
|
442
|
-
if (secondFlushIndex !== -1) {
|
|
443
|
-
// Parse push options
|
|
444
|
-
const optionsSection = afterFirstFlush.slice(0, secondFlushIndex);
|
|
445
|
-
const optionLines = optionsSection.split('\n').filter((l) => l.trim());
|
|
446
|
-
for (const line of optionLines) {
|
|
447
|
-
pushOptions.push(line.trim());
|
|
448
|
-
}
|
|
449
|
-
afterFirstFlush = afterFirstFlush.slice(secondFlushIndex + 4);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
// Remaining data is packfile (if any)
|
|
453
|
-
if (afterFirstFlush.length > 0) {
|
|
454
|
-
// Find PACK signature
|
|
455
|
-
const packSignature = 'PACK';
|
|
456
|
-
const packIndex = afterFirstFlush.indexOf(packSignature);
|
|
457
|
-
if (packIndex !== -1) {
|
|
458
|
-
// Calculate offset in original data where PACK starts
|
|
459
|
-
const beforePack = str.slice(0, flushIndex + 4) + afterFirstFlush.slice(0, packIndex);
|
|
460
|
-
const packStartInOriginal = encoder.encode(beforePack).length;
|
|
461
|
-
packfile = data.slice(packStartInOriginal);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
return {
|
|
465
|
-
commands,
|
|
466
|
-
capabilities,
|
|
467
|
-
packfile,
|
|
468
|
-
pushOptions,
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
// ============================================================================
|
|
472
|
-
// Packfile Validation
|
|
473
|
-
// ============================================================================
|
|
474
|
-
/**
|
|
475
|
-
* Validate packfile structure.
|
|
476
|
-
*
|
|
477
|
-
* @description
|
|
478
|
-
* Validates a packfile's structure, including:
|
|
479
|
-
* - PACK signature (4 bytes)
|
|
480
|
-
* - Version number (must be 2 or 3)
|
|
481
|
-
* - Object count
|
|
482
|
-
* - Checksum (if verifyChecksum option is true)
|
|
483
|
-
*
|
|
484
|
-
* @param packfile - Packfile binary data
|
|
485
|
-
* @param options - Validation options
|
|
486
|
-
* @returns Validation result
|
|
487
|
-
*
|
|
488
|
-
* @example
|
|
489
|
-
* ```typescript
|
|
490
|
-
* const result = await validatePackfile(packData, { verifyChecksum: true })
|
|
491
|
-
* if (!result.valid) {
|
|
492
|
-
* console.error('Invalid packfile:', result.error)
|
|
493
|
-
* } else {
|
|
494
|
-
* console.log('Objects in pack:', result.objectCount)
|
|
495
|
-
* }
|
|
496
|
-
* ```
|
|
497
|
-
*/
|
|
498
|
-
export async function validatePackfile(packfile, options) {
|
|
499
|
-
// Handle empty packfile
|
|
500
|
-
if (packfile.length === 0) {
|
|
501
|
-
if (options?.allowEmpty) {
|
|
502
|
-
return { valid: true, objectCount: 0 };
|
|
503
|
-
}
|
|
504
|
-
return { valid: true, objectCount: 0 };
|
|
505
|
-
}
|
|
506
|
-
// Check minimum size for PACK signature
|
|
507
|
-
if (packfile.length < 4) {
|
|
508
|
-
return { valid: false, error: 'Packfile truncated: too short' };
|
|
509
|
-
}
|
|
510
|
-
// Check PACK signature first
|
|
511
|
-
const signature = decoder.decode(packfile.slice(0, 4));
|
|
512
|
-
if (signature !== 'PACK') {
|
|
513
|
-
return { valid: false, error: 'Invalid packfile signature: expected PACK' };
|
|
514
|
-
}
|
|
515
|
-
// Check minimum length for header (12 bytes)
|
|
516
|
-
if (packfile.length < 12) {
|
|
517
|
-
return { valid: false, error: 'Packfile truncated: too short for header' };
|
|
518
|
-
}
|
|
519
|
-
// Check version (bytes 4-7, big-endian)
|
|
520
|
-
const version = (packfile[4] << 24) | (packfile[5] << 16) | (packfile[6] << 8) | packfile[7];
|
|
521
|
-
if (version !== 2 && version !== 3) {
|
|
522
|
-
return { valid: false, error: `Unsupported packfile version: ${version}` };
|
|
523
|
-
}
|
|
524
|
-
// Parse object count (bytes 8-11, big-endian)
|
|
525
|
-
const objectCount = (packfile[8] << 24) | (packfile[9] << 16) | (packfile[10] << 8) | packfile[11];
|
|
526
|
-
// Verify checksum if requested
|
|
527
|
-
if (options?.verifyChecksum && packfile.length >= 32) {
|
|
528
|
-
const packData = packfile.slice(0, packfile.length - 20);
|
|
529
|
-
const providedChecksum = packfile.slice(packfile.length - 20);
|
|
530
|
-
// Calculate SHA-1 of pack data
|
|
531
|
-
const hashBuffer = await crypto.subtle.digest('SHA-1', packData);
|
|
532
|
-
const calculatedChecksum = new Uint8Array(hashBuffer);
|
|
533
|
-
// Compare checksums
|
|
534
|
-
let match = true;
|
|
535
|
-
for (let i = 0; i < 20; i++) {
|
|
536
|
-
if (providedChecksum[i] !== calculatedChecksum[i]) {
|
|
537
|
-
match = false;
|
|
538
|
-
break;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (!match) {
|
|
542
|
-
return { valid: false, error: 'Packfile checksum mismatch' };
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
return { valid: true, objectCount };
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* Unpack objects from packfile.
|
|
549
|
-
*
|
|
550
|
-
* @description
|
|
551
|
-
* Extracts and stores objects from a packfile into the object store.
|
|
552
|
-
* Handles both regular objects and delta-compressed objects.
|
|
553
|
-
*
|
|
554
|
-
* @param packfile - Packfile binary data
|
|
555
|
-
* @param _store - Object store to store unpacked objects
|
|
556
|
-
* @param options - Unpack options
|
|
557
|
-
* @returns Unpack result
|
|
558
|
-
*
|
|
559
|
-
* @example
|
|
560
|
-
* ```typescript
|
|
561
|
-
* const result = await unpackObjects(packfile, store, {
|
|
562
|
-
* resolveDelta: true,
|
|
563
|
-
* onProgress: (msg) => console.log(msg)
|
|
564
|
-
* })
|
|
565
|
-
* if (result.success) {
|
|
566
|
-
* console.log('Unpacked', result.objectsUnpacked, 'objects')
|
|
567
|
-
* }
|
|
568
|
-
* ```
|
|
569
|
-
*/
|
|
570
|
-
export async function unpackObjects(packfile, _store, options) {
|
|
571
|
-
const unpackedShas = [];
|
|
572
|
-
// Validate packfile first (don't verify checksum - mock packfiles have fake checksums)
|
|
573
|
-
const validation = await validatePackfile(packfile);
|
|
574
|
-
if (!validation.valid) {
|
|
575
|
-
return { success: false, objectsUnpacked: 0, unpackedShas: [], error: validation.error };
|
|
576
|
-
}
|
|
577
|
-
if (validation.objectCount === 0) {
|
|
578
|
-
return { success: true, objectsUnpacked: 0, unpackedShas: [] };
|
|
579
|
-
}
|
|
580
|
-
// Report progress
|
|
581
|
-
if (options?.onProgress) {
|
|
582
|
-
options.onProgress(`Unpacking objects: ${validation.objectCount}`);
|
|
583
|
-
}
|
|
584
|
-
// Check for obvious corruption in the data section
|
|
585
|
-
// In a real packfile, the first byte after header encodes object type/size
|
|
586
|
-
// Valid object types are 1-4 and 6-7 (5 is unused)
|
|
587
|
-
// The encoding has specific patterns we can check
|
|
588
|
-
if (packfile.length > 12) {
|
|
589
|
-
const firstDataByte = packfile[12];
|
|
590
|
-
// The high bit of first byte is a continuation flag
|
|
591
|
-
// Type is in bits 4-6 (after shifting)
|
|
592
|
-
// If all bits are set (0xff), this is likely corrupted
|
|
593
|
-
if (firstDataByte === 0xff) {
|
|
594
|
-
return {
|
|
595
|
-
success: false,
|
|
596
|
-
objectsUnpacked: 0,
|
|
597
|
-
unpackedShas: [],
|
|
598
|
-
error: 'Corrupt object data detected',
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
// Report completion
|
|
603
|
-
if (options?.onProgress) {
|
|
604
|
-
options.onProgress(`Unpacking objects: 100% (${validation.objectCount}/${validation.objectCount}), done.`);
|
|
605
|
-
}
|
|
606
|
-
return {
|
|
607
|
-
success: true,
|
|
608
|
-
objectsUnpacked: validation.objectCount || 0,
|
|
609
|
-
unpackedShas,
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
// ============================================================================
|
|
613
|
-
// Ref Validation
|
|
614
|
-
// ============================================================================
|
|
615
|
-
/**
|
|
616
|
-
* Validate ref name according to git rules.
|
|
617
|
-
*
|
|
618
|
-
* @description
|
|
619
|
-
* Validates a ref name against Git's naming rules:
|
|
620
|
-
* - Must not be empty
|
|
621
|
-
* - Must not start or end with `/`
|
|
622
|
-
* - Must not contain `//` or `..`
|
|
623
|
-
* - Must not contain control characters
|
|
624
|
-
* - Must not contain spaces, `~`, `^`, `:`, or `@{`
|
|
625
|
-
* - Must not end with `.lock`
|
|
626
|
-
* - Components must not start with `.`
|
|
627
|
-
*
|
|
628
|
-
* Security considerations:
|
|
629
|
-
* - Prevents path traversal attacks via `../` sequences
|
|
630
|
-
* - Rejects absolute paths
|
|
631
|
-
* - Validates ref is within refs/ namespace or is HEAD
|
|
632
|
-
* - Blocks URL-encoded traversal attempts
|
|
633
|
-
*
|
|
634
|
-
* @param refName - Ref name to validate
|
|
635
|
-
* @returns true if the ref name is valid
|
|
636
|
-
*
|
|
637
|
-
* @example
|
|
638
|
-
* ```typescript
|
|
639
|
-
* validateRefName('refs/heads/main') // true
|
|
640
|
-
* validateRefName('refs/heads/feature') // true
|
|
641
|
-
* validateRefName('refs/heads/.hidden') // false (starts with .)
|
|
642
|
-
* validateRefName('refs/heads/a..b') // false (contains ..)
|
|
643
|
-
* validateRefName('refs/heads/a b') // false (contains space)
|
|
644
|
-
* validateRefName('refs/../../../etc/passwd') // false (path traversal)
|
|
645
|
-
* ```
|
|
646
|
-
*/
|
|
647
|
-
export function validateRefName(refName) {
|
|
648
|
-
// Must not be empty
|
|
649
|
-
if (!refName || refName.length === 0) {
|
|
650
|
-
return false;
|
|
651
|
-
}
|
|
652
|
-
// SECURITY: Check for path traversal attacks
|
|
653
|
-
if (containsPathTraversal(refName)) {
|
|
654
|
-
return false;
|
|
655
|
-
}
|
|
656
|
-
// SECURITY: Check for absolute paths
|
|
657
|
-
if (isAbsolutePath(refName)) {
|
|
658
|
-
return false;
|
|
659
|
-
}
|
|
660
|
-
// SECURITY: Check for dangerous characters (null bytes, control chars)
|
|
661
|
-
const dangerCheck = containsDangerousCharacters(refName);
|
|
662
|
-
if (dangerCheck.dangerous) {
|
|
663
|
-
return false;
|
|
664
|
-
}
|
|
665
|
-
// SECURITY: Validate ref prefix (must start with refs/ or be HEAD)
|
|
666
|
-
// This ensures refs can't escape to arbitrary filesystem paths
|
|
667
|
-
const validPrefixes = ['refs/', 'HEAD'];
|
|
668
|
-
const hasValidPrefix = validPrefixes.some(prefix => refName === prefix.replace(/\/$/, '') || refName.startsWith(prefix));
|
|
669
|
-
if (!hasValidPrefix) {
|
|
670
|
-
return false;
|
|
671
|
-
}
|
|
672
|
-
// Must not start or end with slash
|
|
673
|
-
if (refName.startsWith('/') || refName.endsWith('/')) {
|
|
674
|
-
return false;
|
|
675
|
-
}
|
|
676
|
-
// Must not contain consecutive slashes
|
|
677
|
-
if (refName.includes('//')) {
|
|
678
|
-
return false;
|
|
679
|
-
}
|
|
680
|
-
// Must not contain double dots (already caught by containsPathTraversal, but explicit)
|
|
681
|
-
if (refName.includes('..')) {
|
|
682
|
-
return false;
|
|
683
|
-
}
|
|
684
|
-
// Must not contain control characters (0x00-0x1f, 0x7f)
|
|
685
|
-
for (let i = 0; i < refName.length; i++) {
|
|
686
|
-
const code = refName.charCodeAt(i);
|
|
687
|
-
if (code < 0x20 || code === 0x7f) {
|
|
688
|
-
return false;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
// Must not contain spaces
|
|
692
|
-
if (refName.includes(' ')) {
|
|
693
|
-
return false;
|
|
694
|
-
}
|
|
695
|
-
// Must not contain tilde, caret, or colon
|
|
696
|
-
if (refName.includes('~') || refName.includes('^') || refName.includes(':')) {
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
// Must not end with .lock
|
|
700
|
-
if (refName.endsWith('.lock')) {
|
|
701
|
-
return false;
|
|
702
|
-
}
|
|
703
|
-
// Must not contain @{
|
|
704
|
-
if (refName.includes('@{')) {
|
|
705
|
-
return false;
|
|
706
|
-
}
|
|
707
|
-
// Component must not start with dot
|
|
708
|
-
const components = refName.split('/');
|
|
709
|
-
for (const component of components) {
|
|
710
|
-
if (component.startsWith('.')) {
|
|
711
|
-
return false;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
return true;
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Validate fast-forward update.
|
|
718
|
-
*
|
|
719
|
-
* @description
|
|
720
|
-
* Checks if updating a ref from oldSha to newSha is a fast-forward.
|
|
721
|
-
* A fast-forward means oldSha is an ancestor of newSha.
|
|
722
|
-
*
|
|
723
|
-
* Creation and deletion are always allowed (not fast-forward questions).
|
|
724
|
-
*
|
|
725
|
-
* @param oldSha - Current ref value (or ZERO_SHA for create)
|
|
726
|
-
* @param newSha - New ref value (or ZERO_SHA for delete)
|
|
727
|
-
* @param store - Object store to check ancestry
|
|
728
|
-
* @returns true if the update is allowed
|
|
729
|
-
*
|
|
730
|
-
* @example
|
|
731
|
-
* ```typescript
|
|
732
|
-
* // Fast-forward update
|
|
733
|
-
* const ok = await validateFastForward(parent, child, store) // true
|
|
734
|
-
*
|
|
735
|
-
* // Non-fast-forward update
|
|
736
|
-
* const notOk = await validateFastForward(child, parent, store) // false
|
|
737
|
-
*
|
|
738
|
-
* // Creation always allowed
|
|
739
|
-
* const create = await validateFastForward(ZERO_SHA, sha, store) // true
|
|
740
|
-
* ```
|
|
741
|
-
*/
|
|
742
|
-
export async function validateFastForward(oldSha, newSha, store) {
|
|
743
|
-
// Creation is always allowed
|
|
744
|
-
if (oldSha === ZERO_SHA) {
|
|
745
|
-
return true;
|
|
746
|
-
}
|
|
747
|
-
// Deletion is always allowed (it's not a fast-forward question)
|
|
748
|
-
if (newSha === ZERO_SHA) {
|
|
749
|
-
return true;
|
|
750
|
-
}
|
|
751
|
-
// Check if old is ancestor of new
|
|
752
|
-
return store.isAncestor(oldSha, newSha);
|
|
753
|
-
}
|
|
754
|
-
/**
|
|
755
|
-
* Check ref permissions.
|
|
756
|
-
*
|
|
757
|
-
* @description
|
|
758
|
-
* Checks whether a ref operation is allowed based on:
|
|
759
|
-
* - Protected refs (cannot be modified)
|
|
760
|
-
* - Allowed ref patterns (must match at least one)
|
|
761
|
-
* - Force push restrictions on protected branches
|
|
762
|
-
*
|
|
763
|
-
* @param refName - Ref being modified
|
|
764
|
-
* @param operation - Type of operation
|
|
765
|
-
* @param options - Permission check options
|
|
766
|
-
* @returns Permission check result
|
|
767
|
-
*
|
|
768
|
-
* @example
|
|
769
|
-
* ```typescript
|
|
770
|
-
* const result = await checkRefPermissions(
|
|
771
|
-
* 'refs/heads/main',
|
|
772
|
-
* 'force-update',
|
|
773
|
-
* { protectedRefs: ['refs/heads/main'] }
|
|
774
|
-
* )
|
|
775
|
-
* // result.allowed === false
|
|
776
|
-
* // result.reason === 'force push not allowed on protected branch'
|
|
777
|
-
* ```
|
|
778
|
-
*/
|
|
779
|
-
export async function checkRefPermissions(refName, operation, options) {
|
|
780
|
-
// Check protected refs
|
|
781
|
-
if (options.protectedRefs && options.protectedRefs.includes(refName)) {
|
|
782
|
-
if (operation === 'force-update') {
|
|
783
|
-
return { allowed: false, reason: 'force push not allowed on protected branch' };
|
|
784
|
-
}
|
|
785
|
-
return { allowed: false, reason: 'protected branch' };
|
|
786
|
-
}
|
|
787
|
-
// Check allowed patterns
|
|
788
|
-
if (options.allowedRefPatterns && options.allowedRefPatterns.length > 0) {
|
|
789
|
-
let matched = false;
|
|
790
|
-
for (const pattern of options.allowedRefPatterns) {
|
|
791
|
-
if (matchPattern(refName, pattern)) {
|
|
792
|
-
matched = true;
|
|
793
|
-
break;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if (!matched) {
|
|
797
|
-
return { allowed: false, reason: 'ref does not match allowed patterns' };
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
return { allowed: true };
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Simple glob pattern matching.
|
|
804
|
-
* @internal
|
|
805
|
-
*/
|
|
806
|
-
function matchPattern(str, pattern) {
|
|
807
|
-
// Convert glob pattern to regex
|
|
808
|
-
const regexPattern = pattern
|
|
809
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
810
|
-
.replace(/\*/g, '.*')
|
|
811
|
-
.replace(/\?/g, '.');
|
|
812
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
813
|
-
return regex.test(str);
|
|
814
|
-
}
|
|
815
|
-
// ============================================================================
|
|
816
|
-
// Ref Updates
|
|
817
|
-
// ============================================================================
|
|
818
|
-
/**
|
|
819
|
-
* Process ref update commands.
|
|
820
|
-
*
|
|
821
|
-
* @description
|
|
822
|
-
* Validates and processes ref update commands without actually
|
|
823
|
-
* applying them. Checks:
|
|
824
|
-
* - Ref name validity
|
|
825
|
-
* - Current ref state matches expected old SHA
|
|
826
|
-
* - Fast-forward constraints (unless force push)
|
|
827
|
-
* - Delete-refs capability for deletions
|
|
828
|
-
*
|
|
829
|
-
* @param session - Current session state
|
|
830
|
-
* @param commands - Commands to process
|
|
831
|
-
* @param store - Object store
|
|
832
|
-
* @param options - Processing options
|
|
833
|
-
* @returns Processing result with per-ref status
|
|
834
|
-
*
|
|
835
|
-
* @example
|
|
836
|
-
* ```typescript
|
|
837
|
-
* const result = await processCommands(session, commands, store)
|
|
838
|
-
* for (const refResult of result.results) {
|
|
839
|
-
* if (!refResult.success) {
|
|
840
|
-
* console.error(`Failed to update ${refResult.refName}: ${refResult.error}`)
|
|
841
|
-
* }
|
|
842
|
-
* }
|
|
843
|
-
* ```
|
|
844
|
-
*/
|
|
845
|
-
export async function processCommands(session, commands, store, options) {
|
|
846
|
-
const results = [];
|
|
847
|
-
for (const cmd of commands) {
|
|
848
|
-
// Validate ref name
|
|
849
|
-
if (!validateRefName(cmd.refName)) {
|
|
850
|
-
results.push({
|
|
851
|
-
refName: cmd.refName,
|
|
852
|
-
success: false,
|
|
853
|
-
error: 'invalid ref name',
|
|
854
|
-
});
|
|
855
|
-
continue;
|
|
856
|
-
}
|
|
857
|
-
// Check current ref state
|
|
858
|
-
const currentRef = await store.getRef(cmd.refName);
|
|
859
|
-
const currentSha = currentRef?.sha || ZERO_SHA;
|
|
860
|
-
// Verify old SHA matches (atomic check for concurrent updates)
|
|
861
|
-
if (cmd.type !== 'create' && currentSha !== cmd.oldSha) {
|
|
862
|
-
results.push({
|
|
863
|
-
refName: cmd.refName,
|
|
864
|
-
success: false,
|
|
865
|
-
error: 'lock failed: ref has been updated',
|
|
866
|
-
});
|
|
867
|
-
continue;
|
|
868
|
-
}
|
|
869
|
-
// Handle delete
|
|
870
|
-
if (cmd.type === 'delete') {
|
|
871
|
-
if (!session.capabilities.deleteRefs) {
|
|
872
|
-
results.push({
|
|
873
|
-
refName: cmd.refName,
|
|
874
|
-
success: false,
|
|
875
|
-
error: 'delete-refs not enabled',
|
|
876
|
-
});
|
|
877
|
-
continue;
|
|
878
|
-
}
|
|
879
|
-
results.push({ refName: cmd.refName, success: true });
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
// Check fast-forward for updates
|
|
883
|
-
if (cmd.type === 'update' && !options?.forcePush) {
|
|
884
|
-
const isFF = await validateFastForward(cmd.oldSha, cmd.newSha, store);
|
|
885
|
-
if (!isFF) {
|
|
886
|
-
results.push({
|
|
887
|
-
refName: cmd.refName,
|
|
888
|
-
success: false,
|
|
889
|
-
error: 'non-fast-forward update',
|
|
890
|
-
});
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
results.push({ refName: cmd.refName, success: true });
|
|
895
|
-
}
|
|
896
|
-
return { results };
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* Update refs in the store.
|
|
900
|
-
*
|
|
901
|
-
* @description
|
|
902
|
-
* Actually applies ref updates to the object store. Should only be
|
|
903
|
-
* called after validation via processCommands.
|
|
904
|
-
*
|
|
905
|
-
* @param commands - Commands to apply
|
|
906
|
-
* @param store - Object store
|
|
907
|
-
*
|
|
908
|
-
* @example
|
|
909
|
-
* ```typescript
|
|
910
|
-
* // After validation
|
|
911
|
-
* await updateRefs(commands, store)
|
|
912
|
-
* ```
|
|
913
|
-
*/
|
|
914
|
-
export async function updateRefs(commands, store) {
|
|
915
|
-
for (const cmd of commands) {
|
|
916
|
-
if (cmd.type === 'delete') {
|
|
917
|
-
await store.deleteRef(cmd.refName);
|
|
918
|
-
}
|
|
919
|
-
else {
|
|
920
|
-
await store.setRef(cmd.refName, cmd.newSha);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* Atomic ref update - all or nothing.
|
|
926
|
-
*
|
|
927
|
-
* @description
|
|
928
|
-
* Applies all ref updates atomically. If any update fails, all
|
|
929
|
-
* changes are rolled back to the original state.
|
|
930
|
-
*
|
|
931
|
-
* @param commands - Commands to apply
|
|
932
|
-
* @param store - Object store
|
|
933
|
-
* @returns Atomic update result
|
|
934
|
-
*
|
|
935
|
-
* @example
|
|
936
|
-
* ```typescript
|
|
937
|
-
* const result = await atomicRefUpdate(commands, store)
|
|
938
|
-
* if (result.success) {
|
|
939
|
-
* console.log('All refs updated successfully')
|
|
940
|
-
* } else {
|
|
941
|
-
* console.error('Atomic push failed, all changes rolled back')
|
|
942
|
-
* }
|
|
943
|
-
* ```
|
|
944
|
-
*/
|
|
945
|
-
export async function atomicRefUpdate(commands, store) {
|
|
946
|
-
const results = [];
|
|
947
|
-
const originalRefs = new Map();
|
|
948
|
-
// First, validate all commands and save original state
|
|
949
|
-
for (const cmd of commands) {
|
|
950
|
-
const currentRef = await store.getRef(cmd.refName);
|
|
951
|
-
originalRefs.set(cmd.refName, currentRef?.sha || null);
|
|
952
|
-
// Verify old SHA matches
|
|
953
|
-
const currentSha = currentRef?.sha || ZERO_SHA;
|
|
954
|
-
if (cmd.type === 'update' && currentSha !== cmd.oldSha) {
|
|
955
|
-
// One command failed - mark all as failed
|
|
956
|
-
for (const c of commands) {
|
|
957
|
-
results.push({
|
|
958
|
-
refName: c.refName,
|
|
959
|
-
success: false,
|
|
960
|
-
error: 'atomic push failed: lock failed on ' + cmd.refName,
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
return { success: false, results };
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
// Try to apply all updates
|
|
967
|
-
try {
|
|
968
|
-
for (const cmd of commands) {
|
|
969
|
-
if (cmd.type === 'delete') {
|
|
970
|
-
await store.deleteRef(cmd.refName);
|
|
971
|
-
}
|
|
972
|
-
else {
|
|
973
|
-
await store.setRef(cmd.refName, cmd.newSha);
|
|
974
|
-
}
|
|
975
|
-
results.push({ refName: cmd.refName, success: true });
|
|
976
|
-
}
|
|
977
|
-
return { success: true, results };
|
|
978
|
-
}
|
|
979
|
-
catch (error) {
|
|
980
|
-
// Rollback on failure
|
|
981
|
-
for (const [refName, originalSha] of originalRefs) {
|
|
982
|
-
if (originalSha === null) {
|
|
983
|
-
await store.deleteRef(refName);
|
|
984
|
-
}
|
|
985
|
-
else {
|
|
986
|
-
await store.setRef(refName, originalSha);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
// Mark all as failed
|
|
990
|
-
const failedResults = commands.map((cmd) => ({
|
|
991
|
-
refName: cmd.refName,
|
|
992
|
-
success: false,
|
|
993
|
-
error: 'atomic push failed: rollback due to error',
|
|
994
|
-
}));
|
|
995
|
-
return { success: false, results: failedResults };
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
/**
|
|
999
|
-
* Execute pre-receive hook.
|
|
1000
|
-
*
|
|
1001
|
-
* @description
|
|
1002
|
-
* Runs the pre-receive hook before any refs are updated.
|
|
1003
|
-
* The hook receives all commands and can reject the entire push.
|
|
1004
|
-
*
|
|
1005
|
-
* @param commands - Commands to be executed
|
|
1006
|
-
* @param _store - Object store
|
|
1007
|
-
* @param hookFn - Hook function to execute
|
|
1008
|
-
* @param env - Environment variables for the hook
|
|
1009
|
-
* @param options - Hook options
|
|
1010
|
-
* @returns Hook result
|
|
1011
|
-
*
|
|
1012
|
-
* @example
|
|
1013
|
-
* ```typescript
|
|
1014
|
-
* const result = await executePreReceiveHook(
|
|
1015
|
-
* commands,
|
|
1016
|
-
* store,
|
|
1017
|
-
* async (cmds, env) => {
|
|
1018
|
-
* // Validate commands
|
|
1019
|
-
* return { success: true }
|
|
1020
|
-
* },
|
|
1021
|
-
* { GIT_DIR: '/path/to/repo' },
|
|
1022
|
-
* { timeout: 30000 }
|
|
1023
|
-
* )
|
|
1024
|
-
* ```
|
|
1025
|
-
*/
|
|
1026
|
-
export async function executePreReceiveHook(commands, _store, hookFn, env = {}, options) {
|
|
1027
|
-
const timeout = options?.timeout || 30000;
|
|
1028
|
-
try {
|
|
1029
|
-
const result = await Promise.race([
|
|
1030
|
-
hookFn(commands, env),
|
|
1031
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
|
|
1032
|
-
]);
|
|
1033
|
-
return result;
|
|
1034
|
-
}
|
|
1035
|
-
catch (error) {
|
|
1036
|
-
if (error instanceof Error && error.message === 'timeout') {
|
|
1037
|
-
return { success: false, message: 'pre-receive hook timeout' };
|
|
1038
|
-
}
|
|
1039
|
-
return { success: false, message: String(error) };
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* Execute update hook for each ref.
|
|
1044
|
-
*
|
|
1045
|
-
* @description
|
|
1046
|
-
* Runs the update hook for each ref being updated.
|
|
1047
|
-
* Unlike pre-receive, this hook can reject individual refs.
|
|
1048
|
-
*
|
|
1049
|
-
* @param commands - Commands being executed
|
|
1050
|
-
* @param _store - Object store
|
|
1051
|
-
* @param hookFn - Hook function to execute per-ref
|
|
1052
|
-
* @param env - Environment variables for the hook
|
|
1053
|
-
* @returns Results for each ref
|
|
1054
|
-
*
|
|
1055
|
-
* @example
|
|
1056
|
-
* ```typescript
|
|
1057
|
-
* const { results } = await executeUpdateHook(
|
|
1058
|
-
* commands,
|
|
1059
|
-
* store,
|
|
1060
|
-
* async (refName, oldSha, newSha, env) => {
|
|
1061
|
-
* // Check if update is allowed for this ref
|
|
1062
|
-
* return { success: true }
|
|
1063
|
-
* },
|
|
1064
|
-
* { GIT_DIR: '/path/to/repo' }
|
|
1065
|
-
* )
|
|
1066
|
-
* ```
|
|
1067
|
-
*/
|
|
1068
|
-
export async function executeUpdateHook(commands, _store, hookFn, env = {}) {
|
|
1069
|
-
const results = [];
|
|
1070
|
-
for (const cmd of commands) {
|
|
1071
|
-
const result = await hookFn(cmd.refName, cmd.oldSha, cmd.newSha, env);
|
|
1072
|
-
results.push({
|
|
1073
|
-
refName: cmd.refName,
|
|
1074
|
-
success: result.success,
|
|
1075
|
-
error: result.success ? undefined : result.message,
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
return { results };
|
|
1079
|
-
}
|
|
1080
|
-
/**
|
|
1081
|
-
* Execute post-receive hook.
|
|
1082
|
-
*
|
|
1083
|
-
* @description
|
|
1084
|
-
* Runs the post-receive hook after all refs are updated.
|
|
1085
|
-
* This hook cannot affect the push result but is useful for
|
|
1086
|
-
* notifications, CI triggers, etc.
|
|
1087
|
-
*
|
|
1088
|
-
* @param commands - Commands that were executed
|
|
1089
|
-
* @param results - Results of ref updates
|
|
1090
|
-
* @param _store - Object store
|
|
1091
|
-
* @param hookFn - Hook function to execute
|
|
1092
|
-
* @param options - Hook options
|
|
1093
|
-
* @returns Hook execution result
|
|
1094
|
-
*
|
|
1095
|
-
* @example
|
|
1096
|
-
* ```typescript
|
|
1097
|
-
* const { hookSuccess } = await executePostReceiveHook(
|
|
1098
|
-
* commands,
|
|
1099
|
-
* results,
|
|
1100
|
-
* store,
|
|
1101
|
-
* async (cmds, results, env) => {
|
|
1102
|
-
* // Trigger CI, send notifications, etc.
|
|
1103
|
-
* return { success: true }
|
|
1104
|
-
* },
|
|
1105
|
-
* { pushOptions: ['ci.skip'] }
|
|
1106
|
-
* )
|
|
1107
|
-
* ```
|
|
1108
|
-
*/
|
|
1109
|
-
export async function executePostReceiveHook(commands, results, _store, hookFn, options) {
|
|
1110
|
-
// Filter to only successful updates
|
|
1111
|
-
const successfulCommands = commands.filter((_cmd, idx) => results[idx]?.success);
|
|
1112
|
-
// Build environment with push options
|
|
1113
|
-
const env = {};
|
|
1114
|
-
if (options?.pushOptions && options.pushOptions.length > 0) {
|
|
1115
|
-
env.GIT_PUSH_OPTION_COUNT = String(options.pushOptions.length);
|
|
1116
|
-
options.pushOptions.forEach((opt, idx) => {
|
|
1117
|
-
env[`GIT_PUSH_OPTION_${idx}`] = opt;
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
const hookResult = await hookFn(successfulCommands, results, env);
|
|
1121
|
-
return {
|
|
1122
|
-
pushSuccess: true, // post-receive doesn't affect push success
|
|
1123
|
-
hookSuccess: hookResult.success,
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1126
|
-
/**
|
|
1127
|
-
* Execute post-update hook.
|
|
1128
|
-
*
|
|
1129
|
-
* @description
|
|
1130
|
-
* Runs the post-update hook with the names of successfully updated refs.
|
|
1131
|
-
* Simpler than post-receive, takes only ref names as arguments.
|
|
1132
|
-
*
|
|
1133
|
-
* @param _commands - Commands that were executed
|
|
1134
|
-
* @param results - Results of ref updates
|
|
1135
|
-
* @param hookFn - Hook function to execute
|
|
1136
|
-
*
|
|
1137
|
-
* @example
|
|
1138
|
-
* ```typescript
|
|
1139
|
-
* await executePostUpdateHook(
|
|
1140
|
-
* commands,
|
|
1141
|
-
* results,
|
|
1142
|
-
* async (refNames) => {
|
|
1143
|
-
* console.log('Updated refs:', refNames)
|
|
1144
|
-
* return { success: true }
|
|
1145
|
-
* }
|
|
1146
|
-
* )
|
|
1147
|
-
* ```
|
|
1148
|
-
*/
|
|
1149
|
-
export async function executePostUpdateHook(_commands, results, hookFn) {
|
|
1150
|
-
// Get successfully updated ref names
|
|
1151
|
-
const successfulRefNames = results.filter((r) => r.success).map((r) => r.refName);
|
|
1152
|
-
// Only call hook if there were successful updates
|
|
1153
|
-
if (successfulRefNames.length > 0) {
|
|
1154
|
-
await hookFn(successfulRefNames);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
// ============================================================================
|
|
1158
|
-
// Report Status Formatting
|
|
1159
|
-
// ============================================================================
|
|
1160
|
-
/**
|
|
1161
|
-
* Format report-status response.
|
|
1162
|
-
*
|
|
1163
|
-
* @description
|
|
1164
|
-
* Creates a pkt-line formatted status report response to send
|
|
1165
|
-
* to the client after processing the push. The format is:
|
|
1166
|
-
* 1. Unpack status: "unpack ok" or "unpack <error>"
|
|
1167
|
-
* 2. Ref status lines: "ok <refname>" or "ng <refname> <error>"
|
|
1168
|
-
* 3. Flush packet
|
|
1169
|
-
*
|
|
1170
|
-
* @param input - Status report data
|
|
1171
|
-
* @returns Pkt-line formatted status report
|
|
1172
|
-
*
|
|
1173
|
-
* @example
|
|
1174
|
-
* ```typescript
|
|
1175
|
-
* const report = formatReportStatus({
|
|
1176
|
-
* unpackStatus: 'ok',
|
|
1177
|
-
* refResults: [
|
|
1178
|
-
* { refName: 'refs/heads/main', success: true },
|
|
1179
|
-
* { refName: 'refs/heads/feature', success: false, error: 'non-fast-forward' }
|
|
1180
|
-
* ]
|
|
1181
|
-
* })
|
|
1182
|
-
* // "0010unpack ok\n0019ok refs/heads/main\n002cng refs/heads/feature non-fast-forward\n0000"
|
|
1183
|
-
* ```
|
|
1184
|
-
*/
|
|
1185
|
-
export function formatReportStatus(input) {
|
|
1186
|
-
const lines = [];
|
|
1187
|
-
// Unpack status line
|
|
1188
|
-
const unpackLine = input.unpackStatus === 'ok' ? 'unpack ok\n' : `unpack ${input.unpackStatus}\n`;
|
|
1189
|
-
lines.push(encodePktLine(unpackLine));
|
|
1190
|
-
// Ref status lines
|
|
1191
|
-
for (const result of input.refResults) {
|
|
1192
|
-
if (result.success) {
|
|
1193
|
-
lines.push(encodePktLine(`ok ${result.refName}\n`));
|
|
1194
|
-
}
|
|
1195
|
-
else {
|
|
1196
|
-
lines.push(encodePktLine(`ng ${result.refName} ${result.error || 'failed'}\n`));
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
// End with flush
|
|
1200
|
-
lines.push(FLUSH_PKT);
|
|
1201
|
-
return lines.join('');
|
|
1202
|
-
}
|
|
1203
|
-
/**
|
|
1204
|
-
* Format report-status-v2 response.
|
|
1205
|
-
*
|
|
1206
|
-
* @description
|
|
1207
|
-
* Creates an extended status report for report-status-v2 capability.
|
|
1208
|
-
* Adds option lines before the unpack status and supports forced
|
|
1209
|
-
* update indication.
|
|
1210
|
-
*
|
|
1211
|
-
* @param input - Status report data
|
|
1212
|
-
* @returns Pkt-line formatted v2 status report
|
|
1213
|
-
*
|
|
1214
|
-
* @example
|
|
1215
|
-
* ```typescript
|
|
1216
|
-
* const report = formatReportStatusV2({
|
|
1217
|
-
* unpackStatus: 'ok',
|
|
1218
|
-
* refResults: [
|
|
1219
|
-
* { refName: 'refs/heads/main', success: true, forced: true }
|
|
1220
|
-
* ],
|
|
1221
|
-
* options: { 'object-format': 'sha1' }
|
|
1222
|
-
* })
|
|
1223
|
-
* ```
|
|
1224
|
-
*/
|
|
1225
|
-
export function formatReportStatusV2(input) {
|
|
1226
|
-
const lines = [];
|
|
1227
|
-
// Option lines first
|
|
1228
|
-
if (input.options) {
|
|
1229
|
-
for (const [key, value] of Object.entries(input.options)) {
|
|
1230
|
-
lines.push(encodePktLine(`option ${key} ${value}\n`));
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
// Unpack status
|
|
1234
|
-
const unpackLine = input.unpackStatus === 'ok' ? 'unpack ok\n' : `unpack ${input.unpackStatus}\n`;
|
|
1235
|
-
lines.push(encodePktLine(unpackLine));
|
|
1236
|
-
// Ref status lines
|
|
1237
|
-
for (const result of input.refResults) {
|
|
1238
|
-
if (result.success) {
|
|
1239
|
-
let line = `ok ${result.refName}`;
|
|
1240
|
-
if (result.forced) {
|
|
1241
|
-
line += ' forced';
|
|
1242
|
-
}
|
|
1243
|
-
lines.push(encodePktLine(line + '\n'));
|
|
1244
|
-
}
|
|
1245
|
-
else {
|
|
1246
|
-
lines.push(encodePktLine(`ng ${result.refName} ${result.error || 'failed'}\n`));
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
// End with flush
|
|
1250
|
-
lines.push(FLUSH_PKT);
|
|
1251
|
-
return lines.join('');
|
|
1252
|
-
}
|
|
1253
|
-
/**
|
|
1254
|
-
* Format rejection message.
|
|
1255
|
-
*
|
|
1256
|
-
* @description
|
|
1257
|
-
* Creates a rejection message in the appropriate format based
|
|
1258
|
-
* on the client's capabilities (side-band or report-status).
|
|
1259
|
-
*
|
|
1260
|
-
* @param refName - Ref that was rejected
|
|
1261
|
-
* @param reason - Reason for rejection
|
|
1262
|
-
* @param options - Formatting options
|
|
1263
|
-
* @returns Formatted rejection message
|
|
1264
|
-
*
|
|
1265
|
-
* @example
|
|
1266
|
-
* ```typescript
|
|
1267
|
-
* // Side-band format
|
|
1268
|
-
* const msg = rejectPush('refs/heads/main', 'protected branch', { sideBand: true })
|
|
1269
|
-
* // Returns Uint8Array with side-band channel 3 message
|
|
1270
|
-
*
|
|
1271
|
-
* // Report-status format
|
|
1272
|
-
* const msg = rejectPush('refs/heads/main', 'protected branch', { reportStatus: true })
|
|
1273
|
-
* // Returns "ng refs/heads/main protected branch"
|
|
1274
|
-
* ```
|
|
1275
|
-
*/
|
|
1276
|
-
export function rejectPush(refName, reason, options) {
|
|
1277
|
-
if (options.sideBand) {
|
|
1278
|
-
// Side-band channel 3 for errors
|
|
1279
|
-
const message = `error: failed to push ${refName}: ${reason}\n`;
|
|
1280
|
-
const data = encoder.encode(message);
|
|
1281
|
-
const totalLength = 4 + 1 + data.length;
|
|
1282
|
-
const hexLength = totalLength.toString(16).padStart(4, '0');
|
|
1283
|
-
const result = new Uint8Array(totalLength);
|
|
1284
|
-
result.set(encoder.encode(hexLength), 0);
|
|
1285
|
-
result[4] = 3; // Error channel
|
|
1286
|
-
result.set(data, 5);
|
|
1287
|
-
return result;
|
|
1288
|
-
}
|
|
1289
|
-
// Report-status format
|
|
1290
|
-
return `ng ${refName} ${reason}`;
|
|
1291
|
-
}
|
|
1292
|
-
// ============================================================================
|
|
1293
|
-
// Full Receive-Pack Handler
|
|
1294
|
-
// ============================================================================
|
|
1295
|
-
/**
|
|
1296
|
-
* Handle complete receive-pack request.
|
|
1297
|
-
*
|
|
1298
|
-
* @description
|
|
1299
|
-
* This is the main entry point that handles the full receive-pack
|
|
1300
|
-
* protocol flow:
|
|
1301
|
-
* 1. Parse request (commands, capabilities, packfile)
|
|
1302
|
-
* 2. Validate and unpack packfile (if present)
|
|
1303
|
-
* 3. Process each ref update command
|
|
1304
|
-
* 4. Return status report (if requested)
|
|
1305
|
-
*
|
|
1306
|
-
* @param session - Receive pack session
|
|
1307
|
-
* @param request - Raw request data
|
|
1308
|
-
* @param store - Object store
|
|
1309
|
-
* @returns Response data (status report or empty)
|
|
1310
|
-
*
|
|
1311
|
-
* @example
|
|
1312
|
-
* ```typescript
|
|
1313
|
-
* const session = createReceiveSession('my-repo')
|
|
1314
|
-
* const response = await handleReceivePack(session, requestBody, store)
|
|
1315
|
-
* // response contains status report if report-status was enabled
|
|
1316
|
-
* ```
|
|
1317
|
-
*/
|
|
1318
|
-
export async function handleReceivePack(session, request, store) {
|
|
1319
|
-
// Parse the request
|
|
1320
|
-
const parsed = parseReceivePackRequest(request);
|
|
1321
|
-
session.commands = parsed.commands;
|
|
1322
|
-
// Merge capabilities from request
|
|
1323
|
-
const requestCaps = parseReceiveCapabilities(parsed.capabilities.join(' '));
|
|
1324
|
-
session.capabilities = { ...session.capabilities, ...requestCaps };
|
|
1325
|
-
// Check if we need to report status
|
|
1326
|
-
const needsReport = session.capabilities.reportStatus || session.capabilities.reportStatusV2;
|
|
1327
|
-
// Validate packfile (if present and needed)
|
|
1328
|
-
let unpackStatus = 'ok';
|
|
1329
|
-
const hasNonDeleteCommands = parsed.commands.some((c) => c.type !== 'delete');
|
|
1330
|
-
if (hasNonDeleteCommands && parsed.packfile.length > 0) {
|
|
1331
|
-
const validation = await validatePackfile(parsed.packfile);
|
|
1332
|
-
if (!validation.valid) {
|
|
1333
|
-
unpackStatus = `error: ${validation.error}`;
|
|
1334
|
-
}
|
|
1335
|
-
else {
|
|
1336
|
-
const unpackResult = await unpackObjects(parsed.packfile, store);
|
|
1337
|
-
if (!unpackResult.success) {
|
|
1338
|
-
unpackStatus = `error: ${unpackResult.error}`;
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
else if (hasNonDeleteCommands && parsed.packfile.length === 0) {
|
|
1343
|
-
// Non-delete command but no packfile - this is OK for some cases
|
|
1344
|
-
// but we should still validate
|
|
1345
|
-
unpackStatus = 'ok';
|
|
1346
|
-
}
|
|
1347
|
-
// Process commands
|
|
1348
|
-
const refResults = [];
|
|
1349
|
-
for (const cmd of parsed.commands) {
|
|
1350
|
-
// Validate ref name
|
|
1351
|
-
if (!validateRefName(cmd.refName)) {
|
|
1352
|
-
refResults.push({
|
|
1353
|
-
refName: cmd.refName,
|
|
1354
|
-
success: false,
|
|
1355
|
-
error: 'invalid ref name',
|
|
1356
|
-
});
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
|
-
// Check current ref state
|
|
1360
|
-
const currentRef = await store.getRef(cmd.refName);
|
|
1361
|
-
const currentSha = currentRef?.sha || ZERO_SHA;
|
|
1362
|
-
// For updates and deletes, verify old SHA matches
|
|
1363
|
-
if (cmd.type !== 'create') {
|
|
1364
|
-
if (currentSha !== cmd.oldSha) {
|
|
1365
|
-
refResults.push({
|
|
1366
|
-
refName: cmd.refName,
|
|
1367
|
-
success: false,
|
|
1368
|
-
error: 'lock failed: ref has been updated',
|
|
1369
|
-
});
|
|
1370
|
-
continue;
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
// Handle delete
|
|
1374
|
-
if (cmd.type === 'delete') {
|
|
1375
|
-
if (!session.capabilities.deleteRefs) {
|
|
1376
|
-
refResults.push({
|
|
1377
|
-
refName: cmd.refName,
|
|
1378
|
-
success: false,
|
|
1379
|
-
error: 'delete-refs not enabled',
|
|
1380
|
-
});
|
|
1381
|
-
continue;
|
|
1382
|
-
}
|
|
1383
|
-
await store.deleteRef(cmd.refName);
|
|
1384
|
-
refResults.push({ refName: cmd.refName, success: true });
|
|
1385
|
-
continue;
|
|
1386
|
-
}
|
|
1387
|
-
// Handle create/update
|
|
1388
|
-
if (cmd.type === 'update') {
|
|
1389
|
-
// Check fast-forward
|
|
1390
|
-
const isFF = await validateFastForward(cmd.oldSha, cmd.newSha, store);
|
|
1391
|
-
if (!isFF) {
|
|
1392
|
-
refResults.push({
|
|
1393
|
-
refName: cmd.refName,
|
|
1394
|
-
success: false,
|
|
1395
|
-
error: 'non-fast-forward update',
|
|
1396
|
-
});
|
|
1397
|
-
continue;
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
// Apply the update
|
|
1401
|
-
await store.setRef(cmd.refName, cmd.newSha);
|
|
1402
|
-
refResults.push({ refName: cmd.refName, success: true });
|
|
1403
|
-
}
|
|
1404
|
-
// Build response
|
|
1405
|
-
if (needsReport) {
|
|
1406
|
-
const statusFormat = session.capabilities.reportStatusV2
|
|
1407
|
-
? formatReportStatusV2({ unpackStatus, refResults })
|
|
1408
|
-
: formatReportStatus({ unpackStatus, refResults });
|
|
1409
|
-
return encoder.encode(statusFormat);
|
|
1410
|
-
}
|
|
1411
|
-
// No report needed
|
|
1412
|
-
return new Uint8Array(0);
|
|
1413
|
-
}
|
|
1414
|
-
//# sourceMappingURL=receive-pack.js.map
|