Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 343feed

Browse files
refactor(core): extract repository integration semantics
Generated-By: PostHog Code Task-Id: c1bbe3cf-742b-4b24-bf96-d11a18b4cf22
1 parent e1af136 commit 343feed

2 files changed

Lines changed: 190 additions & 0 deletions

File tree

packages/core/src/integrations/repositories.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { describe, expect, it } from "vitest";
22
import {
3+
buildTeamRepositoryOptions,
4+
buildUserRepositoryOptions,
35
combineGithubRepositories,
46
combineRepositoryPicker,
57
combineUserGithubRepositories,
68
getIntegrationIdForRepo,
79
isEmptyRepositoryMap,
810
isRepoInIntegration,
911
normalizeRepoKey,
12+
normalizeRepositoryNames,
1013
type RepositoryCacheAction,
1114
type RepositoryQueryResult,
15+
repositoryLoadWarning,
16+
repositoryOptionsEqual,
1217
resolveEffectiveUserRepositoryMap,
1318
resolveUserRepositoryCacheAction,
1419
sameUserRepositoryMap,
@@ -18,6 +23,67 @@ import {
1823
type UserRepositoryIntegrationRef,
1924
} from "./repositories";
2025

26+
describe("repository options", () => {
27+
it("normalizes repository names", () => {
28+
expect(normalizeRepositoryNames(["PostHog/Code", ""])).toEqual([
29+
"posthog/code",
30+
]);
31+
});
32+
33+
it("builds sorted team options with integration labels", () => {
34+
expect(
35+
buildTeamRepositoryOptions(
36+
[
37+
{ id: 2, display_name: "Work" },
38+
{ id: 1, config: { account: { login: "personal" } } },
39+
],
40+
{ 1: ["z/repo"], 2: ["a/repo"] },
41+
),
42+
).toEqual([
43+
{ integrationId: 2, integrationLabel: "Work", repository: "a/repo" },
44+
{
45+
integrationId: 1,
46+
integrationLabel: "personal",
47+
repository: "z/repo",
48+
},
49+
]);
50+
});
51+
52+
it("builds user options with the same shape", () => {
53+
expect(
54+
buildUserRepositoryOptions(
55+
[{ id: "user-1", installation_id: "42", account: { name: "Me" } }],
56+
{ 42: ["posthog/code"] },
57+
),
58+
).toEqual([
59+
{ integrationId: 42, integrationLabel: "Me", repository: "posthog/code" },
60+
]);
61+
});
62+
63+
it.each([
64+
[0, 2, null],
65+
[1, 2, "Some GitHub repositories could not be loaded. Pull to retry."],
66+
[2, 2, "Could not load GitHub repositories. Pull to retry."],
67+
])(
68+
"describes %i of %i failed repository loads",
69+
(failed, total, expected) => {
70+
expect(repositoryLoadWarning(failed, total)).toBe(expected);
71+
},
72+
);
73+
74+
it("compares option lists by content", () => {
75+
const options = [
76+
{ integrationId: 1, integrationLabel: "Me", repository: "a/repo" },
77+
];
78+
expect(
79+
repositoryOptionsEqual(
80+
options,
81+
options.map((option) => ({ ...option })),
82+
),
83+
).toBe(true);
84+
});
85+
});
86+
2187
function result<T>(
2288
data: T | undefined,
2389
flags: Partial<Omit<RepositoryQueryResult<T>, "data">> = {},

packages/core/src/integrations/repositories.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,130 @@ export interface RepositoryQueryResult<TData> {
55
isRefetching: boolean;
66
}
77

8+
export interface RepositoryOption {
9+
integrationId: number;
10+
integrationLabel: string;
11+
repository: string;
12+
}
13+
14+
export interface RepositorySelection {
15+
integrationId: number | null;
16+
repository: string | null;
17+
}
18+
19+
export interface TeamRepositoryIntegration {
20+
id: number;
21+
display_name?: string;
22+
config?: { account?: { login?: string } };
23+
}
24+
25+
export interface UserRepositoryIntegration {
26+
id: string;
27+
installation_id: string;
28+
account?: { name?: string | null } | null;
29+
}
30+
31+
export function normalizeRepositoryNames(
32+
repositories: ReadonlyArray<string>,
33+
): string[] {
34+
return repositories
35+
.map((repository) => repository.toLowerCase())
36+
.filter((repository) => repository.length > 0);
37+
}
38+
39+
export function repositoryLoadWarning(
40+
failedCount: number,
41+
totalCount: number,
42+
): string | null {
43+
if (failedCount === 0) return null;
44+
return failedCount === totalCount
45+
? "Could not load GitHub repositories. Pull to retry."
46+
: "Some GitHub repositories could not be loaded. Pull to retry.";
47+
}
48+
49+
export function buildTeamRepositoryOptions(
50+
integrations: ReadonlyArray<TeamRepositoryIntegration>,
51+
repositoriesByIntegration: Readonly<Record<number, string[]>>,
52+
): RepositoryOption[] {
53+
return integrations
54+
.flatMap((integration) =>
55+
(repositoriesByIntegration[integration.id] ?? []).map((repository) => ({
56+
integrationId: integration.id,
57+
integrationLabel:
58+
integration.display_name ??
59+
integration.config?.account?.login ??
60+
`GitHub ${integration.id}`,
61+
repository,
62+
})),
63+
)
64+
.sort((left, right) => left.repository.localeCompare(right.repository));
65+
}
66+
67+
export function buildUserRepositoryOptions(
68+
integrations: ReadonlyArray<UserRepositoryIntegration>,
69+
repositoriesByInstallation: Readonly<Record<string, string[]>>,
70+
): RepositoryOption[] {
71+
return integrations
72+
.flatMap((integration) =>
73+
(repositoriesByInstallation[integration.installation_id] ?? []).map(
74+
(repository) => ({
75+
integrationId: Number(integration.installation_id),
76+
integrationLabel:
77+
integration.account?.name ??
78+
`GitHub ${integration.installation_id}`,
79+
repository,
80+
}),
81+
),
82+
)
83+
.sort((left, right) => left.repository.localeCompare(right.repository));
84+
}
85+
86+
export function repositoryOptionsEqual(
87+
left: ReadonlyArray<RepositoryOption>,
88+
right: ReadonlyArray<RepositoryOption>,
89+
): boolean {
90+
return (
91+
left.length === right.length &&
92+
left.every((option, index) => {
93+
const other = right[index];
94+
return (
95+
other?.integrationId === option.integrationId &&
96+
other.integrationLabel === option.integrationLabel &&
97+
other.repository === option.repository
98+
);
99+
})
100+
);
101+
}
102+
103+
export function findRepositoryOption(
104+
options: ReadonlyArray<RepositoryOption>,
105+
selection: RepositorySelection,
106+
): RepositoryOption | null {
107+
if (!selection.integrationId || !selection.repository) return null;
108+
return (
109+
options.find(
110+
(option) =>
111+
option.integrationId === selection.integrationId &&
112+
option.repository === selection.repository,
113+
) ?? null
114+
);
115+
}
116+
117+
export function toRepositorySelection(
118+
option: RepositoryOption | null,
119+
): RepositorySelection {
120+
return {
121+
integrationId: option?.integrationId ?? null,
122+
repository: option?.repository ?? null,
123+
};
124+
}
125+
126+
export function isRepositorySelectionComplete(
127+
selection: RepositorySelection,
128+
): boolean {
129+
return !!selection.integrationId && !!selection.repository;
130+
}
131+
8132
export interface TeamRepositoriesResult {
9133
integrationId: number;
10134
repos?: string[] | null;

0 commit comments

Comments
 (0)