feat(integrations): add Git hosting integrations and repo listing
Add support for integrating with external Git hosts (GitLab, Gitea, and Azure DevOps). The backend gains a client to fetch paginated repository lists, normalise base URLs, and surface provider errors. Credentials are loaded from the OS keychain and a Tauri command is exposed for the frontend to list integration repositories. - Implement integration client with pagination, deserialization, and provider-specific handling. - Centralise keychain credential loading and expose listing command. - Update UI to manage integration metadata, persist settings, and save/remove tokens to the OS keychain for cloning and operations.
This commit is contained in:
+65
-2
@@ -117,6 +117,7 @@
|
||||
launchExternalMerge,
|
||||
launchExternalTool,
|
||||
credLoad,
|
||||
credDelete,
|
||||
credSave,
|
||||
getFilePatch,
|
||||
readConflict,
|
||||
@@ -158,6 +159,9 @@
|
||||
ExplorerNodeKind,
|
||||
ExternalDiffScope,
|
||||
ExternalToolsSettings,
|
||||
GitIntegrationSecretUpdate,
|
||||
GitIntegrationSettings,
|
||||
GitIntegrationProvider,
|
||||
GitBlameLine,
|
||||
GitBranch as GitBranchInfo,
|
||||
GitCommit,
|
||||
@@ -190,6 +194,11 @@
|
||||
normaliseExternalToolsSettings,
|
||||
resolveDetectedExternalToolPrograms,
|
||||
} from "./lib/externalTools";
|
||||
import {
|
||||
defaultGitIntegrationSettings,
|
||||
integrationCredentialKey,
|
||||
normaliseGitIntegrationSettings,
|
||||
} from "./lib/integrations";
|
||||
|
||||
import {
|
||||
orgKeyFromUrl,
|
||||
@@ -259,6 +268,7 @@
|
||||
const CUSTOM_THEME_KEY = "gitlite.customTheme.v1";
|
||||
const APP_LANGUAGE_KEY = "gitlite.language.v1";
|
||||
const EXTERNAL_TOOLS_SETTINGS_KEY = "gitlite.externalTools.v1";
|
||||
const GIT_INTEGRATIONS_SETTINGS_KEY = "gitlite.integrations.v1";
|
||||
const AUTO_REFRESH_ENABLED_KEY = "gitlite.autoRefreshEnabled.v1";
|
||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
|
||||
@@ -372,6 +382,7 @@
|
||||
let customTheme: CustomThemeColors = loadCustomTheme();
|
||||
let appLanguage: AppLanguage = loadLanguagePreference();
|
||||
let externalToolsSettings: ExternalToolsSettings = loadExternalToolsSettings();
|
||||
let gitIntegrationSettings: GitIntegrationSettings = loadGitIntegrationSettings();
|
||||
let externalToolsConfigured = hasStoredExternalToolsSettings();
|
||||
let detectedExternalTools: DetectedExternalTool[] = [];
|
||||
let externalToolsDetectionPending = true;
|
||||
@@ -1260,7 +1271,33 @@
|
||||
if (appTheme === "system") applyThemePreference(appTheme);
|
||||
}
|
||||
|
||||
function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme, nextAppearance: AppAppearance, nextCustomTheme: CustomThemeColors, nextLanguage: AppLanguage, nextAutoRefresh: boolean, nextExternalTools: ExternalToolsSettings) {
|
||||
async function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme, nextAppearance: AppAppearance, nextCustomTheme: CustomThemeColors, nextLanguage: AppLanguage, nextAutoRefresh: boolean, nextExternalTools: ExternalToolsSettings, nextIntegrations: GitIntegrationSettings, integrationSecrets: GitIntegrationSecretUpdate[]) {
|
||||
const integrationsToSave = structuredClone(nextIntegrations);
|
||||
try {
|
||||
for (const update of integrationSecrets) {
|
||||
const key = integrationCredentialKey(update.provider, update.accountId);
|
||||
const azureOrganization = update.provider === "azure-devops" && update.accountId
|
||||
? integrationsToSave.azureDevOpsOrganizations.find((organization) => organization.id === update.accountId)
|
||||
: undefined;
|
||||
const providerConfig = integrationsToSave.providers[update.provider];
|
||||
if (update.removeToken) {
|
||||
await credDelete(key);
|
||||
if (azureOrganization) azureOrganization.tokenStored = false;
|
||||
else providerConfig.tokenStored = false;
|
||||
} else if (update.token) {
|
||||
const username = (azureOrganization?.username ?? providerConfig.username).trim() || "oauth2";
|
||||
await credSave(key, username, update.token, "token");
|
||||
if (azureOrganization) azureOrganization.tokenStored = true;
|
||||
else providerConfig.tokenStored = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage = appLanguage === "de"
|
||||
? `Integration konnte nicht gespeichert werden: ${String(error)}`
|
||||
: `Could not save integration: ${String(error)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
const autoRefreshWasEnabled = autoRefreshEnabled;
|
||||
analyticsSettings = next;
|
||||
appTheme = nextTheme;
|
||||
@@ -1269,12 +1306,14 @@
|
||||
appLanguage = nextLanguage;
|
||||
autoRefreshEnabled = nextAutoRefresh;
|
||||
externalToolsSettings = nextExternalTools;
|
||||
gitIntegrationSettings = integrationsToSave;
|
||||
persistAnalyticsSettings(next);
|
||||
persistThemePreference(nextTheme);
|
||||
persistAppearancePreference(nextAppearance, nextCustomTheme);
|
||||
persistLanguagePreference(nextLanguage);
|
||||
persistStoredBoolean(AUTO_REFRESH_ENABLED_KEY, nextAutoRefresh);
|
||||
persistExternalToolsSettings(nextExternalTools);
|
||||
persistGitIntegrationSettings(integrationsToSave);
|
||||
externalToolsConfigured = true;
|
||||
setTelemetryEnabled(next.enabled);
|
||||
appSettingsOpen = false;
|
||||
@@ -2578,6 +2617,11 @@
|
||||
trackEvent("clone_dialog_opened");
|
||||
}
|
||||
|
||||
function cloneFromDialog(remoteUrl: string, parentPath: string, directoryName: string, provider?: GitIntegrationProvider, accountId?: string) {
|
||||
const credentialKey = provider ? integrationCredentialKey(provider, accountId) : undefined;
|
||||
void cloneRepo(remoteUrl, parentPath, directoryName, undefined, undefined, credentialKey, false, provider ? "token" : "credentials");
|
||||
}
|
||||
|
||||
function openRepoManagement() {
|
||||
if (isBusy) return;
|
||||
activeView = "management";
|
||||
@@ -4587,6 +4631,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
function loadGitIntegrationSettings(): GitIntegrationSettings {
|
||||
try {
|
||||
return normaliseGitIntegrationSettings(JSON.parse(localStorage.getItem(GIT_INTEGRATIONS_SETTINGS_KEY) ?? "null"));
|
||||
} catch {
|
||||
return defaultGitIntegrationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
function persistGitIntegrationSettings(next: GitIntegrationSettings) {
|
||||
try {
|
||||
localStorage.setItem(GIT_INTEGRATIONS_SETTINGS_KEY, JSON.stringify(next));
|
||||
} catch {
|
||||
// Metadata persistence is best-effort; tokens remain in the OS keychain.
|
||||
}
|
||||
}
|
||||
|
||||
function hasStoredExternalToolsSettings(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(EXTERNAL_TOOLS_SETTINGS_KEY) != null;
|
||||
@@ -5710,6 +5770,7 @@
|
||||
language={appLanguage}
|
||||
autoRefresh={autoRefreshEnabled}
|
||||
externalTools={externalToolsSettings}
|
||||
integrations={gitIntegrationSettings}
|
||||
detectedTools={detectedExternalTools}
|
||||
detectionPending={externalToolsDetectionPending}
|
||||
detectionUnavailable={externalToolsDetectionUnavailable}
|
||||
@@ -6043,7 +6104,9 @@
|
||||
<CloneRepositoryDialog
|
||||
isBusy={operation === "Cloning repository"}
|
||||
error={cloneDialogError}
|
||||
onClone={cloneRepo}
|
||||
language={appLanguage}
|
||||
integrations={gitIntegrationSettings}
|
||||
onClone={cloneFromDialog}
|
||||
onClose={() => { if (!isBusy) cloneDialogOpen = false; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user