@dotinc/ogre 0.15.2 → 0.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -109,6 +109,50 @@ test("checkout and create new branch with at least 1 commit", async (t) => {
109
109
  );
110
110
  });
111
111
 
112
+ test("checkout tag leaves HEAD detached", async (t) => {
113
+ const [repo] = await getBaseline();
114
+ repo.data.name = "new name";
115
+ const commitHash = await repo.commit("tagged change", testAuthor);
116
+ repo.tag("1.0.0");
117
+
118
+ // Move main forward so tag and main diverge
119
+ repo.data.description = "newer change";
120
+ await repo.commit("after tag", testAuthor);
121
+
122
+ // Checkout the tag
123
+ await repo.checkout("1.0.0");
124
+ t.equal(repo.branch(), "HEAD", "HEAD should be detached after tag checkout");
125
+ t.equal(repo.head(), commitHash, "HEAD should point to tagged commit hash");
126
+ });
127
+
128
+ test("commit after tag checkout does not move tag", async (t) => {
129
+ const [repo] = await getBaseline();
130
+ repo.data.name = "tagged";
131
+ const taggedCommit = await repo.commit("tagged change", testAuthor);
132
+ repo.tag("1.0.0");
133
+
134
+ await repo.checkout("1.0.0");
135
+
136
+ // Commit on detached HEAD
137
+ repo.data.description = "detached work";
138
+ const detachedCommit = await repo.commit("detached commit", testAuthor);
139
+
140
+ // Tag should NOT have moved
141
+ t.equal(
142
+ repo.ref("refs/tags/1.0.0"),
143
+ taggedCommit,
144
+ "tag should still point to original commit",
145
+ );
146
+ // HEAD should point to the new detached commit
147
+ t.equal(repo.head(), detachedCommit, "HEAD should point to detached commit");
148
+ // refs/heads/main should not have changed
149
+ t.equal(
150
+ repo.ref("refs/heads/main"),
151
+ taggedCommit,
152
+ "main should not move",
153
+ );
154
+ });
155
+
112
156
  test("replacing default branch on empty master removes main", async (t) => {
113
157
  const cx: ComplexObject = {
114
158
  nested: [],
package/src/repository.ts CHANGED
@@ -2,12 +2,8 @@ import {
2
2
  applyPatch,
3
3
  compare,
4
4
  deepClone,
5
- generate,
6
5
  JsonPatchError,
7
- observe,
8
- type Observer,
9
6
  type Operation,
10
- unobserve,
11
7
  validate,
12
8
  } from "fast-json-patch";
13
9
  import type { Commit, CommitHashContent } from "./commit.js";
@@ -28,6 +24,7 @@ import {
28
24
  REFS_HEAD_KEY,
29
25
  REFS_MAIN_KEY,
30
26
  shaishToCommit,
27
+ isTagRef,
31
28
  tagToRef,
32
29
  validateBranchName,
33
30
  validateRef,
@@ -133,7 +130,7 @@ export class Repository<T extends { [k: PropertyKey]: any }>
133
130
  this.original = deepClone(obj);
134
131
  // store js ref, so obj can still be modified without going through repo.data
135
132
  this.data = obj as T;
136
- this.observer = observe(obj as T);
133
+ this._snapshot = deepClone(obj) as T;
137
134
  this.refs = options.history?.refs
138
135
  ? mutableMapCopy(options.history?.refs)
139
136
  : new Map<string, Reference>([
@@ -204,7 +201,7 @@ export class Repository<T extends { [k: PropertyKey]: any }>
204
201
  // stores the remote state upon initialization
205
202
  private remoteCommits: ReadonlyArray<Readonly<string>> | undefined;
206
203
 
207
- private observer: Observer<T>;
204
+ private _snapshot: T;
208
205
 
209
206
  private readonly refs: Map<string, Reference>;
210
207
  private readonly commits: Array<Commit>;
@@ -326,9 +323,8 @@ export class Repository<T extends { [k: PropertyKey]: any }>
326
323
  return;
327
324
  }
328
325
 
329
- this.observer.unobserve();
330
326
  jsondiffpatch.patch(this.data, patchToTarget);
331
- this.observer = observe(this.data);
327
+ this._snapshot = deepClone(this.data);
332
328
  }
333
329
 
334
330
  // FIXME: refactor this to use jsondiffpatch delta instead of json-patch for advanced type support
@@ -368,10 +364,6 @@ export class Repository<T extends { [k: PropertyKey]: any }>
368
364
  mode: "soft" | "hard" | undefined = "hard",
369
365
  shaish: string | undefined = REFS_HEAD_KEY,
370
366
  ): Promise<void> {
371
- if (mode === "hard") {
372
- unobserve(this.data, this.observer);
373
- }
374
-
375
367
  const [commit] = shaishToCommit(shaish, this.refs, this.commits);
376
368
  await this.moveTo(commit);
377
369
 
@@ -386,10 +378,6 @@ export class Repository<T extends { [k: PropertyKey]: any }>
386
378
  for (const ref of moveableRefs) {
387
379
  this.moveRef(ref, commit);
388
380
  }
389
-
390
- if (mode === "hard") {
391
- this.observer = observe(this.data);
392
- }
393
381
  }
394
382
 
395
383
  branch(): string {
@@ -448,10 +436,9 @@ export class Repository<T extends { [k: PropertyKey]: any }>
448
436
  this.commits,
449
437
  );
450
438
  await this.moveTo(commit);
451
- this.moveRef(
452
- REFS_HEAD_KEY,
453
- isRef && refKey !== undefined ? refKey : commit,
454
- );
439
+ // Only follow branch refs. Tags produce detached HEAD (like git).
440
+ const isBranchRef = isRef && refKey !== undefined && !isTagRef(refKey);
441
+ this.moveRef(REFS_HEAD_KEY, isBranchRef ? refKey : commit);
455
442
  }
456
443
  }
457
444
 
@@ -473,14 +460,14 @@ export class Repository<T extends { [k: PropertyKey]: any }>
473
460
  }
474
461
  }
475
462
 
476
- // FIXME: refactor this to use parent tree vs this.data instead to get json-patch
477
- const patch = generate(this.observer);
463
+ const patch = compare(this._snapshot, this.data);
478
464
  if (
479
465
  (patch.length === 0 && !amend) ||
480
466
  (amend && message === parent?.message)
481
467
  ) {
482
468
  throw new Error(`no changes to commit`);
483
469
  }
470
+ this._snapshot = deepClone(this.data);
484
471
 
485
472
  const timestamp = this.timestampFn ? this.timestampFn() : new Date();
486
473
  const parentChanges =