Skip to content
Open
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
66 changes: 66 additions & 0 deletions packages/core/src/integrations/repositories.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { describe, expect, it } from "vitest";
import {
buildTeamRepositoryOptions,
buildUserRepositoryOptions,
combineGithubRepositories,
combineRepositoryPicker,
combineUserGithubRepositories,
getIntegrationIdForRepo,
isEmptyRepositoryMap,
isRepoInIntegration,
normalizeRepoKey,
normalizeRepositoryNames,
type RepositoryCacheAction,
type RepositoryQueryResult,
repositoryLoadWarning,
repositoryOptionsEqual,
resolveEffectiveUserRepositoryMap,
resolveUserRepositoryCacheAction,
sameUserRepositoryMap,
Expand All @@ -18,6 +23,67 @@ import {
type UserRepositoryIntegrationRef,
} from "./repositories";

describe("repository options", () => {
it("normalizes repository names", () => {
expect(normalizeRepositoryNames(["PostHog/Code", ""])).toEqual([
"posthog/code",
]);
});

it("builds sorted team options with integration labels", () => {
expect(
buildTeamRepositoryOptions(
[
{ id: 2, display_name: "Work" },
{ id: 1, config: { account: { login: "personal" } } },
],
{ 1: ["z/repo"], 2: ["a/repo"] },
),
).toEqual([
{ integrationId: 2, integrationLabel: "Work", repository: "a/repo" },
{
integrationId: 1,
integrationLabel: "personal",
repository: "z/repo",
},
]);
});

it("builds user options with the same shape", () => {
expect(
buildUserRepositoryOptions(
[{ id: "user-1", installation_id: "42", account: { name: "Me" } }],
{ 42: ["posthog/code"] },
),
).toEqual([
{ integrationId: 42, integrationLabel: "Me", repository: "posthog/code" },
]);
});

it.each([
[0, 2, null],
[1, 2, "Some GitHub repositories could not be loaded. Pull to retry."],
[2, 2, "Could not load GitHub repositories. Pull to retry."],
])(
"describes %i of %i failed repository loads",
(failed, total, expected) => {
expect(repositoryLoadWarning(failed, total)).toBe(expected);
},
);

it("compares option lists by content", () => {
const options = [
{ integrationId: 1, integrationLabel: "Me", repository: "a/repo" },
];
expect(
repositoryOptionsEqual(
options,
options.map((option) => ({ ...option })),
),
).toBe(true);
});
});

function result<T>(
data: T | undefined,
flags: Partial<Omit<RepositoryQueryResult<T>, "data">> = {},
Expand Down
124 changes: 124 additions & 0 deletions packages/core/src/integrations/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,130 @@ export interface RepositoryQueryResult<TData> {
isRefetching: boolean;
}

export interface RepositoryOption {
integrationId: number;
integrationLabel: string;
repository: string;
}

export interface RepositorySelection {
integrationId: number | null;
repository: string | null;
}

export interface TeamRepositoryIntegration {
id: number;
display_name?: string;
config?: { account?: { login?: string } };
}

export interface UserRepositoryIntegration {
id: string;
installation_id: string;
account?: { name?: string | null } | null;
}

export function normalizeRepositoryNames(
repositories: ReadonlyArray<string>,
): string[] {
return repositories
.map((repository) => repository.toLowerCase())
.filter((repository) => repository.length > 0);
}

export function repositoryLoadWarning(
failedCount: number,
totalCount: number,
): string | null {
if (failedCount === 0) return null;
return failedCount === totalCount
? "Could not load GitHub repositories. Pull to retry."
: "Some GitHub repositories could not be loaded. Pull to retry.";
}

export function buildTeamRepositoryOptions(
integrations: ReadonlyArray<TeamRepositoryIntegration>,
repositoriesByIntegration: Readonly<Record<number, string[]>>,
): RepositoryOption[] {
return integrations
.flatMap((integration) =>
(repositoriesByIntegration[integration.id] ?? []).map((repository) => ({
integrationId: integration.id,
integrationLabel:
integration.display_name ??
integration.config?.account?.login ??
`GitHub ${integration.id}`,
repository,
})),
)
.sort((left, right) => left.repository.localeCompare(right.repository));
}

export function buildUserRepositoryOptions(
integrations: ReadonlyArray<UserRepositoryIntegration>,
repositoriesByInstallation: Readonly<Record<string, string[]>>,
): RepositoryOption[] {
return integrations
.flatMap((integration) =>
(repositoriesByInstallation[integration.installation_id] ?? []).map(
(repository) => ({
integrationId: Number(integration.installation_id),
integrationLabel:
integration.account?.name ??
`GitHub ${integration.installation_id}`,
repository,
}),
),
)
.sort((left, right) => left.repository.localeCompare(right.repository));
}

export function repositoryOptionsEqual(
left: ReadonlyArray<RepositoryOption>,
right: ReadonlyArray<RepositoryOption>,
): boolean {
return (
left.length === right.length &&
left.every((option, index) => {
const other = right[index];
return (
other?.integrationId === option.integrationId &&
other.integrationLabel === option.integrationLabel &&
other.repository === option.repository
);
})
);
}

export function findRepositoryOption(
options: ReadonlyArray<RepositoryOption>,
selection: RepositorySelection,
): RepositoryOption | null {
if (!selection.integrationId || !selection.repository) return null;
return (
options.find(
(option) =>
option.integrationId === selection.integrationId &&
option.repository === selection.repository,
) ?? null
);
}

export function toRepositorySelection(
option: RepositoryOption | null,
): RepositorySelection {
return {
integrationId: option?.integrationId ?? null,
repository: option?.repository ?? null,
};
}

export function isRepositorySelectionComplete(
selection: RepositorySelection,
): boolean {
return !!selection.integrationId && !!selection.repository;
}

export interface TeamRepositoriesResult {
integrationId: number;
repos?: string[] | null;
Expand Down
Loading