Skip to content
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
64 changes: 60 additions & 4 deletions src/gui/UpdateModal/UpdateModal.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { App } from "obsidian";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getReleaseNotesAfter, renderVideoAttachments } from "./UpdateModal";
import {
getReleaseNotesAfter,
renderVideoAttachments,
UpdateModal,
} from "./UpdateModal";

const requestUrlMock = vi.hoisted(() => vi.fn());

Expand Down Expand Up @@ -185,10 +190,61 @@ describe("getReleaseNotesAfter", () => {
);
});

it("throws a clear error when the start tag is absent from the releases array", async () => {
it("returns every stable release newer than a version missing from the list", async () => {
// A user upgrading from a version older than the fetched page (e.g. 1.18.1)
// must still get notes, not a "could not find release" failure.
const release = (tag_name: string, extra = {}) => ({
tag_name,
body: tag_name,
draft: false,
prerelease: false,
...extra,
});
mockResponse(200, [
release("2.10.0"),
release("2.9.5", { prerelease: true }),
release("2.9.4", { draft: true }),
release("2.9.0"),
release("not-a-version"),
release("1.18.0"),
]);

const releases = await getReleaseNotesAfter("chhoumann", "quickadd", "1.18.1");

expect(releases.map((r) => r.tag_name)).toEqual(["2.10.0", "2.9.0"]);
});

it("compares versions numerically, not as strings", async () => {
mockResponse(200, [
{ tag_name: "2.10.0", body: "", draft: false, prerelease: false },
{ tag_name: "2.9.0", body: "", draft: false, prerelease: false },
]);

const releases = await getReleaseNotesAfter("chhoumann", "quickadd", "2.9.0");

expect(releases.map((r) => r.tag_name)).toEqual(["2.10.0"]);
});

it("rejects a previous version that is not valid semver", async () => {
mockResponse(200, [{ tag_name: "9.9.9", body: "", draft: false, prerelease: false }]);
await expect(getReleaseNotesAfter("chhoumann", "quickadd", "1.0.0")).rejects.toThrow(
"Could not find release with tag 1.0.0",
await expect(getReleaseNotesAfter("chhoumann", "quickadd", "garbage")).rejects.toThrow(
"Invalid version garbage",
);
});
});

describe("UpdateModal", () => {
beforeEach(() => {
requestUrlMock.mockReset();
});

it("closes instead of staying on the loading state when the fetch fails", async () => {
mockResponse(403, { message: "API rate limit exceeded" });

const modal = new UpdateModal(new App() as never, "2.0.0");
modal.open();
expect(modal.contentEl.textContent).toContain("Fetching release notes...");

await vi.waitFor(() => expect(modal.containerEl.isConnected).toBe(false));
});
});
42 changes: 21 additions & 21 deletions src/gui/UpdateModal/UpdateModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
requestUrl,
} from "obsidian";
import { log } from "src/logger/logManager";
import { compareSemver, parseSemver } from "src/utils/semver";

type Release = {
tag_name: string;
Expand All @@ -17,23 +18,24 @@ type Release = {
};

/**
* Fetches the releases for a repository on GitHub and returns the release notes for every release
* that comes after a specific release.
* Fetches the releases for a repository on GitHub and returns every stable release
* whose version is newer than the given one, newest first.
*
* @param repoOwner The owner of the repository.
* @param repoName The name of the repository.
* @param releaseTagName The tag name of the release to start getting release notes from.
* @param releaseTagName The version the user is upgrading from.
* @returns An array of Release objects, each containing the tag name and release notes for a single release.
* @throws An error if there was an error fetching the releases or if the release with the specified tag name
* could not be found.
* @throws An error if the releases could not be fetched or the given version is not valid semver.
*/
export async function getReleaseNotesAfter(
repoOwner: string,
repoName: string,
releaseTagName: string
): Promise<Release[]> {
const response = await requestUrl({
url: `https://api.github.com/repos/${repoOwner}/${repoName}/releases`,
// The newest 100 releases (GitHub's page-size cap). Anyone further behind
// sees the latest 100.
url: `https://api.github.com/repos/${repoOwner}/${repoName}/releases?per_page=100`,
throw: false,
});

Expand All @@ -58,19 +60,16 @@ export async function getReleaseNotesAfter(
throw new Error(`Failed to fetch releases: ${message}`);
}

const releases = body as Release[];

const startReleaseIdx = releases.findIndex(
(release) => release.tag_name === releaseTagName
);

if (startReleaseIdx === -1) {
throw new Error(`Could not find release with tag ${releaseTagName}`);
const previous = parseSemver(releaseTagName);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!previous) {
throw new Error(`Invalid version ${releaseTagName}`);
}

return releases
.slice(0, startReleaseIdx)
.filter((release) => !release.draft && !release.prerelease);
return (body as Release[]).filter((release) => {
if (release.draft || release.prerelease) return false;
const version = parseSemver(release.tag_name);
return version !== null && compareSemver(version, previous) > 0;
Comment thread
chhoumann marked this conversation as resolved.
Comment thread
chhoumann marked this conversation as resolved.
});
}

const USER_ATTACHMENT_VIDEO_URL =
Expand Down Expand Up @@ -204,15 +203,16 @@ export class UpdateModal extends Modal {

this.releases = releases;

if (this.releases.length === 0) {
this.close();
return;
}
if (this.releases.length === 0) {
this.close();
return;
}

this.display();
})
.catch((err) => {
log.logError(`Failed to fetch release notes: ${err as string}`);
if (!this.isClosed) this.close();
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/gui/apiModernization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("API modernization", () => {
},
]);
expect(requestUrlSpy).toHaveBeenCalledWith({
url: "https://api.github.com/repos/owner/repo/releases",
url: "https://api.github.com/repos/owner/repo/releases?per_page=100",
throw: false,
});

Expand Down
8 changes: 8 additions & 0 deletions src/utils/semver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,11 @@ export function isMajorUpdate(
return current.major > previous.major;
}


/**
* Orders two parsed versions: negative if `a` is older than `b`, positive if
* newer, 0 if equal.
*/
export function compareSemver(a: ParsedVersion, b: ParsedVersion): number {
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
}
Loading