feat(integrations): add merge-method selection and provider-specific payloads
Introduce dedicated merge handling for integration review merges: - Add src-tauri/src/integrations/merge.rs: implements merge_options (read provider repo settings), merge_payload (build provider-specific merge body) and a Tauri command get_integration_review_merge_options. Includes unit tests for behavior. - Wire merge module into integrations.rs and pass an optional merge_method into provider-specific review action functions (GitHub, GitLab, Gitea, Azure DevOps). run_integration_review_action now accepts an optional merge_method, validates it early, and includes provider-specific merge payloads when performing a merge. - Export the new command in src-tauri/src/main.rs so the frontend can request merge options. Frontend changes to support selecting a merge method before merging: - ConfirmDialog.svelte: add SelectMenu support and a select field to confirm requests. - ReviewCenter.svelte: fetch integration merge options, show a merge-method selector in the merge confirmation, and pass the chosen method to the review action. - Update types and git bindings to surface IntegrationMergeOptions / IntegrationMergeMethod and the getIntegrationReviewMergeOptions call (git.ts / types.ts changes staged). Effect: users can pick a merge method appropriate to the provider/project; the integration layer generates the correct API payload per provider. Tests added for merge logic.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* untranslated, event-blocking browser dialog.
|
||||
*/
|
||||
import { AlertTriangle, Check, LoaderCircle, Trash2, X } from "@lucide/svelte";
|
||||
import SelectMenu from "./SelectMenu.svelte";
|
||||
import { t } from "../i18n.svelte";
|
||||
|
||||
export interface ConfirmRequest {
|
||||
@@ -25,6 +26,7 @@
|
||||
input?: { label: string; placeholder?: string; value?: string; optional?: boolean };
|
||||
/** Destructive actions get the red confirm button and warning icon. */
|
||||
danger?: boolean;
|
||||
select?: { label: string; value: string; options: { value: string; label: string }[] };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -47,13 +49,13 @@
|
||||
let value = $state("");
|
||||
let inputElement = $state<HTMLInputElement | null>(null);
|
||||
let missingInput = $derived(Boolean(request.input) && request.input?.optional !== true && value.trim().length === 0);
|
||||
let blocked = $derived((Boolean(request.checkbox?.required) && !checked) || missingInput);
|
||||
let blocked = $derived((Boolean(request.checkbox?.required) && !checked) || missingInput || (Boolean(request.select) && !request.select?.options.some(option => option.value === value)));
|
||||
|
||||
$effect(() => {
|
||||
// Start from the defaults again whenever a different confirmation is shown.
|
||||
request.title;
|
||||
checked = request.checkbox?.defaultChecked ?? false;
|
||||
value = request.input?.value ?? "";
|
||||
value = request.select?.value ?? request.input?.value ?? "";
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -144,6 +146,13 @@
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
{#if request.select}
|
||||
<div class="confirm-input">
|
||||
<span>{request.select.label}</span>
|
||||
<SelectMenu {value} options={request.select.options} ariaLabel={request.select.label} disabled={isBusy} onChange={(selected) => value = selected} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if request.checkbox}
|
||||
<label class="confirm-check">
|
||||
<input type="checkbox" bind:checked disabled={isBusy} />
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
CircleCheck, Clock3, CircleDotDashed, ExternalLink, GitBranch, GitMerge, GitPullRequest, Inbox,
|
||||
LoaderCircle, PanelRightOpen, RefreshCw, RotateCcw, Search, Settings2, X, XCircle,
|
||||
} from "@lucide/svelte";
|
||||
import { addIntegrationReviewComment, getIntegrationReviewDetails, listIntegrationReviewRequests, openInBrowser, runIntegrationReviewAction } from "../git";
|
||||
import { addIntegrationReviewComment, getIntegrationReviewMergeOptions, getIntegrationReviewDetails, listIntegrationReviewRequests, openInBrowser, runIntegrationReviewAction } from "../git";
|
||||
import { configuredIntegrationSources, integrationCredentialKey, providerLabel } from "../integrations";
|
||||
import type { AppLanguage, GitIntegrationSettings, IntegrationReviewAction, IntegrationReviewRequest, IntegrationReviewState, StoredCredential } from "../types";
|
||||
import type { AppLanguage, GitIntegrationSettings, IntegrationMergeMethod, IntegrationMergeOptions, IntegrationReviewAction, IntegrationReviewRequest, IntegrationReviewState, StoredCredential } from "../types";
|
||||
|
||||
type LocalResolutionPhase = "idle" | "preparing" | "conflicts" | "ready-to-continue" | "ready-to-push" | "complete" | "error";
|
||||
|
||||
@@ -384,36 +384,48 @@
|
||||
return de ? "Request wieder öffnen" : "Reopen request";
|
||||
}
|
||||
|
||||
let reviewConfirmRequest = $state<ConfirmRequest | null>(null);
|
||||
let reviewConfirmResolve: ((confirmed: boolean) => void) | null = null;
|
||||
function mergeMethodLabel(method: IntegrationMergeMethod, request: IntegrationReviewRequest): string {
|
||||
if (request.provider.startsWith("gitlab")) {
|
||||
if (method === "merge") return de ? "Ohne Squash zusammenführen" : "Merge without squashing";
|
||||
if (method === "squash") return de ? "Mit Squash zusammenführen" : "Squash and merge";
|
||||
}
|
||||
switch (method) {
|
||||
case "merge": return de ? "Merge-Commit erstellen" : "Create merge commit";
|
||||
case "rebase": return de ? "Rebase, dann Fast-forward" : "Rebase, then fast-forward";
|
||||
case "rebase-merge": return de ? "Rebase, dann Merge-Commit erstellen" : "Rebase, then create merge commit";
|
||||
case "squash": return de ? "Squash-Commit erstellen" : "Create squash commit";
|
||||
case "fast-forward-only": return de ? "Nur Fast-forward" : "Fast-forward only";
|
||||
default: return de ? "Projektstandard verwenden" : "Use project default";
|
||||
}
|
||||
}
|
||||
|
||||
function askReviewConfirmation(request: IntegrationReviewRequest, action: IntegrationReviewAction): Promise<boolean> {
|
||||
let reviewConfirmRequest = $state<ConfirmRequest | null>(null);
|
||||
let reviewConfirmResolve: ((result: { confirmed: boolean; method?: IntegrationMergeMethod }) => void) | null = null;
|
||||
|
||||
function askReviewConfirmation(request: IntegrationReviewRequest, action: IntegrationReviewAction, mergeOptions?: IntegrationMergeOptions): Promise<{ confirmed: boolean; method?: IntegrationMergeMethod }> {
|
||||
const values = { number: request.number };
|
||||
reviewConfirmRequest = action === "merge"
|
||||
? { title: t("confirm.review.mergeTitle", values), message: t("confirm.review.mergeMessage"), items: [request.title], confirmLabel: t("confirm.review.mergeAction"), danger: false }
|
||||
? { title: t("confirm.review.mergeTitle", values), message: t("confirm.review.mergeMessage"), items: [request.title], confirmLabel: t("confirm.review.mergeAction"), danger: false, select: mergeOptions ? { label: de ? "Merge-Methode" : "Merge method", value: mergeOptions.defaultMethod, options: mergeOptions.methods.map(method => ({ value: method, label: mergeMethodLabel(method, request) })) } : undefined }
|
||||
: action === "close"
|
||||
? { title: t("confirm.review.closeTitle", values), message: t("confirm.review.closeMessage"), items: [request.title], confirmLabel: t("confirm.review.closeAction") }
|
||||
: { title: t("confirm.review.reopenTitle", values), message: t("confirm.review.reopenMessage"), items: [request.title], confirmLabel: t("confirm.review.reopenAction"), danger: false };
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
return new Promise<{ confirmed: boolean; method?: IntegrationMergeMethod }>((resolve) => {
|
||||
reviewConfirmResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function answerReviewConfirmation(confirmed: boolean) {
|
||||
function answerReviewConfirmation(confirmed: boolean, value?: string) {
|
||||
const method = reviewConfirmRequest?.select?.options.find(option => option.value === value)?.value as IntegrationMergeMethod | undefined;
|
||||
const resolve = reviewConfirmResolve;
|
||||
reviewConfirmRequest = null;
|
||||
reviewConfirmResolve = null;
|
||||
resolve?.(confirmed);
|
||||
resolve?.({ confirmed, method });
|
||||
}
|
||||
|
||||
async function performReviewAction(request: IntegrationReviewRequest, action: IntegrationReviewAction) {
|
||||
const source = activeSource;
|
||||
if (!source || actionBusyId) return;
|
||||
if (action !== "approve") {
|
||||
const confirmed = await askReviewConfirmation(request, action);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
actionMenuId = "";
|
||||
actionNotice = "";
|
||||
actionBusyId = request.id;
|
||||
@@ -421,7 +433,15 @@
|
||||
try {
|
||||
const credential = await withTimeout(loadCredential(integrationCredentialKey(source.provider, source.accountId)), `${source.label} keychain`, 15_000);
|
||||
if (!credential?.password) throw new Error(de ? "Kein Token für diese Integration gespeichert." : "No token is stored for this integration.");
|
||||
await withTimeout(runIntegrationReviewAction(source.provider, source.baseUrl, credential.username, credential.password, request, action), source.label);
|
||||
let mergeMethod: IntegrationMergeMethod | undefined;
|
||||
if (action !== "approve") {
|
||||
const options = action === "merge" ? await withTimeout(getIntegrationReviewMergeOptions(source.provider, source.baseUrl, credential.password, request), source.label) : undefined;
|
||||
if (options && options.methods.length === 0) throw new Error(de ? "Für dieses Repository ist keine unterstützte Merge-Methode freigegeben." : "No supported merge method is enabled for this repository.");
|
||||
const result = await askReviewConfirmation(request, action, options);
|
||||
if (!result.confirmed) return;
|
||||
mergeMethod = result.method;
|
||||
}
|
||||
await withTimeout(runIntegrationReviewAction(source.provider, source.baseUrl, credential.username, credential.password, request, action, mergeMethod), source.label);
|
||||
actionNotice = de ? `${reviewActionLabel(action)} erfolgreich.` : `${reviewActionLabel(action)} succeeded.`;
|
||||
requests = [];
|
||||
loadedStates = new Set();
|
||||
@@ -723,7 +743,7 @@
|
||||
{#if reviewConfirmRequest}
|
||||
<ConfirmDialog
|
||||
request={reviewConfirmRequest}
|
||||
onConfirm={() => answerReviewConfirmation(true)}
|
||||
onConfirm={({ value }) => answerReviewConfirmation(true, value)}
|
||||
onCancel={() => answerReviewConfirmation(false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
+8
-2
@@ -19,6 +19,8 @@ import type {
|
||||
GitIntegrationRepository,
|
||||
IntegrationReviewRequest,
|
||||
IntegrationReviewAction,
|
||||
IntegrationMergeMethod,
|
||||
IntegrationMergeOptions,
|
||||
GitLfsStatus,
|
||||
GitRepositoryFile,
|
||||
GitRemote,
|
||||
@@ -66,8 +68,12 @@ export function listIntegrationReviewRequests(provider: GitIntegrationProvider,
|
||||
return invoke<IntegrationReviewRequest[]>("list_integration_review_requests", { provider, baseUrl, username, token, state });
|
||||
}
|
||||
|
||||
export function runIntegrationReviewAction(provider: GitIntegrationProvider, baseUrl: string, username: string, token: string, request: IntegrationReviewRequest, action: IntegrationReviewAction): Promise<void> {
|
||||
return invoke<void>("run_integration_review_action", { provider, baseUrl, username, token, repositoryId: request.repositoryId, repositoryName: request.repositoryName, number: request.number, action });
|
||||
export function getIntegrationReviewMergeOptions(provider: GitIntegrationProvider, baseUrl: string, token: string, request: IntegrationReviewRequest): Promise<IntegrationMergeOptions> {
|
||||
return invoke("get_integration_review_merge_options", { provider, baseUrl, token, repositoryId: request.repositoryId, repositoryName: request.repositoryName });
|
||||
}
|
||||
|
||||
export function runIntegrationReviewAction(provider: GitIntegrationProvider, baseUrl: string, username: string, token: string, request: IntegrationReviewRequest, action: IntegrationReviewAction, mergeMethod?: IntegrationMergeMethod): Promise<void> {
|
||||
return invoke<void>("run_integration_review_action", { provider, baseUrl, username, token, repositoryId: request.repositoryId, repositoryName: request.repositoryName, number: request.number, action, mergeMethod });
|
||||
}
|
||||
|
||||
export function getIntegrationReviewDetails(provider: GitIntegrationProvider, baseUrl: string, username: string, token: string, request: IntegrationReviewRequest): Promise<IntegrationReviewRequest> {
|
||||
|
||||
@@ -66,6 +66,9 @@ export interface GitIntegrationRepository {
|
||||
}
|
||||
|
||||
export type IntegrationReviewState = "open" | "draft" | "merged" | "closed";
|
||||
export type IntegrationMergeMethod = "default" | "merge" | "squash" | "rebase" | "rebase-merge" | "fast-forward-only";
|
||||
export interface IntegrationMergeOptions { methods: IntegrationMergeMethod[]; defaultMethod: IntegrationMergeMethod; }
|
||||
|
||||
export type IntegrationReviewAction = "merge" | "approve" | "close" | "reopen";
|
||||
|
||||
export interface IntegrationReviewComment {
|
||||
|
||||
Reference in New Issue
Block a user