@dotinc/ogre 0.8.1 → 0.9.0

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 (27) hide show
  1. package/.tap/processinfo/{4bb3fbc0-322c-47fe-99ca-5a2029006dab.json → 2426d9f3-699c-4fb9-94b2-9ec9c3eac102.json} +23 -23
  2. package/.tap/processinfo/{999e3026-ccc7-47b1-bfbd-82a08fbfebb2.json → 45f9dbea-d45b-4370-ad84-bacbe0f7923b.json} +7 -7
  3. package/.tap/processinfo/{46e1fc09-045e-4621-a147-8e17ab127537.json → 6a2329ae-15e9-4041-98f8-205645f535f5.json} +7 -7
  4. package/.tap/processinfo/{9e618086-85e0-4b56-855b-6e7251bd1b83.json → 7073c462-24dd-4066-8ee7-610cc0a4cdc6.json} +23 -23
  5. package/.tap/processinfo/{0edfcd98-724a-4657-9a64-db2d38f2bc26.json → 8ce3d798-c3e7-49ba-a002-477d6aed67d1.json} +11 -11
  6. package/.tap/processinfo/a33fc811-9cba-46ee-8577-3d68cb1ae96f.json +246 -0
  7. package/.tap/processinfo/{afde0f56-0117-4d71-a370-ed7621252dcc.json → cc694d82-e679-4b5d-8cf9-decd8029a65a.json} +7 -7
  8. package/.tap/test-results/src/branch.test.ts.tap +6 -6
  9. package/.tap/test-results/src/checkout.test.ts.tap +9 -9
  10. package/.tap/test-results/src/commit.test.ts.tap +18 -18
  11. package/.tap/test-results/src/merge.test.ts.tap +2 -2
  12. package/.tap/test-results/src/repository.test.ts.tap +111 -26
  13. package/.tap/test-results/src/tag.test.ts.tap +3 -3
  14. package/.tap/test-results/src/utils.test.ts.tap +41 -8
  15. package/CHANGELOG.md +17 -0
  16. package/lib/commit.d.ts +2 -2
  17. package/lib/repository.d.ts +19 -7
  18. package/lib/repository.js +84 -5
  19. package/lib/utils.d.ts +12 -1
  20. package/lib/utils.js +40 -8
  21. package/package.json +4 -2
  22. package/src/commit.ts +9 -9
  23. package/src/repository.test.ts +359 -5
  24. package/src/repository.ts +118 -13
  25. package/src/utils.test.ts +88 -1
  26. package/src/utils.ts +48 -10
  27. package/.tap/processinfo/ae7dad2b-5163-4c3f-8d16-e25e7696c657.json +0 -242
@@ -10,9 +10,8 @@ import {
10
10
  testAuthor,
11
11
  updateHeaderData,
12
12
  } from "./test.utils";
13
- import { History, Reference } from "./interfaces";
14
- import { compare, Operation } from "fast-json-patch";
15
- import { printChange, printChangeLog } from "./utils";
13
+ import { Reference } from "./interfaces";
14
+ import { compare } from "fast-json-patch";
16
15
 
17
16
  test("diff is ok", async (t) => {
18
17
  const [repo, obj] = await getBaseline();
@@ -137,10 +136,14 @@ test("history", async (t) => {
137
136
  const history = repo.getHistory();
138
137
 
139
138
  const obj2 = {};
140
- // @ts-ignore
141
139
  const repo2 = new Repository(obj2, { history });
142
140
 
143
- t.matchOnly(obj, obj2, "restored object does not equal last version.");
141
+ t.matchOnlyStrict(
142
+ obj,
143
+ obj2,
144
+ "restored object does not equal last version.",
145
+ );
146
+ t.not(obj, obj2, "should not be the js ref");
144
147
  });
145
148
 
146
149
  t.test("remoteRefs doesn't change on commit", async (t) => {
@@ -373,3 +376,354 @@ test("apply", async (t) => {
373
376
  t.match(repo.data, targetState, "The final state does not match up");
374
377
  });
375
378
  });
379
+
380
+ test("pending changes - push helpers", async (t) => {
381
+ t.test("1 commit & 1 ref update", async (t) => {
382
+ const [repo, obj] = await getBaseline();
383
+ updateHeaderData(obj);
384
+ await repo.commit("header data", testAuthor);
385
+ addOneNested(obj);
386
+ await repo.commit("first nested", testAuthor);
387
+ repo.tag("v0.1.0");
388
+ const history = repo.getHistory();
389
+
390
+ const repo2 = new Repository<ComplexObject>({}, { history });
391
+
392
+ repo2.data.description = "changed description";
393
+ const hash = await repo2.commit("changed desc", testAuthor);
394
+ const { commits } = repo2.getHistory();
395
+ const pendingCommit = commits.find((c) => c.hash === hash);
396
+
397
+ const pending = repo2.cherry();
398
+
399
+ t.equal(pending.commits.length, 1, "incorrect number of pending commits");
400
+ t.equal(pending.refs.size, 1, "incorrect number of pending ref updates");
401
+ t.matchOnly(pending.commits[0], pendingCommit, "wrong pending commit");
402
+ t.matchOnlyStrict(
403
+ Array.from(pending.refs.keys()),
404
+ ["refs/heads/main"],
405
+ "wrong pending ref update",
406
+ );
407
+ });
408
+
409
+ t.test("2 commit & 1 ref update", async (t) => {
410
+ const [repo, obj] = await getBaseline();
411
+ updateHeaderData(obj);
412
+ await repo.commit("header data", testAuthor);
413
+ addOneNested(obj);
414
+ await repo.commit("first nested", testAuthor);
415
+ repo.tag("v0.1.0");
416
+ const history = repo.getHistory();
417
+
418
+ const repo2 = new Repository<ComplexObject>({}, { history });
419
+
420
+ repo2.data.description = "changed description";
421
+ const hash1 = await repo2.commit("changed desc", testAuthor);
422
+ repo2.data.uuid = "uuid1";
423
+ const hash2 = await repo2.commit("changed uuid", testAuthor);
424
+
425
+ const { commits } = repo2.getHistory();
426
+ const pendingCommit1 = commits.find((c) => c.hash === hash1);
427
+ const pendingCommit2 = commits.find((c) => c.hash === hash2);
428
+
429
+ const pending = repo2.cherry();
430
+
431
+ t.equal(pending.commits.length, 2, "incorrect number of pending commits");
432
+ t.equal(pending.refs.size, 1, "incorrect number of pending ref updates");
433
+ t.matchOnlyStrict(
434
+ new Set(pending.commits),
435
+ new Set([pendingCommit2, pendingCommit1]),
436
+ "wrong pending commits",
437
+ );
438
+ t.matchOnlyStrict(
439
+ Array.from(pending.refs.keys()),
440
+ ["refs/heads/main"],
441
+ "wrong pending ref update",
442
+ );
443
+ });
444
+
445
+ t.test("1 commit & 2 ref updates", async (t) => {
446
+ const [repo, obj] = await getBaseline();
447
+ updateHeaderData(obj);
448
+ await repo.commit("header data", testAuthor);
449
+ addOneNested(obj);
450
+ await repo.commit("first nested", testAuthor);
451
+ repo.tag("v0.1.0");
452
+ const history = repo.getHistory();
453
+
454
+ const repo2 = new Repository<ComplexObject>({}, { history });
455
+
456
+ repo2.data.description = "changed description";
457
+ const hash1 = await repo2.commit("changed desc", testAuthor);
458
+ repo2.createBranch("branch2");
459
+
460
+ const { commits } = repo2.getHistory();
461
+ const pendingCommit1 = commits.find((c) => c.hash === hash1);
462
+
463
+ const pending = repo2.cherry();
464
+
465
+ t.equal(pending.commits.length, 1, "incorrect number of pending commits");
466
+ t.equal(pending.refs.size, 2, "incorrect number of pending ref updates");
467
+ t.matchOnlyStrict(
468
+ pending.commits,
469
+ [pendingCommit1],
470
+ "wrong pending commits",
471
+ );
472
+ t.matchOnlyStrict(
473
+ new Set(pending.refs.keys()),
474
+ new Set(["refs/heads/main", "refs/heads/branch2"]),
475
+ "wrong pending ref update",
476
+ );
477
+ });
478
+
479
+ t.test("2 commit & 2 ref updates", async (t) => {
480
+ const [repo, obj] = await getBaseline();
481
+ updateHeaderData(obj);
482
+ await repo.commit("header data", testAuthor);
483
+ addOneNested(obj);
484
+ await repo.commit("first nested", testAuthor);
485
+ repo.tag("v0.1.0");
486
+ const history = repo.getHistory();
487
+
488
+ const repo2 = new Repository<ComplexObject>({}, { history });
489
+
490
+ repo2.data.description = "changed description";
491
+ const hash1 = await repo2.commit("changed desc", testAuthor);
492
+ repo2.data.uuid = "uuid1";
493
+ const hash2 = await repo2.commit("changed uuid", testAuthor);
494
+ repo2.checkout("branch2", true);
495
+
496
+ const { commits } = repo2.getHistory();
497
+ const pendingCommit1 = commits.find((c) => c.hash === hash1);
498
+ const pendingCommit2 = commits.find((c) => c.hash === hash2);
499
+
500
+ const pending = repo2.cherry();
501
+
502
+ t.equal(pending.commits.length, 2, "incorrect number of pending commits");
503
+ t.equal(pending.refs.size, 2, "incorrect number of pending ref updates");
504
+ t.matchOnlyStrict(
505
+ new Set(pending.commits),
506
+ new Set([pendingCommit2, pendingCommit1]),
507
+ "wrong pending commits",
508
+ );
509
+ t.matchOnlyStrict(
510
+ new Set(pending.refs.keys()),
511
+ new Set(["refs/heads/main", "refs/heads/branch2"]),
512
+ "wrong pending ref update",
513
+ );
514
+ });
515
+
516
+ t.test("3 commit & 2 ref updates", async (t) => {
517
+ const [repo, obj] = await getBaseline();
518
+ updateHeaderData(obj);
519
+ await repo.commit("header data", testAuthor);
520
+ addOneNested(obj);
521
+ await repo.commit("first nested", testAuthor);
522
+ repo.tag("v0.1.0");
523
+ const history = repo.getHistory();
524
+
525
+ const repo2 = new Repository<ComplexObject>({}, { history });
526
+
527
+ repo2.data.description = "changed description";
528
+ const hash1 = await repo2.commit("changed desc", testAuthor);
529
+ repo2.data.uuid = "uuid1";
530
+ const hash2 = await repo2.commit("changed uuid", testAuthor);
531
+ repo2.checkout("branch2", true);
532
+
533
+ repo2.data.nested = [{ name: "a", uuid: "thing" }];
534
+ const hash3 = await repo2.commit("added a thing", testAuthor);
535
+
536
+ const { commits } = repo2.getHistory();
537
+ const pendingCommit1 = commits.find((c) => c.hash === hash1);
538
+ const pendingCommit2 = commits.find((c) => c.hash === hash2);
539
+ const pendingCommit3 = commits.find((c) => c.hash === hash3);
540
+
541
+ const pending = repo2.cherry();
542
+
543
+ t.equal(pending.commits.length, 3, "incorrect number of pending commits");
544
+ t.equal(pending.refs.size, 2, "incorrect number of pending ref updates");
545
+ t.matchOnlyStrict(
546
+ new Set(pending.commits),
547
+ new Set([pendingCommit3, pendingCommit2, pendingCommit1]),
548
+ "wrong pending commits",
549
+ );
550
+ t.matchOnlyStrict(
551
+ new Set(pending.refs.keys()),
552
+ new Set(["refs/heads/main", "refs/heads/branch2"]),
553
+ "wrong pending ref update",
554
+ );
555
+ });
556
+
557
+ t.test("3 commit & 3 ref updates", async (t) => {
558
+ const [repo, obj] = await getBaseline();
559
+ updateHeaderData(obj);
560
+ await repo.commit("header data", testAuthor);
561
+ addOneNested(obj);
562
+ await repo.commit("first nested", testAuthor);
563
+ repo.tag("v0.1.0");
564
+ const history = repo.getHistory();
565
+
566
+ const repo2 = new Repository<ComplexObject>({}, { history });
567
+
568
+ repo2.data.description = "changed description";
569
+ const hash1 = await repo2.commit("changed desc", testAuthor);
570
+ repo2.data.uuid = "uuid1";
571
+ const hash2 = await repo2.commit("changed uuid", testAuthor);
572
+ repo2.checkout("branch2", true);
573
+
574
+ repo2.data.nested = [{ name: "a", uuid: "thing" }];
575
+ const hash3 = await repo2.commit("added a thing", testAuthor);
576
+ repo2.tag("v0.2.0");
577
+
578
+ const { commits } = repo2.getHistory();
579
+ const pendingCommit1 = commits.find((c) => c.hash === hash1);
580
+ const pendingCommit2 = commits.find((c) => c.hash === hash2);
581
+ const pendingCommit3 = commits.find((c) => c.hash === hash3);
582
+
583
+ const pending = repo2.cherry();
584
+
585
+ t.equal(pending.commits.length, 3, "incorrect number of pending commits");
586
+ t.equal(pending.refs.size, 3, "incorrect number of pending ref updates");
587
+ t.matchOnlyStrict(
588
+ new Set(pending.commits),
589
+ new Set([pendingCommit3, pendingCommit2, pendingCommit1]),
590
+ "wrong pending commits",
591
+ );
592
+ t.matchOnlyStrict(
593
+ new Set(pending.refs.keys()),
594
+ new Set(["refs/heads/main", "refs/heads/branch2", "refs/tags/v0.2.0"]),
595
+ "wrong pending ref update",
596
+ );
597
+ });
598
+
599
+ t.test("after merge 1 commit & 3 ref updates", async (t) => {
600
+ const [repo, obj] = await getBaseline();
601
+ updateHeaderData(obj);
602
+ await repo.commit("header data", testAuthor);
603
+ addOneNested(obj);
604
+ await repo.commit("first nested", testAuthor);
605
+ repo.tag("v0.1.0");
606
+ const history = repo.getHistory();
607
+
608
+ const repo2 = new Repository<ComplexObject>({}, { history });
609
+ repo2.checkout("branch2", true);
610
+
611
+ repo2.data.nested = [{ name: "a", uuid: "thing" }];
612
+ const hash3 = await repo2.commit("added a thing", testAuthor);
613
+ repo2.tag("v0.2.0");
614
+
615
+ repo2.checkout("main");
616
+ repo2.merge("branch2");
617
+
618
+ const { commits } = repo2.getHistory();
619
+ const pendingCommit3 = commits.find((c) => c.hash === hash3);
620
+
621
+ const pending = repo2.cherry();
622
+
623
+ t.equal(pending.commits.length, 1, "incorrect number of pending commits");
624
+ t.equal(pending.refs.size, 3, "incorrect number of pending ref updates");
625
+ t.matchOnlyStrict(
626
+ pending.commits,
627
+ [pendingCommit3],
628
+ "wrong pending commits",
629
+ );
630
+ t.matchOnlyStrict(
631
+ new Set(pending.refs.keys()),
632
+ new Set(["refs/heads/main", "refs/heads/branch2", "refs/tags/v0.2.0"]),
633
+ "wrong pending ref update",
634
+ );
635
+ });
636
+
637
+ t.test("after merge 1 commit & 2 ref updates", async (t) => {
638
+ const [repo, obj] = await getBaseline();
639
+ updateHeaderData(obj);
640
+ await repo.commit("header data", testAuthor);
641
+ addOneNested(obj);
642
+ await repo.commit("first nested", testAuthor);
643
+ repo.tag("v0.1.0");
644
+ const history = repo.getHistory();
645
+
646
+ const repo2 = new Repository<ComplexObject>({}, { history });
647
+ repo2.checkout("branch2", true);
648
+
649
+ repo2.data.nested = [{ name: "a", uuid: "thing" }];
650
+ const hash3 = await repo2.commit("added a thing", testAuthor);
651
+
652
+ repo2.checkout("main");
653
+ repo2.merge("branch2");
654
+
655
+ const { commits } = repo2.getHistory();
656
+ const pendingCommit3 = commits.find((c) => c.hash === hash3);
657
+
658
+ const pending = repo2.cherry();
659
+
660
+ t.equal(pending.commits.length, 1, "incorrect number of pending commits");
661
+ t.equal(pending.refs.size, 2, "incorrect number of pending ref updates");
662
+ t.matchOnlyStrict(
663
+ pending.commits,
664
+ [pendingCommit3],
665
+ "wrong pending commits",
666
+ );
667
+ t.matchOnlyStrict(
668
+ new Set(pending.refs.keys()),
669
+ new Set(["refs/heads/main", "refs/heads/branch2"]),
670
+ "wrong pending ref update",
671
+ );
672
+ });
673
+ t.test("no merge 1 commit & 1 ref updates", async (t) => {
674
+ const [repo, obj] = await getBaseline();
675
+ updateHeaderData(obj);
676
+ await repo.commit("header data", testAuthor);
677
+ addOneNested(obj);
678
+ await repo.commit("first nested", testAuthor);
679
+ repo.tag("v0.1.0");
680
+ const history = repo.getHistory();
681
+
682
+ const repo2 = new Repository<ComplexObject>({}, { history });
683
+ repo2.checkout("branch2", true);
684
+
685
+ repo2.data.nested = [{ name: "a", uuid: "thing" }];
686
+ const hash3 = await repo2.commit("added a thing", testAuthor);
687
+
688
+ const { commits } = repo2.getHistory();
689
+ const pendingCommit3 = commits.find((c) => c.hash === hash3);
690
+
691
+ const pending = repo2.cherry();
692
+
693
+ t.equal(pending.commits.length, 1, "incorrect number of pending commits");
694
+ t.equal(pending.refs.size, 1, "incorrect number of pending ref updates");
695
+ t.matchOnlyStrict(
696
+ pending.commits,
697
+ [pendingCommit3],
698
+ "wrong pending commits",
699
+ );
700
+ t.matchOnlyStrict(
701
+ pending.refs.keys(),
702
+ ["refs/heads/branch2"],
703
+ "wrong pending ref update",
704
+ );
705
+ });
706
+ t.test("no merge no commit & 1 ref updates", async (t) => {
707
+ const [repo, obj] = await getBaseline();
708
+ updateHeaderData(obj);
709
+ await repo.commit("header data", testAuthor);
710
+ addOneNested(obj);
711
+ await repo.commit("first nested", testAuthor);
712
+ repo.tag("v0.1.0");
713
+ const history = repo.getHistory();
714
+
715
+ const repo2 = new Repository<ComplexObject>({}, { history });
716
+ repo2.checkout("branch2", true);
717
+
718
+ const pending = repo2.cherry();
719
+
720
+ t.equal(pending.commits.length, 0, "incorrect number of pending commits");
721
+ t.equal(pending.refs.size, 1, "incorrect number of pending ref updates");
722
+ t.matchOnlyStrict(pending.commits, [], "wrong pending commits");
723
+ t.matchOnlyStrict(
724
+ pending.refs.keys(),
725
+ ["refs/heads/branch2"],
726
+ "wrong pending ref update",
727
+ );
728
+ });
729
+ });
package/src/repository.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  createHeadRefValue,
22
22
  getLastRefPathElement,
23
23
  headValueRefPrefix,
24
+ immutableArrayCopy,
24
25
  immutableMapCopy,
25
26
  localHeadPathPrefix,
26
27
  mapPath,
@@ -50,18 +51,18 @@ export interface RepositoryObject<T extends { [k: string]: any }> {
50
51
  * @param shaishFrom expression (e.g. refs (branches, tags), commitSha)
51
52
  * @param shaishTo expression (e.g. refs (branches, tags), commitSha)
52
53
  */
53
- diff(shaishFrom: string, shaishTo?: string): Operation[];
54
+ diff(shaishFrom: string, shaishTo?: string): Array<Operation>;
54
55
 
55
56
  /**
56
57
  * Returns pending changes.
57
58
  */
58
- status(): Operation[];
59
+ status(): Array<Operation>;
59
60
 
60
61
  /**
61
62
  * Applies a patch to the repository's HEAD
62
63
  * @param patch
63
64
  */
64
- apply(patch: Operation[]): void;
65
+ apply(patch: Array<Operation>): void;
65
66
 
66
67
  // It returns the reference where we are currently at
67
68
  head(): string;
@@ -73,7 +74,7 @@ export interface RepositoryObject<T extends { [k: string]: any }> {
73
74
 
74
75
  checkout(shaish: string, createBranch?: boolean): void;
75
76
 
76
- logs(commits?: number): Commit[];
77
+ logs(commits?: number): Array<Commit>;
77
78
 
78
79
  createBranch(name: string): string;
79
80
 
@@ -98,6 +99,11 @@ export interface RepositoryObject<T extends { [k: string]: any }> {
98
99
  * The returned map is a readonly of remote.
99
100
  */
100
101
  remote(): ReadonlyMap<string, Readonly<Reference>> | undefined;
102
+
103
+ /**
104
+ * Cherry returns the commits that are missing from upstream and the refs that have been moved since remote
105
+ */
106
+ cherry(): { commits: Array<Commit>; refs: Map<string, Reference> };
101
107
  }
102
108
 
103
109
  /**
@@ -108,8 +114,11 @@ export class Repository<T extends { [k: PropertyKey]: any }>
108
114
  {
109
115
  constructor(obj: Partial<T>, options: RepositoryOptions<T>) {
110
116
  // FIXME: move this to refs/remote as git would do?
111
-
112
117
  this.remoteRefs = immutableMapCopy(options.history?.refs);
118
+ this.remoteCommits = immutableArrayCopy<Commit, string>(
119
+ options.history?.commits,
120
+ (c) => c.hash,
121
+ );
113
122
  this.original = deepClone(obj);
114
123
  // store js ref, so obj can still be modified without going through repo.data
115
124
  this.data = obj as T;
@@ -146,10 +155,106 @@ export class Repository<T extends { [k: PropertyKey]: any }>
146
155
  | ReadonlyMap<string, Readonly<Reference>>
147
156
  | undefined;
148
157
 
158
+ // stores the remote state upon initialization
159
+ private readonly remoteCommits: ReadonlyArray<Readonly<string>> | undefined;
160
+
149
161
  private observer: Observer<T>;
150
162
 
151
163
  private readonly refs: Map<string, Reference>;
152
- private readonly commits: Commit[];
164
+ private readonly commits: Array<Commit>;
165
+
166
+ cherry(): { commits: Array<Commit>; refs: Map<string, Reference> } {
167
+ const commits: Array<Commit> = [];
168
+ const refs = new Map<string, Reference>();
169
+
170
+ const collectedHashes: Array<string> = [];
171
+ const shouldExclude = (hash: string) =>
172
+ this.remoteCommits?.includes(hash) || collectedHashes.includes(hash);
173
+ const collect = (c: Commit) => {
174
+ // we can't include remote state in the pending report
175
+ if (shouldExclude(c.hash)) {
176
+ return false;
177
+ }
178
+ commits.push(c);
179
+ collectedHashes.push(c.hash);
180
+ return true;
181
+ };
182
+ // collect ref updates and commits that are not present on the remote
183
+ for (const [key, ref] of this.refs) {
184
+ if (key === REFS_HEAD_KEY) {
185
+ continue;
186
+ }
187
+ const remote = this.remoteRefs?.get(key);
188
+ if (!remote) {
189
+ // if we have no remote pair, we need to sync the ref
190
+ refs.set(key, ref);
191
+ const localCommit = this.commits.find((c) => c.hash === ref.value);
192
+ // if ref is not pointing to a commit move on
193
+ if (!localCommit) {
194
+ continue;
195
+ }
196
+ // map all commits to root
197
+ const [isAncestor, path] = mapPath(
198
+ this.commits,
199
+ localCommit,
200
+ undefined,
201
+ );
202
+
203
+ if (isAncestor) {
204
+ for (let i = 0; i < path.length; i++) {
205
+ const commit = path[i];
206
+ collect({
207
+ hash: commit.hash,
208
+ author: commit.author,
209
+ changes: commit.changes,
210
+ message: commit.message,
211
+ parent: commit.parent,
212
+ timestamp: commit.timestamp,
213
+ tree: commit.tree,
214
+ });
215
+ }
216
+ }
217
+ continue;
218
+ }
219
+
220
+ if (remote.value === ref.value) {
221
+ // early exit if remote is the same
222
+ continue;
223
+ }
224
+
225
+ // local and remote refs differ
226
+ refs.set(key, ref);
227
+ const localCommit = this.commits.find((c) => c.hash === ref.value);
228
+ // if ref is not pointing to a commit move on
229
+ if (!localCommit) {
230
+ continue;
231
+ }
232
+ // FIXME: do we have to have the remote ref as a commit locally?
233
+ const remoteCommit = this.commits.find((c) => c.hash === remote.value)!;
234
+ const [isAncestor, path] = mapPath(
235
+ this.commits,
236
+ localCommit,
237
+ remoteCommit,
238
+ );
239
+
240
+ if (isAncestor) {
241
+ for (let i = 0; i < path.length; i++) {
242
+ const commit = path[i];
243
+ collect({
244
+ hash: commit.hash,
245
+ author: commit.author,
246
+ changes: commit.changes,
247
+ message: commit.message,
248
+ parent: commit.parent,
249
+ timestamp: commit.timestamp,
250
+ tree: commit.tree,
251
+ });
252
+ }
253
+ }
254
+ }
255
+
256
+ return { commits, refs };
257
+ }
153
258
 
154
259
  remote(): ReadonlyMap<string, Readonly<Reference>> | undefined {
155
260
  return this.remoteRefs;
@@ -166,8 +271,8 @@ export class Repository<T extends { [k: PropertyKey]: any }>
166
271
  this.observer = observe(this.data);
167
272
  }
168
273
 
169
- apply(patch: Operation[]): JsonPatchError | undefined {
170
- const p = deepClone(patch) as Operation[];
274
+ apply(patch: Array<Operation>): JsonPatchError | undefined {
275
+ const p = deepClone(patch) as Array<Operation>;
171
276
  const err = validate(p, this.data);
172
277
  if (err) {
173
278
  // credit goes to @NicBright
@@ -247,7 +352,7 @@ export class Repository<T extends { [k: PropertyKey]: any }>
247
352
  return this.diff(commit.hash);
248
353
  }
249
354
 
250
- diff(shaishFrom: string, shaishTo?: string): Operation[] {
355
+ diff(shaishFrom: string, shaishTo?: string): Array<Operation> {
251
356
  const [cFrom] = shaishToCommit(shaishFrom, this.refs, this.commits);
252
357
  let target: T;
253
358
  if (shaishTo) {
@@ -393,7 +498,7 @@ export class Repository<T extends { [k: PropertyKey]: any }>
393
498
  }
394
499
  // traverse backwards and build commit tree
395
500
  let c: Commit | undefined = commit;
396
- let commitsList: Commit[] = [];
501
+ let commitsList: Array<Commit> = [];
397
502
  while (c !== undefined) {
398
503
  commitsList = [c, ...commitsList];
399
504
  c = this.commits.find((parent) => parent.hash === c?.parent);
@@ -408,8 +513,8 @@ export class Repository<T extends { [k: PropertyKey]: any }>
408
513
  };
409
514
  }
410
515
 
411
- logs(numberOfCommits?: number): Commit[] {
412
- const logs: Commit[] = [];
516
+ logs(numberOfCommits?: number): Array<Commit> {
517
+ const logs: Array<Commit> = [];
413
518
  const limit = numberOfCommits ?? -1;
414
519
  let c = this.commitAtHead();
415
520
  let counter = 0;
@@ -458,7 +563,7 @@ export class Repository<T extends { [k: PropertyKey]: any }>
458
563
  // *---*
459
564
  // \
460
565
  // *---*---* (master, foo)
461
- const [isAncestor] = mapPath(headCommit, srcCommit, this.commits);
566
+ const [isAncestor] = mapPath(this.commits, srcCommit, headCommit);
462
567
  if (isAncestor) {
463
568
  this.moveRef(this.head(), srcCommit);
464
569
  this.moveTo(srcCommit);
package/src/utils.test.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { test } from "tap";
2
- import { cleanAuthor } from "./utils";
2
+ import { cleanAuthor, mapPath, shaishToCommit } from "./utils";
3
+ import { Repository } from "./repository";
4
+ import { ComplexObject, testAuthor } from "./test.utils";
3
5
 
4
6
  test("author <email@domain.info>", (t) => {
5
7
  const [name, email] = cleanAuthor("author <email@domain.info>");
@@ -52,3 +54,88 @@ test("empty author", (t) => {
52
54
  );
53
55
  t.end();
54
56
  });
57
+
58
+ test("mapPath", async (t) => {
59
+ t.test("find root", async (t) => {
60
+ const repo = new Repository<ComplexObject>({}, {});
61
+ repo.data.name = "name";
62
+ const hash = await repo.commit("set name", testAuthor);
63
+
64
+ const { commits, refs } = repo.getHistory();
65
+ const [c] = shaishToCommit(hash, refs, commits);
66
+ const [isAncestor, path] = mapPath(commits, c);
67
+ t.equal(path.length, 1, "path does not contain 1 commit");
68
+ t.equal(isAncestor, true, "root is not ancestor of commit");
69
+ t.matchOnlyStrict(path[0], c, "path does not contain the right commit");
70
+ });
71
+
72
+ t.test("finds full path to root", async (t) => {
73
+ const repo = new Repository<ComplexObject>({}, {});
74
+ repo.data.name = "name";
75
+ const h1 = await repo.commit("set name", testAuthor);
76
+ repo.data.description = "description";
77
+ const h2 = await repo.commit("set desc", testAuthor);
78
+
79
+ const { commits, refs } = repo.getHistory();
80
+ const [c1] = shaishToCommit(h1, refs, commits);
81
+ const [c2] = shaishToCommit(h2, refs, commits);
82
+ const [isAncestor, path] = mapPath(commits, c2);
83
+
84
+ t.equal(path.length, 2, "path does not contain 1 commit");
85
+ t.equal(isAncestor, true, "root is not ancestor of commit");
86
+ t.matchOnlyStrict(
87
+ path[0],
88
+ c2,
89
+ "path does not contain the right commit at 0",
90
+ );
91
+ t.matchOnlyStrict(
92
+ path[1],
93
+ c1,
94
+ "path does not contain the right commit at 1",
95
+ );
96
+ });
97
+
98
+ t.test("parent-to-child no ancestor", async (t) => {
99
+ const repo = new Repository<ComplexObject>({}, {});
100
+ repo.data.name = "name";
101
+ const h1 = await repo.commit("set name", testAuthor);
102
+ repo.data.description = "description";
103
+ const h2 = await repo.commit("set desc", testAuthor);
104
+
105
+ const { commits, refs } = repo.getHistory();
106
+ const [c1] = shaishToCommit(h1, refs, commits);
107
+ const [c2] = shaishToCommit(h2, refs, commits);
108
+ const [isAncestor, path] = mapPath(commits, c1, c2);
109
+
110
+ t.equal(path.length, 0, "path contains a commit");
111
+ t.equal(isAncestor, false, "child must not be an ancestor of parent");
112
+ });
113
+
114
+ t.test("finds path across 2 branches", async (t) => {
115
+ const repo = new Repository<ComplexObject>({}, {});
116
+ repo.data.name = "name";
117
+ const h1 = await repo.commit("set name", testAuthor);
118
+ repo.checkout("branch", true);
119
+
120
+ repo.data.description = "description";
121
+ const h2 = await repo.commit("set desc", testAuthor);
122
+
123
+ const { commits, refs } = repo.getHistory();
124
+ const [c1] = shaishToCommit(h1, refs, commits);
125
+ const [c2] = shaishToCommit(h2, refs, commits);
126
+ const [isAncestor, path] = mapPath(commits, c2);
127
+
128
+ t.equal(path.length, 2, "path does not contain 1 commit");
129
+ t.equal(isAncestor, true, "root is not ancestor of commit");
130
+ t.matchOnlyStrict(
131
+ path[0],
132
+ c2,
133
+ "path does not contain the right commit at 0",
134
+ );
135
+ t.matchOnlyStrict(
136
+ path[1],
137
+ c1,
138
+ "path does not contain the right commit at 1",
139
+ );
140
+ });
141
+ });