Skip to content

Commit

Permalink
feat: status and apply (#156)
Browse files Browse the repository at this point in the history
<!-- GITHUB_RELEASE PR BODY: canary-version -->
<details>
  <summary>📦 Published PR as canary version: <code>Canary Versions</code></summary>
  <br />

  ✨ Test out this PR locally via:
  
  ```bash
  npm install @dotinc/ogre@0.6.0-canary.156.8124548292.0
  npm install @dotinc/ogre-react@0.6.0-canary.156.8124548292.0
  # or 
  yarn add @dotinc/ogre@0.6.0-canary.156.8124548292.0
  yarn add @dotinc/ogre-react@0.6.0-canary.156.8124548292.0
  ```
</details>
<!-- GITHUB_RELEASE PR BODY: canary-version -->
  • Loading branch information
nadilas committed Mar 2, 2024
2 parents c6bad3a + fe4ac3d commit bdc419a
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 25 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ keep the history around for a bit longer. The library uses json-patch RFC6902 fo
- Checkout
- Reset (soft and hard)
- Diff
- Status
- Apply
> ⚠️ Setting a value for an undefined prop `{prop: undefined}` will result in the `compare` call as a `replace`
operation, but will be recorded by the observer as
an `add` operation. See https://github.com/Starcounter-Jack/JSON-Patch/issues/280 for details
- Visualization via `@dotinc/ogre-react`
- Merge
- fast-forward
Expand Down
18 changes: 18 additions & 0 deletions packages/ogre/src/commit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
testAuthor,
updateHeaderData,
} from "./test.utils";
import { printChangeLog } from "./repository";

test("baseline with 1 commit and zero changelog entries", async (t) => {
const [repo] = await getBaseline();
Expand Down Expand Up @@ -40,6 +41,23 @@ test("no commit without changes after recent commit", async (t) => {
});
});

test("overwrite nested array changes are recognized", async (t) => {
const [repo] = await getBaseline();
repo.data.name = "new name";
await repo.commit("baseline", testAuthor);
repo.data.nested = [{ name: "new item", uuid: "asdf" }];
await repo.commit("overwrite nested array", testAuthor);
});

test("change of nested array element is recognized", async (t) => {
const [repo] = await getBaseline();
repo.data.name = "new name";
addOneStep(repo.data);
await repo.commit("baseline", testAuthor);
repo.data.nested[0].name = "another name which is different";
await repo.commit("changed nested array object", testAuthor);
});

test("treeHash of commit is matching content", async (t) => {
const [repo] = await getBaseline();
repo.data.name = "new name";
Expand Down
2 changes: 2 additions & 0 deletions packages/ogre/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export * from "./ref";
export * from "./size";
export * from "./utils";
export * from "./git2json";

export { compare, deepClone, Operation, JsonPatchError } from "fast-json-patch";
147 changes: 146 additions & 1 deletion packages/ogre/src/repository.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { test } from "tap";

import { Repository } from "./repository";
import { printChange, printChangeLog, Repository } from "./repository";
import {
addOneStep,
addOneStep as addOneNested,
ComplexObject,
getBaseline,
Expand All @@ -10,6 +11,7 @@ import {
updateHeaderData,
} from "./test.utils";
import { History, Reference } from "./interfaces";
import { compare, Operation } from "fast-json-patch";

test("diff is ok", async (t) => {
const [repo, obj] = await getBaseline();
Expand Down Expand Up @@ -188,3 +190,146 @@ test("reset", async (t) => {
t.equal(diff2.length, 0, "failed to reset");
});
});

test("status", async (t) => {
t.test("clean repo no change", async (t) => {
const [repo] = await getBaseline();
const cleanState = repo.status();
t.match(cleanState, [], "Shouldn't have pending changes");
});
t.test("clean repo pending change", async (t) => {
const [repo] = await getBaseline({ name: "base name" });
repo.data.name = "changed name";
const dirtyState = repo.status();
t.equal(dirtyState.length, 1, "Status doesn't contain changes");
});
t.test("reading status doesn't clean observer", async (t) => {
const [repo] = await getBaseline({ name: "base name" });
repo.data.name = "changed name";
const dirtyState = repo.status();
t.equal(dirtyState.length, 1, "Status doesn't contain changes");

const dirtyState2 = repo.status();
t.equal(dirtyState2.length, 1, "Status doesn't contain changes");
t.match(dirtyState2, dirtyState2, "different pending changes??");
});
t.test("after commit no change", async (t) => {
const [repo] = await getBaseline();
repo.data.name = "new name";
await repo.commit("baseline", testAuthor);
const cleanState = repo.status();
t.match(cleanState, [], "Shouldn't have pending changes");
});
t.test("after commit pending change", async (t) => {
const [repo] = await getBaseline();
repo.data.name = "new name";
await repo.commit("baseline", testAuthor);
const cleanState = repo.status();
t.match(cleanState, [], "Shouldn't have pending changes");
repo.data.name = "changed name";
const dirtyState = repo.status();
t.equal(dirtyState.length, 1, "Status doesn't contain changes");
});
t.test("after commit pending change for rewrite array", async (t) => {
const [repo] = await getBaseline();
repo.data.name = "new name";
await repo.commit("baseline", testAuthor);
const cleanState = repo.status();
t.match(cleanState, [], "Shouldn't have pending changes");
repo.data.nested = [{ name: "new item", uuid: "asdf" }];
const dirtyState = repo.status();
t.equal(dirtyState.length, 1, "Status doesn't contain changes");
});
t.test("change of nested array element prop", async (t) => {
const [repo] = await getBaseline();
repo.data.name = "new name";
addOneStep(repo.data);
await repo.commit("baseline", testAuthor);
const cleanState = repo.status();
t.match(cleanState, [], "Shouldn't have pending changes");
repo.data.nested[0].name = "another name which is different";
const dirtyState = repo.status();
t.equal(dirtyState?.length, 1, "Status doesn't contain changes");
});
});

test("apply", async (t) => {
t.test("single patch", async (t) => {
const [repo] = await getBaseline({ name: "base name" });
const cleanState = repo.status();
t.match(cleanState, [], "Shouldn't have pending changes");

const targetState = {
uuid: undefined,
name: "a name",
description: undefined,
nested: [{ name: "new item", uuid: "asdf" }],
};
const patches = compare(repo.data, targetState);
// this should record changes on the observer
const err = repo.apply(patches);
t.match(err, undefined, "Failed to apply patch");
t.match(repo.data, targetState, "The final state does not match up");
const dirtyState = repo.status();
t.equal(dirtyState.length, 2, "Status doesn't contain changes");
t.match(dirtyState, patches, "It should have the right changes");
});

t.test(
"patch for undefined props doesn't work as expected https://github.com/Starcounter-Jack/JSON-Patch/issues/280",
async (t) => {
const [repo] = await getBaseline();
const cleanState = repo.status();
t.match(cleanState, [], "Shouldn't have pending changes");

const targetState: ComplexObject = {
uuid: undefined,
description: undefined,
name: "a name",
nested: [],
};
const patches = compare(repo.data, targetState);
// this should record changes on the observer
const err = repo.apply(patches);
t.match(
err,
{
name: "OPERATION_PATH_UNRESOLVABLE",
operation: {
op: "replace",
path: "/name",
value: "a name",
},
},
"Failed to apply patch",
);
t.notMatch(
repo.data,
targetState,
"The final state shoould not match up, because patch failed",
);
const dirtyState = repo.status();
t.equal(dirtyState.length, 0, "Status doesn't contain changes");
},
);

t.test("multiple patches", async (t) => {
const [repo] = await getBaseline({ name: "base name" });

const cleanState = repo.status();
t.match(cleanState, [], "Shouldn't have pending changes");
const targetState = {
uuid: undefined,
name: "a name",
description: undefined,
nested: [{ name: "new item", uuid: "asdf" }],
};
const patches = compare(repo.data, targetState);
const err = repo.apply(patches);
t.equal(err, undefined, "Failed to apply patch");
const dirtyState = repo.status();
t.equal(dirtyState?.length, 2, "Status doesn't contain changes");
t.match(dirtyState, patches, "It should have the right changes");
t.match(repo.data, targetState, "The final state does not match up");
});
});
72 changes: 51 additions & 21 deletions packages/ogre/src/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
generate,
Observer,
Operation,
validate,
applyPatch,
JsonPatchError,
} from "fast-json-patch";
import { calculateCommitHash, Commit } from "./commit";
import { History, Reference } from "./interfaces";
Expand Down Expand Up @@ -37,6 +40,17 @@ export interface RepositoryObject<T extends { [k: string]: any }> {
*/
diff(shaishFrom: string, shaishTo?: string): Operation[];

/**
* Returns pending changes.
*/
status(): Operation[];

/**
* Applies a patch to the repository's HEAD
* @param patch
*/
apply(patch: Operation[]): void;

// It returns the reference where we are currently at
head(): string;

Expand Down Expand Up @@ -116,11 +130,20 @@ export class Repository<T extends { [k: PropertyKey]: any }>
if (!patchToTarget || patchToTarget.length < 1) {
return;
}
unobserve(this.data, this.observer);
this.observer.unobserve();
patchToTarget.reduce(applyReducer, this.data);
this.observer = observe(this.data);
}

apply(patch: Operation[]): JsonPatchError | undefined {
const err = validate(patch, this.data);
if (err) {
return err;
}
applyPatch(this.data, patch);
// const changed = patch.reduce(applyReducer, this.data);
}

reset(
mode: "soft" | "hard" | undefined = "hard",
shaish: string | undefined = REFS_HEAD_KEY,
Expand Down Expand Up @@ -161,6 +184,15 @@ export class Repository<T extends { [k: PropertyKey]: any }>
return REFS_HEAD_KEY; // detached state
}

status() {
const commit = this.commitAtHead();
if (!commit) {
// on root repo return the pending changes
return compare(this.original, this.data); // this.observer.patches is empty?? :(
}
return this.diff(commit.hash);
}

diff(shaishFrom: string, shaishTo?: string): Operation[] {
const [cFrom] = shaishToCommit(shaishFrom, this.refs, this.commits);
let target: T;
Expand Down Expand Up @@ -455,21 +487,6 @@ const treeToObject = <T = any>(tree: string): T => {
const getLastItem = (thePath: string) =>
thePath.substring(thePath.lastIndexOf("/") + 1);

/**
* Traverses the commit tree backwards and reassembles the changelog
* @param commit
* @param commitsList
*/
const traverseAndCollectChangelog = (commit: Commit, commitsList: Commit[]) => {
let c: Commit | undefined = commit;
let clog: Operation[] = [];
while (c !== undefined) {
clog = [...commit.changes, ...clog];
c = commitsList.find((parent) => parent.hash === c?.parent);
}
return clog;
};

const mapPath = (
from: Commit,
to: Commit,
Expand Down Expand Up @@ -613,16 +630,29 @@ export const printChangeLog = <T extends { [k: string]: any }>(
repository: RepositoryObject<T>,
) => {
console.log("----------------------------------------------------------");
console.log(`Changelog at ${repository.head()}`);
console.log("Changelog");
console.log("----------------------------------------------------------");
const history = repository.getHistory();
const head = commitAtRefIn(repository.head(), history.refs, history.commits);
if (!head) {
throw new Error(`fatal: HEAD is not defined`);
}
const changeLog = traverseAndCollectChangelog(head, history.commits);
for (const [, chg] of changeLog.entries()) {
console.log(` ${JSON.stringify(chg)}`);
let c: Commit | undefined = head;
while (c) {
console.log(
`${c.hash} ${refsAtCommit(history.refs, c)
.map((r) => r.name)
.join(" ")}`,
);
for (const chg of c.changes) {
printChange(chg);
}
c = history.commits.find((parent) => parent.hash === c?.parent);
}

console.log("End of changelog");
console.log("----------------------------------------------------------");
};

export const printChange = (chg: Operation) => {
console.log(` ${JSON.stringify(chg)}`);
};
7 changes: 4 additions & 3 deletions packages/ogre/src/test.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ export type ComplexObject = {

export const testAuthor = "User name <name@domain.com>";

export async function getBaseline(): Promise<
[RepositoryObject<ComplexObject>, ComplexObject]
> {
export async function getBaseline(
obj?: Partial<ComplexObject>,
): Promise<[RepositoryObject<ComplexObject>, ComplexObject]> {
const co: ComplexObject = {
uuid: undefined,
name: undefined,
description: undefined,
nested: [],
...obj,
};
const repo = new Repository(co, {});
return [repo, co];
Expand Down

0 comments on commit bdc419a

Please sign in to comment.