Files
GitLite/src/lib/integrations.ts
T
Christoph c242a72edd feat(integrations): support GitHub repositories and enhance repo browser UI
Add GitHub repository support to integrations and update related tests.
Implement server-side GitHub API calls and normalize GitHub base URLs.
Improve the repository browser UI with compact tabs and updated icons.
Add a custom, accessible scrollbar with pointer and keyboard support.

- Add paging, auth headers, and error handling when listing GitHub repos.
- Default GitHub token username to "x-access-token" when saving credentials.
- Introduce compact repo tab styles, new icons, and a draggable scrollbar.
2026-08-29 23:55:27 +02:00

170 lines
5.6 KiB
TypeScript

import type {
AzureDevOpsOrganization,
GitIntegrationConfig,
GitIntegrationProvider,
GitIntegrationSource,
GitIntegrationSettings,
} from "./types";
export const gitIntegrationProviders: GitIntegrationProvider[] = [
"github",
"gitlab",
"gitlab-self-hosted",
"azure-devops",
"gitea",
];
const defaults: Record<GitIntegrationProvider, Omit<GitIntegrationConfig, "provider">> = {
github: {
enabled: false,
baseUrl: "https://github.com",
username: "",
tokenStored: false,
},
gitlab: {
enabled: false,
baseUrl: "https://gitlab.com",
username: "",
tokenStored: false,
},
"gitlab-self-hosted": {
enabled: false,
baseUrl: "",
username: "",
tokenStored: false,
},
"azure-devops": {
enabled: false,
baseUrl: "https://dev.azure.com/",
username: "",
tokenStored: false,
},
gitea: {
enabled: false,
baseUrl: "",
username: "",
tokenStored: false,
},
};
export function defaultGitIntegrationSettings(): GitIntegrationSettings {
return {
providers: Object.fromEntries(
gitIntegrationProviders.map((provider) => [provider, { provider, ...defaults[provider] }]),
) as GitIntegrationSettings["providers"],
azureDevOpsOrganizations: [],
};
}
function normaliseAzureOrganization(value: unknown): AzureDevOpsOrganization | null {
if (!value || typeof value !== "object") return null;
const stored = value as Partial<AzureDevOpsOrganization>;
const id = typeof stored.id === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(stored.id) ? stored.id : "";
if (!id) return null;
return {
id,
name: typeof stored.name === "string" ? stored.name : "",
enabled: stored.enabled === true,
baseUrl: typeof stored.baseUrl === "string" ? stored.baseUrl : "",
username: typeof stored.username === "string" ? stored.username : "",
tokenStored: stored.tokenStored === true,
};
}
export function normaliseGitIntegrationSettings(value: unknown): GitIntegrationSettings {
const fallback = defaultGitIntegrationSettings();
if (!value || typeof value !== "object") return fallback;
const storedProviders = (value as Partial<GitIntegrationSettings>).providers;
if (!storedProviders || typeof storedProviders !== "object") return fallback;
for (const provider of gitIntegrationProviders) {
const stored = storedProviders[provider] as Partial<GitIntegrationConfig> | undefined;
if (!stored || typeof stored !== "object") continue;
fallback.providers[provider] = {
provider,
enabled: stored.enabled === true,
baseUrl: typeof stored.baseUrl === "string" ? stored.baseUrl : fallback.providers[provider].baseUrl,
username: typeof stored.username === "string" ? stored.username : "",
tokenStored: stored.tokenStored === true,
};
}
const storedOrganizations = (value as Partial<GitIntegrationSettings>).azureDevOpsOrganizations;
if (Array.isArray(storedOrganizations)) {
const seen = new Set<string>();
fallback.azureDevOpsOrganizations = storedOrganizations
.map(normaliseAzureOrganization)
.filter((organization): organization is AzureDevOpsOrganization => {
if (!organization || seen.has(organization.id)) return false;
seen.add(organization.id);
return true;
});
} else {
const legacy = fallback.providers["azure-devops"];
const hasLegacyConfiguration = legacy.enabled || legacy.tokenStored || legacy.username.trim().length > 0 || !/^https:\/\/dev\.azure\.com\/?$/i.test(legacy.baseUrl.trim());
if (hasLegacyConfiguration) {
fallback.azureDevOpsOrganizations = [{
id: "default",
name: "Azure DevOps",
enabled: legacy.enabled,
baseUrl: legacy.baseUrl,
username: legacy.username,
tokenStored: legacy.tokenStored,
}];
}
}
return fallback;
}
export function integrationCredentialKey(provider: GitIntegrationProvider, accountId?: string): string {
if (provider === "azure-devops" && accountId && accountId !== "default") {
return `integration:${provider}:${accountId}`;
}
return `integration:${provider}`;
}
export function configuredIntegrationSources(settings: GitIntegrationSettings): GitIntegrationSource[] {
const sources: GitIntegrationSource[] = [];
for (const provider of gitIntegrationProviders) {
if (provider === "azure-devops") continue;
const config = settings.providers[provider];
if (config.enabled && config.tokenStored && config.baseUrl.trim()) {
sources.push({ id: provider, provider, label: providerLabel(provider), baseUrl: config.baseUrl });
}
}
for (const organization of settings.azureDevOpsOrganizations) {
if (!organization.enabled || !organization.tokenStored || !organization.baseUrl.trim()) continue;
sources.push({
id: `azure-devops:${organization.id}`,
provider: "azure-devops",
accountId: organization.id,
label: organization.name.trim() || organizationNameFromUrl(organization.baseUrl) || "Azure DevOps",
baseUrl: organization.baseUrl,
});
}
return sources;
}
export function configuredIntegrationCount(settings: GitIntegrationSettings): number {
return configuredIntegrationSources(settings).length;
}
export function providerLabel(provider: GitIntegrationProvider): string {
return {
github: "GitHub",
gitlab: "GitLab.com",
"gitlab-self-hosted": "GitLab Self-Managed",
"azure-devops": "Azure DevOps",
gitea: "Gitea",
}[provider];
}
export function organizationNameFromUrl(value: string): string {
try {
const url = new URL(value);
return url.pathname.split("/").filter(Boolean)[0] ?? "";
} catch {
return "";
}
}