gitx.do 0.0.3 → 0.1.1
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 +319 -92
- package/dist/cli/commands/add.d.ts +176 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/add.js +979 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/blame.d.ts +1 -1
- package/dist/cli/commands/blame.d.ts.map +1 -1
- package/dist/cli/commands/blame.js +1 -1
- package/dist/cli/commands/blame.js.map +1 -1
- package/dist/cli/commands/branch.d.ts +1 -1
- package/dist/cli/commands/branch.d.ts.map +1 -1
- package/dist/cli/commands/branch.js +2 -2
- package/dist/cli/commands/branch.js.map +1 -1
- package/dist/cli/commands/checkout.d.ts +73 -0
- package/dist/cli/commands/checkout.d.ts.map +1 -0
- package/dist/cli/commands/checkout.js +725 -0
- package/dist/cli/commands/checkout.js.map +1 -0
- package/dist/cli/commands/commit.d.ts.map +1 -1
- package/dist/cli/commands/commit.js +22 -2
- package/dist/cli/commands/commit.js.map +1 -1
- package/dist/cli/commands/diff.d.ts +4 -4
- package/dist/cli/commands/diff.d.ts.map +1 -1
- package/dist/cli/commands/diff.js +9 -8
- package/dist/cli/commands/diff.js.map +1 -1
- package/dist/cli/commands/log.d.ts +1 -1
- package/dist/cli/commands/log.d.ts.map +1 -1
- package/dist/cli/commands/log.js +1 -1
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/merge.d.ts +106 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +852 -0
- package/dist/cli/commands/merge.js.map +1 -0
- package/dist/cli/commands/review.d.ts +1 -1
- package/dist/cli/commands/review.d.ts.map +1 -1
- package/dist/cli/commands/review.js +26 -1
- package/dist/cli/commands/review.js.map +1 -1
- package/dist/cli/commands/stash.d.ts +157 -0
- package/dist/cli/commands/stash.d.ts.map +1 -0
- package/dist/cli/commands/stash.js +655 -0
- package/dist/cli/commands/stash.js.map +1 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +1 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/web.d.ts.map +1 -1
- package/dist/cli/commands/web.js +3 -2
- package/dist/cli/commands/web.js.map +1 -1
- package/dist/cli/fs-adapter.d.ts.map +1 -1
- package/dist/cli/fs-adapter.js +3 -5
- package/dist/cli/fs-adapter.js.map +1 -1
- package/dist/cli/fsx-cli-adapter.d.ts +359 -0
- package/dist/cli/fsx-cli-adapter.d.ts.map +1 -0
- package/dist/cli/fsx-cli-adapter.js +619 -0
- package/dist/cli/fsx-cli-adapter.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +68 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ui/components/DiffView.d.ts +7 -2
- package/dist/cli/ui/components/DiffView.d.ts.map +1 -1
- package/dist/cli/ui/components/DiffView.js.map +1 -1
- package/dist/cli/ui/components/ErrorDisplay.d.ts +6 -2
- package/dist/cli/ui/components/ErrorDisplay.d.ts.map +1 -1
- package/dist/cli/ui/components/ErrorDisplay.js.map +1 -1
- package/dist/cli/ui/components/FuzzySearch.d.ts +8 -2
- package/dist/cli/ui/components/FuzzySearch.d.ts.map +1 -1
- package/dist/cli/ui/components/FuzzySearch.js.map +1 -1
- package/dist/cli/ui/components/LoadingSpinner.d.ts +6 -2
- package/dist/cli/ui/components/LoadingSpinner.d.ts.map +1 -1
- package/dist/cli/ui/components/LoadingSpinner.js.map +1 -1
- package/dist/cli/ui/components/NavigationList.d.ts +7 -2
- package/dist/cli/ui/components/NavigationList.d.ts.map +1 -1
- package/dist/cli/ui/components/NavigationList.js.map +1 -1
- package/dist/cli/ui/components/ScrollableContent.d.ts +7 -2
- package/dist/cli/ui/components/ScrollableContent.d.ts.map +1 -1
- package/dist/cli/ui/components/ScrollableContent.js.map +1 -1
- package/dist/cli/ui/terminal-ui.d.ts +42 -9
- package/dist/cli/ui/terminal-ui.d.ts.map +1 -1
- package/dist/cli/ui/terminal-ui.js.map +1 -1
- package/dist/do/BashModule.d.ts +871 -0
- package/dist/do/BashModule.d.ts.map +1 -0
- package/dist/do/BashModule.js +1143 -0
- package/dist/do/BashModule.js.map +1 -0
- package/dist/do/FsModule.d.ts +612 -0
- package/dist/do/FsModule.d.ts.map +1 -0
- package/dist/do/FsModule.js +1120 -0
- package/dist/do/FsModule.js.map +1 -0
- package/dist/do/GitModule.d.ts +635 -0
- package/dist/do/GitModule.d.ts.map +1 -0
- package/dist/do/GitModule.js +784 -0
- package/dist/do/GitModule.js.map +1 -0
- package/dist/do/GitRepoDO.d.ts +281 -0
- package/dist/do/GitRepoDO.d.ts.map +1 -0
- package/dist/do/GitRepoDO.js +479 -0
- package/dist/do/GitRepoDO.js.map +1 -0
- package/dist/do/bash-ast.d.ts +246 -0
- package/dist/do/bash-ast.d.ts.map +1 -0
- package/dist/do/bash-ast.js +888 -0
- package/dist/do/bash-ast.js.map +1 -0
- package/dist/do/container-executor.d.ts +491 -0
- package/dist/do/container-executor.d.ts.map +1 -0
- package/dist/do/container-executor.js +731 -0
- package/dist/do/container-executor.js.map +1 -0
- package/dist/do/index.d.ts +53 -0
- package/dist/do/index.d.ts.map +1 -0
- package/dist/do/index.js +91 -0
- package/dist/do/index.js.map +1 -0
- package/dist/do/tiered-storage.d.ts +403 -0
- package/dist/do/tiered-storage.d.ts.map +1 -0
- package/dist/do/tiered-storage.js +689 -0
- package/dist/do/tiered-storage.js.map +1 -0
- package/dist/do/withBash.d.ts +231 -0
- package/dist/do/withBash.d.ts.map +1 -0
- package/dist/do/withBash.js +244 -0
- package/dist/do/withBash.js.map +1 -0
- package/dist/do/withFs.d.ts +237 -0
- package/dist/do/withFs.d.ts.map +1 -0
- package/dist/do/withFs.js +387 -0
- package/dist/do/withFs.js.map +1 -0
- package/dist/do/withGit.d.ts +180 -0
- package/dist/do/withGit.d.ts.map +1 -0
- package/dist/do/withGit.js +271 -0
- package/dist/do/withGit.js.map +1 -0
- package/dist/durable-object/object-store.d.ts +157 -15
- package/dist/durable-object/object-store.d.ts.map +1 -1
- package/dist/durable-object/object-store.js +435 -47
- package/dist/durable-object/object-store.js.map +1 -1
- package/dist/durable-object/schema.d.ts +12 -1
- package/dist/durable-object/schema.d.ts.map +1 -1
- package/dist/durable-object/schema.js +87 -2
- package/dist/durable-object/schema.js.map +1 -1
- package/dist/index.d.ts +84 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/sandbox/miniflare-evaluator.d.ts +22 -0
- package/dist/mcp/sandbox/miniflare-evaluator.d.ts.map +1 -0
- package/dist/mcp/sandbox/miniflare-evaluator.js +140 -0
- package/dist/mcp/sandbox/miniflare-evaluator.js.map +1 -0
- package/dist/mcp/sandbox/object-store-proxy.d.ts +32 -0
- package/dist/mcp/sandbox/object-store-proxy.d.ts.map +1 -0
- package/dist/mcp/sandbox/object-store-proxy.js +30 -0
- package/dist/mcp/sandbox/object-store-proxy.js.map +1 -0
- package/dist/mcp/sandbox/template.d.ts +17 -0
- package/dist/mcp/sandbox/template.d.ts.map +1 -0
- package/dist/mcp/sandbox/template.js +71 -0
- package/dist/mcp/sandbox/template.js.map +1 -0
- package/dist/mcp/sandbox.d.ts.map +1 -1
- package/dist/mcp/sandbox.js +16 -4
- package/dist/mcp/sandbox.js.map +1 -1
- package/dist/mcp/tools/do.d.ts +32 -0
- package/dist/mcp/tools/do.d.ts.map +1 -0
- package/dist/mcp/tools/do.js +117 -0
- package/dist/mcp/tools/do.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1258 -22
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pack/delta.d.ts +8 -0
- package/dist/pack/delta.d.ts.map +1 -1
- package/dist/pack/delta.js +241 -30
- package/dist/pack/delta.js.map +1 -1
- package/dist/refs/branch.d.ts +38 -25
- package/dist/refs/branch.d.ts.map +1 -1
- package/dist/refs/branch.js +421 -94
- package/dist/refs/branch.js.map +1 -1
- package/dist/refs/storage.d.ts +77 -5
- package/dist/refs/storage.d.ts.map +1 -1
- package/dist/refs/storage.js +193 -43
- package/dist/refs/storage.js.map +1 -1
- package/dist/refs/tag.d.ts +44 -24
- package/dist/refs/tag.d.ts.map +1 -1
- package/dist/refs/tag.js +411 -70
- package/dist/refs/tag.js.map +1 -1
- package/dist/storage/backend.d.ts +425 -0
- package/dist/storage/backend.d.ts.map +1 -0
- package/dist/storage/backend.js +41 -0
- package/dist/storage/backend.js.map +1 -0
- package/dist/storage/fsx-adapter.d.ts +204 -0
- package/dist/storage/fsx-adapter.d.ts.map +1 -0
- package/dist/storage/fsx-adapter.js +518 -0
- package/dist/storage/fsx-adapter.js.map +1 -0
- package/dist/storage/r2-pack.d.ts.map +1 -1
- package/dist/storage/r2-pack.js +4 -1
- package/dist/storage/r2-pack.js.map +1 -1
- package/dist/tiered/cdc-pipeline.js +3 -3
- package/dist/tiered/cdc-pipeline.js.map +1 -1
- package/dist/tiered/migration.d.ts.map +1 -1
- package/dist/tiered/migration.js +4 -1
- package/dist/tiered/migration.js.map +1 -1
- package/dist/types/capability.d.ts +1385 -0
- package/dist/types/capability.d.ts.map +1 -0
- package/dist/types/capability.js +36 -0
- package/dist/types/capability.js.map +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/interfaces.d.ts +673 -0
- package/dist/types/interfaces.d.ts.map +1 -0
- package/dist/types/interfaces.js +26 -0
- package/dist/types/interfaces.js.map +1 -0
- package/dist/types/objects.d.ts +182 -0
- package/dist/types/objects.d.ts.map +1 -1
- package/dist/types/objects.js +249 -4
- package/dist/types/objects.js.map +1 -1
- package/dist/types/storage.d.ts +114 -0
- package/dist/types/storage.d.ts.map +1 -1
- package/dist/types/storage.js +160 -1
- package/dist/types/storage.js.map +1 -1
- package/dist/types/worker-loader.d.ts +60 -0
- package/dist/types/worker-loader.d.ts.map +1 -0
- package/dist/types/worker-loader.js +62 -0
- package/dist/types/worker-loader.js.map +1 -0
- package/dist/utils/hash.d.ts +126 -80
- package/dist/utils/hash.d.ts.map +1 -1
- package/dist/utils/hash.js +191 -100
- package/dist/utils/hash.js.map +1 -1
- package/dist/utils/sha1.d.ts +206 -0
- package/dist/utils/sha1.d.ts.map +1 -1
- package/dist/utils/sha1.js +405 -0
- package/dist/utils/sha1.js.map +1 -1
- package/dist/wire/path-security.d.ts +157 -0
- package/dist/wire/path-security.d.ts.map +1 -0
- package/dist/wire/path-security.js +307 -0
- package/dist/wire/path-security.js.map +1 -0
- package/dist/wire/receive-pack.d.ts +7 -0
- package/dist/wire/receive-pack.d.ts.map +1 -1
- package/dist/wire/receive-pack.js +29 -1
- package/dist/wire/receive-pack.js.map +1 -1
- package/dist/wire/upload-pack.d.ts.map +1 -1
- package/dist/wire/upload-pack.js +4 -1
- package/dist/wire/upload-pack.js.map +1 -1
- package/package.json +10 -1
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* - Content-addressable storage using SHA-1 hashes
|
|
10
10
|
* - Write-ahead logging (WAL) for durability
|
|
11
11
|
* - Object index for tiered storage support
|
|
12
|
-
* - Batch operations for efficiency
|
|
12
|
+
* - Batch operations for efficiency with transaction support
|
|
13
|
+
* - LRU caching for hot tier objects
|
|
14
|
+
* - Metrics and logging infrastructure
|
|
13
15
|
* - Typed accessors for each Git object type
|
|
14
16
|
*
|
|
15
17
|
* @module durable-object/object-store
|
|
@@ -18,23 +20,39 @@
|
|
|
18
20
|
* ```typescript
|
|
19
21
|
* import { ObjectStore } from './durable-object/object-store'
|
|
20
22
|
*
|
|
21
|
-
* const store = new ObjectStore(durableObjectStorage
|
|
23
|
+
* const store = new ObjectStore(durableObjectStorage, {
|
|
24
|
+
* cacheMaxCount: 1000,
|
|
25
|
+
* cacheMaxBytes: 50 * 1024 * 1024, // 50MB
|
|
26
|
+
* enableMetrics: true
|
|
27
|
+
* })
|
|
22
28
|
*
|
|
23
29
|
* // Store a blob
|
|
24
30
|
* const content = new TextEncoder().encode('Hello, World!')
|
|
25
31
|
* const sha = await store.putObject('blob', content)
|
|
26
32
|
*
|
|
27
|
-
* // Retrieve it
|
|
33
|
+
* // Retrieve it (cached on second access)
|
|
28
34
|
* const obj = await store.getObject(sha)
|
|
29
35
|
* console.log(obj?.type, obj?.size)
|
|
30
36
|
*
|
|
31
37
|
* // Get typed object
|
|
32
38
|
* const blob = await store.getBlobObject(sha)
|
|
39
|
+
*
|
|
40
|
+
* // Get metrics
|
|
41
|
+
* const metrics = store.getMetrics()
|
|
42
|
+
* console.log(`Cache hit rate: ${metrics.cacheHitRate}%`)
|
|
33
43
|
* ```
|
|
34
44
|
*/
|
|
45
|
+
import { LRUCache } from '../storage/lru-cache';
|
|
46
|
+
import { isValidMode, isValidSha } from '../types/objects';
|
|
47
|
+
// Reserved for future validation
|
|
48
|
+
import { validateTreeEntry as _validateTreeEntry } from '../types/objects';
|
|
49
|
+
void _validateTreeEntry;
|
|
35
50
|
import { hashObject } from '../utils/hash';
|
|
36
51
|
const encoder = new TextEncoder();
|
|
37
52
|
const decoder = new TextDecoder();
|
|
53
|
+
// Default cache configuration
|
|
54
|
+
const DEFAULT_CACHE_MAX_COUNT = 500;
|
|
55
|
+
const DEFAULT_CACHE_MAX_BYTES = 25 * 1024 * 1024; // 25MB
|
|
38
56
|
// ============================================================================
|
|
39
57
|
// ObjectStore Class
|
|
40
58
|
// ============================================================================
|
|
@@ -66,13 +84,72 @@ const decoder = new TextDecoder();
|
|
|
66
84
|
*/
|
|
67
85
|
export class ObjectStore {
|
|
68
86
|
storage;
|
|
87
|
+
cache;
|
|
88
|
+
options;
|
|
89
|
+
logger;
|
|
90
|
+
backend;
|
|
91
|
+
// Metrics tracking
|
|
92
|
+
_reads = 0;
|
|
93
|
+
_writes = 0;
|
|
94
|
+
_deletes = 0;
|
|
95
|
+
_bytesWritten = 0;
|
|
96
|
+
_bytesRead = 0;
|
|
97
|
+
_totalWriteLatency = 0;
|
|
98
|
+
_totalReadLatency = 0;
|
|
99
|
+
_batchOperations = 0;
|
|
100
|
+
_batchObjectsTotal = 0;
|
|
69
101
|
/**
|
|
70
102
|
* Create a new ObjectStore.
|
|
71
103
|
*
|
|
72
104
|
* @param storage - Durable Object storage interface with SQL support
|
|
105
|
+
* @param options - Configuration options for caching, metrics, logging, and backend
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* // Basic usage (SQLite backend)
|
|
110
|
+
* const store = new ObjectStore(storage)
|
|
111
|
+
*
|
|
112
|
+
* // With caching and metrics
|
|
113
|
+
* const store = new ObjectStore(storage, {
|
|
114
|
+
* cacheMaxCount: 1000,
|
|
115
|
+
* cacheMaxBytes: 50 * 1024 * 1024,
|
|
116
|
+
* enableMetrics: true,
|
|
117
|
+
* logger: console
|
|
118
|
+
* })
|
|
119
|
+
*
|
|
120
|
+
* // With StorageBackend abstraction
|
|
121
|
+
* const store = new ObjectStore(storage, {
|
|
122
|
+
* backend: fsBackend
|
|
123
|
+
* })
|
|
124
|
+
* ```
|
|
73
125
|
*/
|
|
74
|
-
constructor(storage) {
|
|
126
|
+
constructor(storage, options) {
|
|
75
127
|
this.storage = storage;
|
|
128
|
+
this.options = options ?? {};
|
|
129
|
+
this.logger = options?.logger;
|
|
130
|
+
this.backend = options?.backend ?? null;
|
|
131
|
+
// Initialize LRU cache for hot tier objects
|
|
132
|
+
this.cache = new LRUCache({
|
|
133
|
+
maxCount: options?.cacheMaxCount ?? DEFAULT_CACHE_MAX_COUNT,
|
|
134
|
+
maxBytes: options?.cacheMaxBytes ?? DEFAULT_CACHE_MAX_BYTES,
|
|
135
|
+
defaultTTL: options?.cacheTTL,
|
|
136
|
+
sizeCalculator: (obj) => obj.data.byteLength + 100, // 100 bytes overhead for metadata
|
|
137
|
+
onEvict: (key, _value, reason) => {
|
|
138
|
+
this.log('debug', `Cache eviction: ${key} (reason: ${reason})`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Log a message if logger is configured.
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
log(level, message, ...args) {
|
|
147
|
+
if (!this.logger)
|
|
148
|
+
return;
|
|
149
|
+
const logFn = this.logger[level];
|
|
150
|
+
if (logFn) {
|
|
151
|
+
logFn.call(this.logger, `[ObjectStore] ${message}`, ...args);
|
|
152
|
+
}
|
|
76
153
|
}
|
|
77
154
|
/**
|
|
78
155
|
* Store a raw object and return its SHA.
|
|
@@ -81,6 +158,7 @@ export class ObjectStore {
|
|
|
81
158
|
* Computes the SHA-1 hash of the object in Git format (type + size + content),
|
|
82
159
|
* logs the operation to WAL, stores the object, and updates the object index.
|
|
83
160
|
* If an object with the same SHA already exists, it is replaced (idempotent).
|
|
161
|
+
* The object is also added to the LRU cache for fast subsequent reads.
|
|
84
162
|
*
|
|
85
163
|
* @param type - Object type ('blob', 'tree', 'commit', 'tag')
|
|
86
164
|
* @param data - Raw object content (without Git header)
|
|
@@ -94,14 +172,55 @@ export class ObjectStore {
|
|
|
94
172
|
* ```
|
|
95
173
|
*/
|
|
96
174
|
async putObject(type, data) {
|
|
175
|
+
const startTime = this.options.enableMetrics ? Date.now() : 0;
|
|
176
|
+
// Delegate to backend if available
|
|
177
|
+
if (this.backend) {
|
|
178
|
+
const sha = await this.backend.putObject(type, data);
|
|
179
|
+
// Add to cache for fast subsequent reads
|
|
180
|
+
const storedObject = {
|
|
181
|
+
sha,
|
|
182
|
+
type,
|
|
183
|
+
size: data.length,
|
|
184
|
+
data,
|
|
185
|
+
createdAt: Date.now()
|
|
186
|
+
};
|
|
187
|
+
this.cache.set(sha, storedObject);
|
|
188
|
+
// Update metrics
|
|
189
|
+
if (this.options.enableMetrics) {
|
|
190
|
+
this._writes++;
|
|
191
|
+
this._bytesWritten += data.length;
|
|
192
|
+
this._totalWriteLatency += Date.now() - startTime;
|
|
193
|
+
}
|
|
194
|
+
return sha;
|
|
195
|
+
}
|
|
196
|
+
// Existing SQLite implementation as fallback
|
|
97
197
|
// Compute SHA-1 hash using git object format: "type size\0content"
|
|
98
198
|
const sha = await hashObject(type, data);
|
|
199
|
+
this.log('debug', `Storing ${type} object: ${sha} (${data.length} bytes)`);
|
|
99
200
|
// Log to WAL first
|
|
100
201
|
await this.logToWAL('PUT', sha, type, data);
|
|
202
|
+
const now = Date.now();
|
|
101
203
|
// Store the object
|
|
102
|
-
this.storage.sql.exec('INSERT OR REPLACE INTO objects (sha, type, size, data, created_at) VALUES (?, ?, ?, ?, ?)', sha, type, data.length, data,
|
|
204
|
+
this.storage.sql.exec('INSERT OR REPLACE INTO objects (sha, type, size, data, created_at) VALUES (?, ?, ?, ?, ?)', sha, type, data.length, data, now);
|
|
103
205
|
// Update object index
|
|
104
|
-
this.storage.sql.exec('INSERT OR REPLACE INTO object_index (sha, tier,
|
|
206
|
+
this.storage.sql.exec('INSERT OR REPLACE INTO object_index (sha, tier, pack_id, offset, size, type, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', sha, 'hot', null, // pack_id is null for hot tier
|
|
207
|
+
null, // offset is null for hot tier
|
|
208
|
+
data.length, type, now);
|
|
209
|
+
// Add to cache for fast subsequent reads
|
|
210
|
+
const storedObject = {
|
|
211
|
+
sha,
|
|
212
|
+
type,
|
|
213
|
+
size: data.length,
|
|
214
|
+
data,
|
|
215
|
+
createdAt: now
|
|
216
|
+
};
|
|
217
|
+
this.cache.set(sha, storedObject);
|
|
218
|
+
// Update metrics
|
|
219
|
+
if (this.options.enableMetrics) {
|
|
220
|
+
this._writes++;
|
|
221
|
+
this._bytesWritten += data.length;
|
|
222
|
+
this._totalWriteLatency += Date.now() - startTime;
|
|
223
|
+
}
|
|
105
224
|
return sha;
|
|
106
225
|
}
|
|
107
226
|
/**
|
|
@@ -124,11 +243,44 @@ export class ObjectStore {
|
|
|
124
243
|
* ```
|
|
125
244
|
*/
|
|
126
245
|
async putTreeObject(entries) {
|
|
127
|
-
//
|
|
246
|
+
// Validate all entries first
|
|
247
|
+
const seenNames = new Set();
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
// Check for invalid names: empty, '.', '..', contains '/' or null byte
|
|
250
|
+
if (!entry.name || entry.name === '.' || entry.name === '..') {
|
|
251
|
+
throw new Error(`Invalid entry name: "${entry.name}". Entry names cannot be empty, ".", or ".."`);
|
|
252
|
+
}
|
|
253
|
+
if (entry.name.includes('/')) {
|
|
254
|
+
throw new Error(`Invalid entry name: "${entry.name}". Entry names cannot contain path separators`);
|
|
255
|
+
}
|
|
256
|
+
if (entry.name.includes('\0')) {
|
|
257
|
+
throw new Error(`Invalid entry name: "${entry.name}". Entry names cannot contain null bytes`);
|
|
258
|
+
}
|
|
259
|
+
// Check for duplicate names
|
|
260
|
+
if (seenNames.has(entry.name)) {
|
|
261
|
+
throw new Error(`Duplicate entry name: "${entry.name}". Tree entries must have unique names`);
|
|
262
|
+
}
|
|
263
|
+
seenNames.add(entry.name);
|
|
264
|
+
// Validate mode
|
|
265
|
+
if (!isValidMode(entry.mode)) {
|
|
266
|
+
throw new Error(`Invalid mode: "${entry.mode}". Valid modes: 100644, 100755, 040000, 120000, 160000`);
|
|
267
|
+
}
|
|
268
|
+
// Validate SHA
|
|
269
|
+
if (!isValidSha(entry.sha)) {
|
|
270
|
+
throw new Error(`Invalid SHA: "${entry.sha}". Must be 40 lowercase hex characters`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Sort entries by name using ASCII byte-order comparison
|
|
274
|
+
// Git sorts directories as if they have trailing slashes for comparison
|
|
128
275
|
const sortedEntries = [...entries].sort((a, b) => {
|
|
129
276
|
const aName = a.mode === '040000' ? a.name + '/' : a.name;
|
|
130
277
|
const bName = b.mode === '040000' ? b.name + '/' : b.name;
|
|
131
|
-
|
|
278
|
+
// Use simple comparison for ASCII byte order
|
|
279
|
+
if (aName < bName)
|
|
280
|
+
return -1;
|
|
281
|
+
if (aName > bName)
|
|
282
|
+
return 1;
|
|
283
|
+
return 0;
|
|
132
284
|
});
|
|
133
285
|
// Build tree content (without header)
|
|
134
286
|
const entryParts = [];
|
|
@@ -230,7 +382,9 @@ export class ObjectStore {
|
|
|
230
382
|
lines.push(`object ${tag.object}`);
|
|
231
383
|
lines.push(`type ${tag.objectType}`);
|
|
232
384
|
lines.push(`tag ${tag.name}`);
|
|
233
|
-
|
|
385
|
+
if (tag.tagger) {
|
|
386
|
+
lines.push(`tagger ${tag.tagger.name} <${tag.tagger.email}> ${tag.tagger.timestamp} ${tag.tagger.timezone}`);
|
|
387
|
+
}
|
|
234
388
|
lines.push('');
|
|
235
389
|
lines.push(tag.message);
|
|
236
390
|
const content = encoder.encode(lines.join('\n'));
|
|
@@ -240,8 +394,8 @@ export class ObjectStore {
|
|
|
240
394
|
* Retrieve an object by SHA.
|
|
241
395
|
*
|
|
242
396
|
* @description
|
|
243
|
-
* Fetches an object from the
|
|
244
|
-
* Returns null if the object doesn't exist or if the SHA is invalid.
|
|
397
|
+
* Fetches an object from the LRU cache first, falling back to the database
|
|
398
|
+
* if not cached. Returns null if the object doesn't exist or if the SHA is invalid.
|
|
245
399
|
*
|
|
246
400
|
* @param sha - 40-character SHA-1 hash
|
|
247
401
|
* @returns The stored object or null if not found
|
|
@@ -255,21 +409,75 @@ export class ObjectStore {
|
|
|
255
409
|
* ```
|
|
256
410
|
*/
|
|
257
411
|
async getObject(sha) {
|
|
412
|
+
const startTime = this.options.enableMetrics ? Date.now() : 0;
|
|
258
413
|
if (!sha || sha.length < 4) {
|
|
259
414
|
return null;
|
|
260
415
|
}
|
|
416
|
+
// Check cache first (fast path)
|
|
417
|
+
const cached = this.cache.get(sha);
|
|
418
|
+
if (cached) {
|
|
419
|
+
this.log('debug', `Cache hit for object: ${sha}`);
|
|
420
|
+
if (this.options.enableMetrics) {
|
|
421
|
+
this._reads++;
|
|
422
|
+
this._bytesRead += cached.size;
|
|
423
|
+
this._totalReadLatency += Date.now() - startTime;
|
|
424
|
+
}
|
|
425
|
+
return cached;
|
|
426
|
+
}
|
|
427
|
+
// Delegate to backend if available
|
|
428
|
+
if (this.backend) {
|
|
429
|
+
const result = await this.backend.getObject(sha);
|
|
430
|
+
if (!result) {
|
|
431
|
+
this.log('debug', `Object not found: ${sha}`);
|
|
432
|
+
if (this.options.enableMetrics) {
|
|
433
|
+
this._reads++;
|
|
434
|
+
this._totalReadLatency += Date.now() - startTime;
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const obj = {
|
|
439
|
+
sha,
|
|
440
|
+
type: result.type,
|
|
441
|
+
size: result.content.length,
|
|
442
|
+
data: result.content,
|
|
443
|
+
createdAt: Date.now()
|
|
444
|
+
};
|
|
445
|
+
// Add to cache for subsequent reads
|
|
446
|
+
this.cache.set(sha, obj);
|
|
447
|
+
if (this.options.enableMetrics) {
|
|
448
|
+
this._reads++;
|
|
449
|
+
this._bytesRead += obj.size;
|
|
450
|
+
this._totalReadLatency += Date.now() - startTime;
|
|
451
|
+
}
|
|
452
|
+
return obj;
|
|
453
|
+
}
|
|
454
|
+
// Existing SQLite implementation as fallback
|
|
455
|
+
// Fall back to database
|
|
261
456
|
const result = this.storage.sql.exec('SELECT sha, type, size, data, created_at as createdAt FROM objects WHERE sha = ?', sha);
|
|
262
457
|
const rows = result.toArray();
|
|
263
458
|
if (rows.length === 0) {
|
|
459
|
+
this.log('debug', `Object not found: ${sha}`);
|
|
460
|
+
if (this.options.enableMetrics) {
|
|
461
|
+
this._reads++;
|
|
462
|
+
this._totalReadLatency += Date.now() - startTime;
|
|
463
|
+
}
|
|
264
464
|
return null;
|
|
265
465
|
}
|
|
266
|
-
|
|
466
|
+
const obj = rows[0];
|
|
467
|
+
// Add to cache for subsequent reads
|
|
468
|
+
this.cache.set(sha, obj);
|
|
469
|
+
if (this.options.enableMetrics) {
|
|
470
|
+
this._reads++;
|
|
471
|
+
this._bytesRead += obj.size;
|
|
472
|
+
this._totalReadLatency += Date.now() - startTime;
|
|
473
|
+
}
|
|
474
|
+
return obj;
|
|
267
475
|
}
|
|
268
476
|
/**
|
|
269
477
|
* Delete an object by SHA.
|
|
270
478
|
*
|
|
271
479
|
* @description
|
|
272
|
-
* Removes an object from
|
|
480
|
+
* Removes an object from the cache, objects table, and the object index.
|
|
273
481
|
* The operation is logged to WAL. Returns false if the object doesn't exist.
|
|
274
482
|
*
|
|
275
483
|
* **Warning**: Deleting objects that are still referenced by other objects
|
|
@@ -287,17 +495,42 @@ export class ObjectStore {
|
|
|
287
495
|
* ```
|
|
288
496
|
*/
|
|
289
497
|
async deleteObject(sha) {
|
|
498
|
+
// Delegate to backend if available
|
|
499
|
+
if (this.backend) {
|
|
500
|
+
// Check if object exists first via backend
|
|
501
|
+
const exists = await this.backend.hasObject(sha);
|
|
502
|
+
if (!exists) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
this.log('debug', `Deleting object via backend: ${sha}`);
|
|
506
|
+
await this.backend.deleteObject(sha);
|
|
507
|
+
// Remove from cache
|
|
508
|
+
this.cache.delete(sha);
|
|
509
|
+
// Update metrics
|
|
510
|
+
if (this.options.enableMetrics) {
|
|
511
|
+
this._deletes++;
|
|
512
|
+
}
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
// Existing SQLite implementation as fallback
|
|
290
516
|
// Check if object exists first
|
|
291
517
|
const exists = await this.hasObject(sha);
|
|
292
518
|
if (!exists) {
|
|
293
519
|
return false;
|
|
294
520
|
}
|
|
521
|
+
this.log('debug', `Deleting object: ${sha}`);
|
|
295
522
|
// Log to WAL
|
|
296
523
|
await this.logToWAL('DELETE', sha, 'blob', new Uint8Array(0));
|
|
297
524
|
// Delete from objects table
|
|
298
525
|
this.storage.sql.exec('DELETE FROM objects WHERE sha = ?', sha);
|
|
299
526
|
// Delete from object index
|
|
300
527
|
this.storage.sql.exec('DELETE FROM object_index WHERE sha = ?', sha);
|
|
528
|
+
// Remove from cache
|
|
529
|
+
this.cache.delete(sha);
|
|
530
|
+
// Update metrics
|
|
531
|
+
if (this.options.enableMetrics) {
|
|
532
|
+
this._deletes++;
|
|
533
|
+
}
|
|
301
534
|
return true;
|
|
302
535
|
}
|
|
303
536
|
/**
|
|
@@ -320,6 +553,15 @@ export class ObjectStore {
|
|
|
320
553
|
if (!sha || sha.length < 4) {
|
|
321
554
|
return false;
|
|
322
555
|
}
|
|
556
|
+
// Check cache first (fast path)
|
|
557
|
+
if (this.cache.has(sha)) {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
// Delegate to backend if available
|
|
561
|
+
if (this.backend) {
|
|
562
|
+
return this.backend.hasObject(sha);
|
|
563
|
+
}
|
|
564
|
+
// Existing SQLite implementation as fallback
|
|
323
565
|
// Use getObject and check for null - this works better with the mock
|
|
324
566
|
const obj = await this.getObject(sha);
|
|
325
567
|
return obj !== null;
|
|
@@ -345,11 +587,14 @@ export class ObjectStore {
|
|
|
345
587
|
* ```
|
|
346
588
|
*/
|
|
347
589
|
async verifyObject(sha) {
|
|
348
|
-
|
|
349
|
-
|
|
590
|
+
// Read directly from storage (bypass cache) to verify actual stored data
|
|
591
|
+
const result = this.storage.sql.exec('SELECT type, data FROM objects WHERE sha = ?', sha);
|
|
592
|
+
const rows = result.toArray();
|
|
593
|
+
if (rows.length === 0) {
|
|
350
594
|
return false;
|
|
351
595
|
}
|
|
352
|
-
const
|
|
596
|
+
const obj = rows[0];
|
|
597
|
+
const computedSha = await hashObject(obj.type, new Uint8Array(obj.data));
|
|
353
598
|
return computedSha === sha;
|
|
354
599
|
}
|
|
355
600
|
/**
|
|
@@ -393,12 +638,14 @@ export class ObjectStore {
|
|
|
393
638
|
return obj?.size ?? null;
|
|
394
639
|
}
|
|
395
640
|
/**
|
|
396
|
-
* Store multiple objects in a batch.
|
|
641
|
+
* Store multiple objects in a batch using a single transaction.
|
|
397
642
|
*
|
|
398
643
|
* @description
|
|
399
|
-
* Stores multiple objects
|
|
400
|
-
*
|
|
401
|
-
*
|
|
644
|
+
* Stores multiple objects atomically within a single SQLite transaction.
|
|
645
|
+
* This is more efficient than individual puts for bulk operations as it:
|
|
646
|
+
* - Reduces the number of disk flushes
|
|
647
|
+
* - Ensures atomic writes (all-or-nothing)
|
|
648
|
+
* - Batches WAL entries for better performance
|
|
402
649
|
*
|
|
403
650
|
* @param objects - Array of objects to store
|
|
404
651
|
* @returns Array of SHA-1 hashes in the same order as input
|
|
@@ -412,19 +659,83 @@ export class ObjectStore {
|
|
|
412
659
|
* ```
|
|
413
660
|
*/
|
|
414
661
|
async putObjects(objects) {
|
|
662
|
+
if (objects.length === 0) {
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
// For single objects, delegate to putObject
|
|
666
|
+
if (objects.length === 1) {
|
|
667
|
+
const sha = await this.putObject(objects[0].type, objects[0].data);
|
|
668
|
+
return [sha];
|
|
669
|
+
}
|
|
670
|
+
const startTime = this.options.enableMetrics ? Date.now() : 0;
|
|
415
671
|
const shas = [];
|
|
672
|
+
const now = Date.now();
|
|
673
|
+
let totalBytes = 0;
|
|
674
|
+
this.log('info', `Starting batch write of ${objects.length} objects`);
|
|
675
|
+
// Pre-compute all SHA hashes (CPU-bound, before transaction)
|
|
676
|
+
const objectsWithSha = [];
|
|
416
677
|
for (const obj of objects) {
|
|
417
|
-
const sha = await
|
|
678
|
+
const sha = await hashObject(obj.type, obj.data);
|
|
679
|
+
objectsWithSha.push({ sha, type: obj.type, data: obj.data });
|
|
418
680
|
shas.push(sha);
|
|
681
|
+
totalBytes += obj.data.length;
|
|
682
|
+
}
|
|
683
|
+
// Begin transaction for atomic batch write
|
|
684
|
+
this.storage.sql.exec('BEGIN TRANSACTION');
|
|
685
|
+
try {
|
|
686
|
+
for (const { sha, type, data } of objectsWithSha) {
|
|
687
|
+
// Log batch operation to WAL (single entry for the batch)
|
|
688
|
+
const payload = encoder.encode(JSON.stringify({
|
|
689
|
+
sha,
|
|
690
|
+
type,
|
|
691
|
+
timestamp: now,
|
|
692
|
+
batchSize: objects.length
|
|
693
|
+
}));
|
|
694
|
+
this.storage.sql.exec('INSERT INTO wal (operation, payload, created_at, flushed) VALUES (?, ?, ?, 0)', 'BATCH_PUT', payload, now);
|
|
695
|
+
// Store the object
|
|
696
|
+
this.storage.sql.exec('INSERT OR REPLACE INTO objects (sha, type, size, data, created_at) VALUES (?, ?, ?, ?, ?)', sha, type, data.length, data, now);
|
|
697
|
+
// Update object index
|
|
698
|
+
this.storage.sql.exec('INSERT OR REPLACE INTO object_index (sha, tier, pack_id, offset, size, type, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', sha, 'hot', null, // pack_id is null for hot tier
|
|
699
|
+
null, // offset is null for hot tier
|
|
700
|
+
data.length, type, now);
|
|
701
|
+
// Add to cache
|
|
702
|
+
const storedObject = {
|
|
703
|
+
sha,
|
|
704
|
+
type,
|
|
705
|
+
size: data.length,
|
|
706
|
+
data,
|
|
707
|
+
createdAt: now
|
|
708
|
+
};
|
|
709
|
+
this.cache.set(sha, storedObject);
|
|
710
|
+
}
|
|
711
|
+
// Commit transaction
|
|
712
|
+
this.storage.sql.exec('COMMIT');
|
|
713
|
+
this.log('info', `Batch write completed: ${objects.length} objects, ${totalBytes} bytes`);
|
|
714
|
+
// Update metrics
|
|
715
|
+
if (this.options.enableMetrics) {
|
|
716
|
+
this._writes += objects.length;
|
|
717
|
+
this._bytesWritten += totalBytes;
|
|
718
|
+
this._totalWriteLatency += Date.now() - startTime;
|
|
719
|
+
this._batchOperations++;
|
|
720
|
+
this._batchObjectsTotal += objects.length;
|
|
721
|
+
}
|
|
722
|
+
return shas;
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
// Rollback on error
|
|
726
|
+
this.storage.sql.exec('ROLLBACK');
|
|
727
|
+
this.log('error', `Batch write failed, rolled back`, error);
|
|
728
|
+
throw error;
|
|
419
729
|
}
|
|
420
|
-
return shas;
|
|
421
730
|
}
|
|
422
731
|
/**
|
|
423
|
-
* Retrieve multiple objects by SHA.
|
|
732
|
+
* Retrieve multiple objects by SHA using optimized batch queries.
|
|
424
733
|
*
|
|
425
734
|
* @description
|
|
426
|
-
* Fetches multiple objects by
|
|
427
|
-
*
|
|
735
|
+
* Fetches multiple objects efficiently by:
|
|
736
|
+
* 1. First checking the LRU cache for each SHA
|
|
737
|
+
* 2. Batching uncached SHAs into a single SQL query with IN clause
|
|
738
|
+
* 3. Returning results in the original order with null for missing objects
|
|
428
739
|
*
|
|
429
740
|
* @param shas - Array of 40-character SHA-1 hashes
|
|
430
741
|
* @returns Array of objects (or null for missing) in the same order
|
|
@@ -440,10 +751,58 @@ export class ObjectStore {
|
|
|
440
751
|
* ```
|
|
441
752
|
*/
|
|
442
753
|
async getObjects(shas) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
754
|
+
if (shas.length === 0) {
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
757
|
+
const startTime = this.options.enableMetrics ? Date.now() : 0;
|
|
758
|
+
const results = new Array(shas.length).fill(null);
|
|
759
|
+
const uncachedIndices = [];
|
|
760
|
+
const uncachedShas = [];
|
|
761
|
+
let totalBytesRead = 0;
|
|
762
|
+
// First pass: check cache for each SHA
|
|
763
|
+
for (let i = 0; i < shas.length; i++) {
|
|
764
|
+
const sha = shas[i];
|
|
765
|
+
if (!sha || sha.length < 4) {
|
|
766
|
+
results[i] = null;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const cached = this.cache.get(sha);
|
|
770
|
+
if (cached) {
|
|
771
|
+
results[i] = cached;
|
|
772
|
+
totalBytesRead += cached.size;
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
uncachedIndices.push(i);
|
|
776
|
+
uncachedShas.push(sha);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Second pass: batch query for uncached objects
|
|
780
|
+
if (uncachedShas.length > 0) {
|
|
781
|
+
this.log('debug', `Batch fetching ${uncachedShas.length} uncached objects`);
|
|
782
|
+
// Build optimized IN query
|
|
783
|
+
const placeholders = uncachedShas.map(() => '?').join(', ');
|
|
784
|
+
const result = this.storage.sql.exec(`SELECT sha, type, size, data, created_at as createdAt FROM objects WHERE sha IN (${placeholders})`, ...uncachedShas);
|
|
785
|
+
const rows = result.toArray();
|
|
786
|
+
// Build lookup map for O(1) access
|
|
787
|
+
const rowMap = new Map();
|
|
788
|
+
for (const row of rows) {
|
|
789
|
+
rowMap.set(row.sha, row);
|
|
790
|
+
// Add to cache for future reads
|
|
791
|
+
this.cache.set(row.sha, row);
|
|
792
|
+
totalBytesRead += row.size;
|
|
793
|
+
}
|
|
794
|
+
// Fill in results at original indices
|
|
795
|
+
for (let i = 0; i < uncachedIndices.length; i++) {
|
|
796
|
+
const originalIndex = uncachedIndices[i];
|
|
797
|
+
const sha = uncachedShas[i];
|
|
798
|
+
results[originalIndex] = rowMap.get(sha) ?? null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// Update metrics
|
|
802
|
+
if (this.options.enableMetrics) {
|
|
803
|
+
this._reads += shas.length;
|
|
804
|
+
this._bytesRead += totalBytesRead;
|
|
805
|
+
this._totalReadLatency += Date.now() - startTime;
|
|
447
806
|
}
|
|
448
807
|
return results;
|
|
449
808
|
}
|
|
@@ -505,21 +864,42 @@ export class ObjectStore {
|
|
|
505
864
|
const entries = [];
|
|
506
865
|
let offset = 0;
|
|
507
866
|
const data = obj.data;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
nullIndex
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
867
|
+
try {
|
|
868
|
+
while (offset < data.length) {
|
|
869
|
+
// Find the null byte after mode+name
|
|
870
|
+
let nullIndex = offset;
|
|
871
|
+
while (nullIndex < data.length && data[nullIndex] !== 0) {
|
|
872
|
+
nullIndex++;
|
|
873
|
+
}
|
|
874
|
+
// Check if we found a null byte
|
|
875
|
+
if (nullIndex >= data.length) {
|
|
876
|
+
// No null byte found - malformed data, return empty entries
|
|
877
|
+
return { type: 'tree', data: obj.data, entries: [] };
|
|
878
|
+
}
|
|
879
|
+
const modeNameStr = decoder.decode(data.slice(offset, nullIndex));
|
|
880
|
+
const spaceIndex = modeNameStr.indexOf(' ');
|
|
881
|
+
// Check for valid mode+name format
|
|
882
|
+
if (spaceIndex === -1) {
|
|
883
|
+
// No space found - malformed entry, return empty entries
|
|
884
|
+
return { type: 'tree', data: obj.data, entries: [] };
|
|
885
|
+
}
|
|
886
|
+
const mode = modeNameStr.slice(0, spaceIndex);
|
|
887
|
+
const name = modeNameStr.slice(spaceIndex + 1);
|
|
888
|
+
// Check if we have enough bytes for the 20-byte SHA
|
|
889
|
+
if (nullIndex + 21 > data.length) {
|
|
890
|
+
// Not enough bytes for SHA - return what we have parsed so far as malformed
|
|
891
|
+
return { type: 'tree', data: obj.data, entries: [] };
|
|
892
|
+
}
|
|
893
|
+
// Read 20-byte SHA
|
|
894
|
+
const sha20 = data.slice(nullIndex + 1, nullIndex + 21);
|
|
895
|
+
const entrySha = bytesToHex(sha20);
|
|
896
|
+
entries.push({ mode, name, sha: entrySha });
|
|
897
|
+
offset = nullIndex + 21;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
// Any parsing error - return null or empty entries
|
|
902
|
+
return { type: 'tree', data: obj.data, entries: [] };
|
|
523
903
|
}
|
|
524
904
|
return {
|
|
525
905
|
type: 'tree',
|
|
@@ -622,7 +1002,7 @@ export class ObjectStore {
|
|
|
622
1002
|
let object = '';
|
|
623
1003
|
let objectType = 'commit';
|
|
624
1004
|
let name = '';
|
|
625
|
-
let tagger =
|
|
1005
|
+
let tagger = undefined;
|
|
626
1006
|
let messageStartIndex = 0;
|
|
627
1007
|
for (let i = 0; i < lines.length; i++) {
|
|
628
1008
|
const line = lines[i];
|
|
@@ -640,10 +1020,18 @@ export class ObjectStore {
|
|
|
640
1020
|
name = line.slice(4);
|
|
641
1021
|
}
|
|
642
1022
|
else if (line.startsWith('tagger ')) {
|
|
643
|
-
|
|
1023
|
+
try {
|
|
1024
|
+
tagger = parseAuthorLine(line);
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
// Malformed tagger line - leave tagger as undefined
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
644
1030
|
}
|
|
645
1031
|
}
|
|
646
|
-
|
|
1032
|
+
// Validate required fields - object and name must be present
|
|
1033
|
+
// tagger is optional (some older tags or special tags may not have it)
|
|
1034
|
+
if (!object || !name) {
|
|
647
1035
|
return null;
|
|
648
1036
|
}
|
|
649
1037
|
const message = lines.slice(messageStartIndex).join('\n');
|