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

fix: mutable remote refs #171

Merged
merged 2 commits into from
Apr 4, 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
53 changes: 48 additions & 5 deletions packages/ogre/src/repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ test("restore", async (t) => {
"incorrect # of changelog entries",
);
});
});

test("restoring from history", async (t) => {
test("history", async (t) => {
t.test("successful restore", async (t) => {
const [repo, obj] = await getBaseline();
updateHeaderData(obj);
await repo.commit("header data", testAuthor);
Expand All @@ -140,9 +142,51 @@ test("restore", async (t) => {

t.matchOnly(obj, obj2, "restored object does not equal last version.");
});
});

test("history", async (t) => {
t.test("remoteRefs doesn't change on commit", async (t) => {
const repo = new Repository<ComplexObject>({}, {});
updateHeaderData(repo.data);
await repo.commit("header data", testAuthor);

const history = repo.getHistory();

const r2 = new Repository<ComplexObject>({}, { history });
const remoteBeforeChange = r2.remote();

r2.data.name = "a different name";
await r2.commit("changed name", testAuthor);

const remoteAfterChange = r2.remote();

const history2 = r2.getHistory();

t.not(
remoteBeforeChange,
history.refs,
"input history refs and remote before change should not be the same object",
);
t.matchOnlyStrict(
remoteBeforeChange,
history.refs,
"remote before change is not the same as history input",
);
t.matchOnlyStrict(
remoteAfterChange,
remoteBeforeChange,
"remote before and after change are not the same",
);
t.notMatchOnlyStrict(
history2.refs,
remoteAfterChange,
"history refs must not be the same as static remotes",
);
t.notMatchOnlyStrict(
history.refs,
history2.refs,
"histories must not match anymore",
);
});

t.test("history contains HEAD ref", async (t) => {
const [repo] = await getBaseline();

Expand All @@ -161,10 +205,9 @@ test("history", async (t) => {
() =>
new Repository(co, {
history: {
original: co,
refs: new Map<string, Reference>(),
commits: [],
} as History,
},
}),
{
message: "unreachable: 'HEAD' is not present",
Expand Down
38 changes: 22 additions & 16 deletions packages/ogre/src/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import {
createHeadRefValue,
getLastRefPathElement,
headValueRefPrefix,
immutableMapCopy,
localHeadPathPrefix,
mapPath,
mutableMapCopy,
REFS_HEAD_KEY,
REFS_MAIN_KEY,
refsAtCommit,
Expand Down Expand Up @@ -92,9 +94,10 @@ export interface RepositoryObject<T extends { [k: string]: any }> {
reset(mode?: "soft" | "hard", shaish?: string): void;

/**
* Returns the remote references from the initialization of the repository
* Returns the remote references from the initialization of the repository.
* The returned map is a readonly of remote.
*/
remote(): Map<string, Reference> | undefined;
remote(): ReadonlyMap<string, Readonly<Reference>> | undefined;
}

/**
Expand All @@ -105,22 +108,23 @@ export class Repository<T extends { [k: PropertyKey]: any }>
{
constructor(obj: Partial<T>, options: RepositoryOptions<T>) {
// FIXME: move this to refs/remote as git would do?
this.remoteRefs = options.history?.refs;

this.remoteRefs = immutableMapCopy(options.history?.refs);
this.original = deepClone(obj);
// store js ref, so obj can still be modified without going through repo.data
this.data = obj as T;
this.observer = observe(obj as T);
this.refs =
options.history?.refs ??
new Map<string, Reference>([
[
REFS_HEAD_KEY,
{
name: REFS_HEAD_KEY,
value: `ref: ${REFS_MAIN_KEY}`,
},
],
]);
this.refs = options.history?.refs
? mutableMapCopy(options.history?.refs)
: new Map<string, Reference>([
[
REFS_HEAD_KEY,
{
name: REFS_HEAD_KEY,
value: `ref: ${REFS_MAIN_KEY}`,
},
],
]);

this.commits = options.history?.commits ?? [];

Expand All @@ -138,14 +142,16 @@ export class Repository<T extends { [k: PropertyKey]: any }>
data: T;

// stores the remote state upon initialization
private readonly remoteRefs: Map<string, Reference> | undefined;
private readonly remoteRefs:
| ReadonlyMap<string, Readonly<Reference>>
| undefined;

private observer: Observer<T>;

private readonly refs: Map<string, Reference>;
private readonly commits: Commit[];

remote(): Map<string, Reference> | undefined {
remote(): ReadonlyMap<string, Readonly<Reference>> | undefined {
return this.remoteRefs;
}

Expand Down
21 changes: 21 additions & 0 deletions packages/ogre/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,24 @@ export const printChange = (chg: Operation) => {
*/
export const getLastRefPathElement = (thePath: string) =>
thePath.substring(thePath.lastIndexOf("/") + 1);

export const immutableMapCopy = <T extends object>(
map: Map<string, T> | undefined,
) => {
if (!map) {
return undefined;
}
const m = new Map<string, Readonly<T>>();
for (const [key, value] of map) {
m.set(key, { ...value });
}
return m as ReadonlyMap<string, Readonly<T>>;
};

export const mutableMapCopy = <T extends object>(map: Map<string, T>) => {
const m = new Map<string, T>();
for (const [key, value] of map) {
m.set(key, { ...value });
}
return m;
};
Loading