Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: status and apply #156

Merged
merged 1 commit into from
Mar 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
*/
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 @@
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 @@
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 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 @@
repository: RepositoryObject<T>,
) => {
console.log("----------------------------------------------------------");
console.log(`Changelog at ${repository.head()}`);
console.log("Changelog");
console.log("----------------------------------------------------------");

Check warning on line 634 in packages/ogre/src/repository.ts

View check run for this annotation

Codecov / codecov/patch

packages/ogre/src/repository.ts#L633-L634

Added lines #L633 - L634 were not covered by tests
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(

Check warning on line 642 in packages/ogre/src/repository.ts

View check run for this annotation

Codecov / codecov/patch

packages/ogre/src/repository.ts#L640-L642

Added lines #L640 - L642 were not covered by tests
`${c.hash} ${refsAtCommit(history.refs, c)
.map((r) => r.name)

Check warning on line 644 in packages/ogre/src/repository.ts

View check run for this annotation

Codecov / codecov/patch

packages/ogre/src/repository.ts#L644

Added line #L644 was not covered by tests
.join(" ")}`,
);
for (const chg of c.changes) {
printChange(chg);

Check warning on line 648 in packages/ogre/src/repository.ts

View check run for this annotation

Codecov / codecov/patch

packages/ogre/src/repository.ts#L647-L648

Added lines #L647 - L648 were not covered by tests
}
c = history.commits.find((parent) => parent.hash === c?.parent);
}

console.log("End of changelog");

Check warning on line 652 in packages/ogre/src/repository.ts

View check run for this annotation

Codecov / codecov/patch

packages/ogre/src/repository.ts#L652

Added line #L652 was not covered by tests
console.log("----------------------------------------------------------");
};

export const printChange = (chg: Operation) => {
console.log(` ${JSON.stringify(chg)}`);

Check warning on line 657 in packages/ogre/src/repository.ts

View check run for this annotation

Codecov / codecov/patch

packages/ogre/src/repository.ts#L657

Added line #L657 was not covered by tests
};
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
Loading