gip-remote 1.2.6 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +152 -5
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -210,18 +210,76 @@ class Remote extends ReadyResource {
210
210
  }
211
211
  }
212
212
 
213
- // 4b. Insert file records (idempotent w.r.t. (branch, path) acts as
214
- // upsert for paths whose blob/mode/metadata changed).
213
+ // 4b. Compute, for each file in HEAD's tree, the most recent commit IN
214
+ // THIS PUSH that actually changed its blob — that's the commit we
215
+ // want to credit on the @gip/files row. If we just stamped every
216
+ // file with HEAD's metadata (the previous behaviour), a fresh push
217
+ // of a multi-commit history would make every file look like it was
218
+ // last edited by HEAD, which is useless for a tree view.
219
+ //
220
+ // Algorithm: walk the commit chain HEAD → first-parent through the
221
+ // commits we have in the pack, flatten each commit's tree into a
222
+ // path → oid map, and for each path record the most recent commit
223
+ // whose tree differs at that path from its first-parent's tree.
224
+ // We don't follow merge parents — same heuristic GitHub's "blame"
225
+ // uses; it keeps cost predictable and matches user expectation.
226
+ //
227
+ // Cases the fallback below handles:
228
+ // - Pack is shallow and the path was already at HEAD's blob
229
+ // before our oldest commit — keep the existing row as-is.
230
+ // - First push containing a root commit — files appearing in the
231
+ // root take the root commit's metadata.
232
+ const fileLastTouch = computeFileLastTouch(objects, resolvedOid, commit)
233
+ const oldestInPack = fileLastTouch.oldest
234
+
215
235
  for (const file of files) {
236
+ const existing = await this._db.get('@gip/files', {
237
+ branch: refName,
238
+ path: file.path
239
+ })
240
+
241
+ let meta = fileLastTouch.byPath.get(file.path)
242
+
243
+ if (!meta) {
244
+ // No commit in our pack changed this file. Either it's been at this
245
+ // blob since before our window, or the pack is shallow.
246
+ if (existing && existing.oid === file.oid && existing.mode === file.mode) {
247
+ // Genuinely unchanged from prior push — leave row alone.
248
+ continue
249
+ }
250
+ // Fall back to the oldest commit we have. Best approximation when a
251
+ // shallow clone is being seeded (we don't have the real introducing
252
+ // commit, but the oldest commit in the pack is a strict upper bound
253
+ // on "when it could have last changed" given what we know).
254
+ meta = {
255
+ author: oldestInPack.author,
256
+ message: oldestInPack.message,
257
+ timestamp: oldestInPack.timestamp
258
+ }
259
+ }
260
+
261
+ // Idempotency: if the existing row already reflects the same blob,
262
+ // mode, and metadata we'd write, skip the insert. Saves a write
263
+ // round-trip on no-op pushes (e.g. retries).
264
+ if (
265
+ existing &&
266
+ existing.oid === file.oid &&
267
+ existing.mode === file.mode &&
268
+ existing.message === meta.message &&
269
+ existing.timestamp === meta.timestamp
270
+ ) {
271
+ continue
272
+ }
273
+
216
274
  await this._db.insert('@gip/files', {
217
275
  branch: refName,
218
276
  path: file.path,
219
277
  oid: file.oid,
220
278
  mode: file.mode,
221
279
  size: file.size,
222
- author: commit.author,
223
- message: commit.message,
224
- timestamp: commit.timestamp
280
+ author: meta.author,
281
+ message: meta.message,
282
+ timestamp: meta.timestamp
225
283
  })
226
284
  }
227
285
 
@@ -365,6 +423,95 @@ class Remote extends ReadyResource {
365
423
  }
366
424
  }
367
425
 
426
+ /**
427
+ * Walk the first-parent commit chain (within `objects`) starting at HEAD,
428
+ * and for each path in HEAD's tree return the metadata of the most recent
429
+ * commit whose tree differs at that path from its parent's tree.
430
+ *
431
+ * Returns:
432
+ * - byPath: Map<path, { author, message, timestamp }> for paths we
433
+ * could attribute within the pack.
434
+ * - oldest: the oldest commit reached (used as a fallback by callers
435
+ * when the pack is shallow and a path was already at its
436
+ * current blob before the pack window).
437
+ *
438
+ * Pure function — does no IO, just inspects the in-memory `objects` map.
439
+ */
440
+ function computeFileLastTouch(objects, headOid, headCommit) {
441
+ // 1. Walk the chain through first-parent edges, stopping at the first
442
+ // parent we don't have. Stash both the parsed commit and the OID.
443
+ const chain = []
444
+ let curOid = headOid
445
+ let cur = headCommit
446
+ for (;;) {
447
+ chain.push({ oid: curOid, commit: cur })
448
+ if (!cur.parents || cur.parents.length === 0) break
449
+ const parentOid = cur.parents[0]
450
+ const parentObj = objects.get(parentOid)
451
+ if (!parentObj || parentObj.type !== 'commit') break
452
+ cur = parseCommit(parentObj.data)
453
+ curOid = parentOid
454
+ }
455
+
456
+ // 2. Flatten each commit's tree into a path → oid map. walkTree handles
457
+ // nested trees and skips missing sub-trees gracefully (returns []),
458
+ // which is what we want for incremental packs that don't re-send
459
+ // unchanged sub-trees.
460
+ const treeMaps = chain.map(({ commit }) => {
461
+ const map = new Map()
462
+ for (const f of walkTree(objects, commit.tree, '')) {
463
+ map.set(f.path, f.oid)
464
+ }
465
+ return map
466
+ })
467
+
468
+ // 3. Walk newest-first; record the change point for each HEAD path.
469
+ const byPath = new Map()
470
+ const remaining = new Set(treeMaps[0].keys())
471
+
472
+ for (let i = 0; i < chain.length && remaining.size > 0; i++) {
473
+ const cur = treeMaps[i]
474
+ const isLast = i === chain.length - 1
475
+ const par = isLast ? null : treeMaps[i + 1]
476
+ // True root commit (no parents at all) — files appearing here are new,
477
+ // so they're attributed to this commit. We must NOT do the same when
478
+ // we ran out of pack (parents exist, just not in pack), or we'd make
479
+ // up an attribution that isn't real.
480
+ const isRoot = isLast && (!chain[i].commit.parents || chain[i].commit.parents.length === 0)
481
+
482
+ for (const path of remaining) {
483
+ const curOidAtPath = cur.get(path)
484
+ if (curOidAtPath === undefined) continue
485
+
486
+ if (par) {
487
+ const parOidAtPath = par.get(path)
488
+ if (parOidAtPath !== curOidAtPath) {
489
+ byPath.set(path, {
490
+ author: chain[i].commit.author,
491
+ message: chain[i].commit.message,
492
+ timestamp: chain[i].commit.timestamp
493
+ })
494
+ remaining.delete(path)
495
+ }
496
+ } else if (isRoot) {
497
+ byPath.set(path, {
498
+ author: chain[i].commit.author,
499
+ message: chain[i].commit.message,
500
+ timestamp: chain[i].commit.timestamp
501
+ })
502
+ remaining.delete(path)
503
+ }
504
+ // else: shallow boundary — caller falls back to existing row /
505
+ // oldest-commit metadata.
506
+ }
507
+ }
508
+
509
+ return {
510
+ byPath,
511
+ oldest: chain[chain.length - 1].commit
512
+ }
513
+ }
514
+
368
515
  module.exports = {
369
516
  Remote,
370
517
  RemoteDrive,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gip-remote",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "Git+Pear remote DB for handling git data",
5
5
  "main": "index.js",
6
6
  "scripts": {