fix
This commit is contained in:
@@ -343,6 +343,7 @@
|
||||
{#if showGitManager && gitDashboardId}
|
||||
<GitManager
|
||||
dashboardId={gitDashboardId}
|
||||
envId={environmentId}
|
||||
dashboardTitle={gitDashboardTitle}
|
||||
bind:show={showGitManager}
|
||||
/>
|
||||
|
||||
@@ -1,58 +1,44 @@
|
||||
<!-- #region BranchSelector [C:3] [TYPE Component] [SEMANTICS git, branch, checkout, create, selector] -->
|
||||
<!-- @BRIEF Component component: components/git/BranchSelector.svelte -->
|
||||
<!-- @BRIEF UI for selecting and creating Git branches for a dashboard repository. -->
|
||||
<!-- @LAYER UI -->
|
||||
<!--
|
||||
@SEMANTICS: git, branch, selection, checkout
|
||||
@PURPOSE: UI для выбора и создания веток Git.
|
||||
@LAYER Component
|
||||
@RELATION CALLS -> [gitService.getBranches]
|
||||
@RELATION CALLS -> [GitServiceBranch]
|
||||
@RELATION CALLS -> [gitService.createBranch]
|
||||
@RELATION BINDS_TO -> [onchange]
|
||||
-->
|
||||
<!-- @RELATION CALLS -> [EXT:frontend:gitService.getBranches] -->
|
||||
<!-- @RELATION CALLS -> [EXT:frontend:gitService.checkoutBranch] -->
|
||||
<!-- @RELATION CALLS -> [EXT:frontend:gitService.createBranch] -->
|
||||
<!-- @RELATION BINDS_TO -> [onchange] -->
|
||||
<!-- @PRE dashboardId is a valid dashboard slug; envId required when ref is slug. -->
|
||||
<!-- @UX_STATE Loading -> Select disabled, spinner. -->
|
||||
<!-- @UX_STATE Loaded -> Branch dropdown enabled. -->
|
||||
<!-- @UX_STATE CreateMode -> Inline input for new branch name. -->
|
||||
|
||||
<script>
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount } from 'svelte';
|
||||
import { gitService } from '../../services/gitService';
|
||||
import { addToast as toast } from '../../lib/toasts.js';
|
||||
import { t } from '../../lib/i18n';
|
||||
import { Button, Select, Input } from '../../lib/ui';
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
let {
|
||||
dashboardId,
|
||||
envId = null,
|
||||
currentBranch = $bindable('main'),
|
||||
onchange = () => {},
|
||||
} = $props();
|
||||
// #region props [C:2] [TYPE Block]
|
||||
// @PRE dashboardId is non-empty slug; envId resolved via GitManager fallback chain.
|
||||
let {
|
||||
dashboardId,
|
||||
envId = null,
|
||||
currentBranch = $bindable('main'),
|
||||
onchange = () => {},
|
||||
} = $props();
|
||||
// #endregion props
|
||||
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: STATE]
|
||||
// #region state [C:1] [TYPE Block]
|
||||
let branches = $state([]);
|
||||
let loading = $state(false);
|
||||
let showCreate = $state(false);
|
||||
let newBranchName = $state('');
|
||||
// [/SECTION]
|
||||
// #region onMount:Function [TYPE Function]
|
||||
/**
|
||||
* @purpose Load branches when component is mounted.
|
||||
* @pre Component is initialized.
|
||||
* @post loadBranches is called.
|
||||
*/
|
||||
onMount(async () => {
|
||||
await loadBranches();
|
||||
});
|
||||
// #endregion onMount:Function
|
||||
// #endregion state
|
||||
|
||||
// #region loadBranches:Function [TYPE Function]
|
||||
/**
|
||||
* @purpose Загружает список веток для дашборда.
|
||||
* @pre dashboardId is provided.
|
||||
* @post branches обновлен.
|
||||
*/
|
||||
// #region loadBranches [C:2] [TYPE Function]
|
||||
// @BRIEF Load branch list from backend.
|
||||
// @PRE dashboardId is non-empty; envId required when ref is slug.
|
||||
// @POST branches array populated with BranchSchema objects.
|
||||
// @SIDE_EFFECT GETs /git/repositories/{ref}/branches.
|
||||
async function loadBranches() {
|
||||
console.log(`[EXT:frontend:BranchSelector][Action] Loading branches for dashboard ${dashboardId}`);
|
||||
loading = true;
|
||||
@@ -66,25 +52,22 @@
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
// #endregion loadBranches:Function
|
||||
// #endregion loadBranches
|
||||
|
||||
// #region handleSelect:Function [TYPE Function]
|
||||
/**
|
||||
* @purpose Handles branch selection from dropdown.
|
||||
* @pre event contains branch name.
|
||||
* @post handleCheckout is called with selected branch.
|
||||
*/
|
||||
// #region handleSelect [C:1] [TYPE Function]
|
||||
// @BRIEF Handle branch selection from dropdown.
|
||||
// @PRE event contains target.value with branch name.
|
||||
// @POST handleCheckout called with selected branch name.
|
||||
function handleSelect(event) {
|
||||
handleCheckout(event.target.value);
|
||||
}
|
||||
// #endregion handleSelect:Function
|
||||
// #endregion handleSelect
|
||||
|
||||
// #region handleCheckout:Function [TYPE Function]
|
||||
/**
|
||||
* @purpose Переключает текущую ветку.
|
||||
* @param {string} branchName - Имя ветки.
|
||||
* @post currentBranch обновлен, родительский callback вызван.
|
||||
*/
|
||||
// #region handleCheckout [C:2] [TYPE Function]
|
||||
// @BRIEF Switch repository to selected branch.
|
||||
// @PRE branchName is non-empty; envId required when ref is slug.
|
||||
// @POST currentBranch updated; onchange callback fired.
|
||||
// @SIDE_EFFECT POSTs to /git/repositories/{ref}/checkout.
|
||||
async function handleCheckout(branchName) {
|
||||
console.log(`[EXT:frontend:BranchSelector][Action] Checking out branch ${branchName}`);
|
||||
try {
|
||||
@@ -98,14 +81,13 @@
|
||||
toast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
// #endregion handleCheckout:Function
|
||||
// #endregion handleCheckout
|
||||
|
||||
// #region handleCreate:Function [TYPE Function]
|
||||
/**
|
||||
* @purpose Создает новую ветку.
|
||||
* @pre newBranchName is not empty.
|
||||
* @post Новая ветка создана и загружена; showCreate reset.
|
||||
*/
|
||||
// #region handleCreate [C:2] [TYPE Function]
|
||||
// @BRIEF Create a new branch from current branch.
|
||||
// @PRE newBranchName is non-empty; envId required when ref is slug.
|
||||
// @POST New branch created; branch list reloaded; create mode closed.
|
||||
// @SIDE_EFFECT POSTs to /git/repositories/{ref}/branches.
|
||||
async function handleCreate() {
|
||||
if (!newBranchName) return;
|
||||
console.log(`[EXT:frontend:BranchSelector][Action] Creating branch ${newBranchName} from ${currentBranch}`);
|
||||
@@ -121,7 +103,11 @@
|
||||
toast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
// #endregion handleCreate:Function
|
||||
// #endregion handleCreate
|
||||
|
||||
onMount(async () => {
|
||||
await loadBranches();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
|
||||
@@ -92,10 +92,13 @@
|
||||
const configHost = $derived(extractHttpHost(repositoryConfigUrl));
|
||||
const hasOriginConfigMismatch = $derived(originHost && configHost && originHost !== configHost);
|
||||
|
||||
// ── Resolve envId with localStorage fallback ──────────────
|
||||
let resolvedEnvId = $derived(resolveCurrentEnvironmentId(envId));
|
||||
|
||||
// ── State proxy for composable ────────────────────────────
|
||||
function getState() {
|
||||
return {
|
||||
dashboardId, envId, dashboardTitle, toast, t,
|
||||
dashboardId, envId: resolvedEnvId, dashboardTitle, toast, t,
|
||||
currentBranch, showDeployModal, loading, initialized, checkingStatus,
|
||||
configs, selectedConfigId, remoteUrl, creatingRemoteRepo,
|
||||
activeTab, showAdvancedPromote, promoting, promoteFromBranch, promoteToBranch, promoteMode, promoteReason,
|
||||
@@ -170,7 +173,7 @@
|
||||
await Promise.all([h.checkStatus(), h.loadCurrentEnvironmentStage()]);
|
||||
if (initialized) {
|
||||
try {
|
||||
const binding = await gitService.getRepositoryBinding(dashboardId, envId);
|
||||
const binding = await gitService.getRepositoryBinding(dashboardId, resolvedEnvId);
|
||||
repositoryProvider = binding?.provider || '';
|
||||
repositoryBindingRemoteUrl = binding?.remote_url || '';
|
||||
if (binding?.config_id) selectedConfigId = String(binding.config_id);
|
||||
@@ -203,7 +206,7 @@
|
||||
<span class={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold ${stageBadgeClass(currentEnvStage)}`}>{currentEnvStage || 'DEV'}</span>
|
||||
<span class="text-xs text-slate-600">Текущая ветка: <strong>{currentBranch}</strong></span>
|
||||
</div>
|
||||
<div class="w-full max-w-sm"><BranchSelector {dashboardId} {envId} bind:currentBranch /></div>
|
||||
<div class="w-full max-w-sm"><BranchSelector {dashboardId} envId={resolvedEnvId} bind:currentBranch /></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 border-b border-slate-200 pb-2">
|
||||
<button class={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${activeTab === 'workspace' ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`} onclick={() => (activeTab = 'workspace')}>📝 Фиксация изменений</button>
|
||||
@@ -211,7 +214,7 @@
|
||||
<button class={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${activeTab === 'operations' ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`} onclick={() => (activeTab = 'operations')}>⚙️ Серверные операции</button>
|
||||
</div>
|
||||
{#if activeTab === 'workspace'}
|
||||
<GitWorkspacePanel {hasWorkspaceChanges} {changedFilesCount} {workspaceLoading} {committing} {generatingMessage} bind:commitMessage bind:autoPushAfterCommit {loading} {pushProviderLabel} onSync={h.handleSync} onGenerateMessage={h.handleGenerateMessage} onCommit={h.handleCommit} />
|
||||
<GitWorkspacePanel {hasWorkspaceChanges} {changedFilesCount} {workspaceLoading} {workspaceDiff} {committing} {generatingMessage} bind:commitMessage bind:autoPushAfterCommit {loading} {pushProviderLabel} onSync={h.handleSync} onGenerateMessage={h.handleGenerateMessage} onCommit={h.handleCommit} />
|
||||
{:else if activeTab === 'release'}
|
||||
<GitReleasePanel {currentEnvStage} bind:promoteFromBranch bind:promoteToBranch bind:promoteMode bind:promoteReason {preferredDeployTargetStage} bind:showAdvancedPromote {promoting} onPromote={h.handlePromote} />
|
||||
{:else}
|
||||
@@ -224,5 +227,5 @@
|
||||
</div>
|
||||
{/if}
|
||||
<ConflictResolver conflicts={mergeConflicts} bind:show={showConflictResolver} onresolve={h.handleResolveConflicts} />
|
||||
<DeploymentModal {dashboardId} {envId} preferredTargetStage={preferredDeployTargetStage} bind:show={showDeployModal} />
|
||||
<DeploymentModal {dashboardId} envId={resolvedEnvId} preferredTargetStage={preferredDeployTargetStage} bind:show={showDeployModal} />
|
||||
<!-- #endregion GitManager -->
|
||||
|
||||
@@ -3,17 +3,22 @@
|
||||
<!-- @LAYER UI -->
|
||||
<!-- @RELATION DEPENDS_ON -> [GitUtils] -->
|
||||
<!-- @RELATION CALLS -> [EXT:frontend:gitService] -->
|
||||
<!-- @UX_STATE Idle -> Commit form with diff viewer. -->
|
||||
<!-- @PRE hasWorkspaceChanges and workspaceDiff are propagated from GitManager. -->
|
||||
<!-- @UX_STATE Idle -> "Нет изменений" placeholder when !hasWorkspaceChanges. -->
|
||||
<!-- @UX_STATE Changes -> workspaceDiff rendered when hasWorkspaceChanges && workspaceDiff. -->
|
||||
<!-- @UX_STATE Loading -> GeneratingMessage spinner, workspace loading. -->
|
||||
<!-- @UX_STATE Error -> Toast on failure. -->
|
||||
<script>
|
||||
import { t } from "$lib/i18n";
|
||||
import { Button } from "$lib/ui";
|
||||
import * as Diff2Html from 'diff2html';
|
||||
import 'diff2html/bundles/css/diff2html.min.css';
|
||||
|
||||
let {
|
||||
hasWorkspaceChanges,
|
||||
changedFilesCount,
|
||||
workspaceLoading,
|
||||
workspaceDiff = '',
|
||||
committing,
|
||||
generatingMessage,
|
||||
commitMessage = $bindable(),
|
||||
@@ -24,6 +29,16 @@
|
||||
onGenerateMessage,
|
||||
onCommit,
|
||||
} = $props();
|
||||
|
||||
// ── Render diff as HTML via diff2html ──
|
||||
let diffHtml = $derived(workspaceDiff
|
||||
? Diff2Html.html(workspaceDiff, {
|
||||
outputFormat: 'side-by-side',
|
||||
drawFileList: true,
|
||||
matching: 'lines',
|
||||
highlight: true,
|
||||
})
|
||||
: '');
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
@@ -85,6 +100,10 @@
|
||||
<div class="h-4 animate-pulse rounded bg-slate-200"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if hasWorkspaceChanges && diffHtml}
|
||||
<div class="diff-view">{@html diffHtml}</div>
|
||||
{:else if hasWorkspaceChanges}
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap font-mono text-xs leading-relaxed text-slate-500">Загрузка diff... (повторите синхронизацию)</pre>
|
||||
{:else}
|
||||
<div class="flex h-full items-center justify-center text-sm text-slate-500">
|
||||
Нет изменений для коммита
|
||||
|
||||
@@ -20,8 +20,8 @@ describe('GitManager unfinished merge dialog contract', () => {
|
||||
|
||||
expect(utils).toContain("error_code !== 'GIT_UNFINISHED_MERGE'");
|
||||
expect(handlers).toContain('showUnfinishedMergeDialog: true');
|
||||
expect(handlers).toContain('this.openUnfinishedMergeDialogFromError(e)');
|
||||
expect(handlers).toContain('await this.loadMergeRecoveryState();');
|
||||
expect(handlers).toContain('handlers.openUnfinishedMergeDialogFromError(e)');
|
||||
expect(handlers).toContain('await handlers.loadMergeRecoveryState();');
|
||||
expect(manager).toContain('h.handlePull');
|
||||
expect(manager).toContain('h.loadMergeRecoveryState');
|
||||
});
|
||||
|
||||
@@ -11,7 +11,14 @@ export function createGitHandlers(getState, setState) {
|
||||
const s = () => getState();
|
||||
const u = (patch) => setState(patch);
|
||||
|
||||
return {
|
||||
// ── Use const reference instead of `this` to avoid context loss in callbacks ──
|
||||
const handlers = {
|
||||
|
||||
// #region loadCurrentEnvironmentStage [C:2] [TYPE Function]
|
||||
// @BRIEF Resolve current environment stage (DEV/PREPROD/PROD) and apply GitFlow defaults.
|
||||
// @PRE resolveEnvId() returns a valid environment ID, or handler exits silently.
|
||||
// @POST currentEnvStage and GitFlow defaults (promoteFromBranch, promoteToBranch) are set.
|
||||
// @SIDE_EFFECT Fetches /environments from API.
|
||||
async loadCurrentEnvironmentStage() {
|
||||
const currentEnvId = s().resolveEnvId();
|
||||
if (!currentEnvId) return;
|
||||
@@ -27,7 +34,13 @@ export function createGitHandlers(getState, setState) {
|
||||
console.error(`[GitManager][Coherence:Failed] Failed to resolve environment stage: ${e.message}`);
|
||||
}
|
||||
},
|
||||
// #endregion loadCurrentEnvironmentStage
|
||||
|
||||
// #region checkStatus [C:3] [TYPE Function]
|
||||
// @BRIEF Check if Git repository is initialized for the dashboard.
|
||||
// @PRE dashboardId is a non-numeric slug; envId is required (resolved with localStorage fallback).
|
||||
// @POST initialized=true if repository found and workspace loaded; initialized=false if not found or envId missing.
|
||||
// @SIDE_EFFECT Fetches /git/repositories/{ref}/branches; may fetch /git/repositories/{ref}/status and /diff on success.
|
||||
async checkStatus() {
|
||||
if (isNumericDashboardRef(s().dashboardId)) {
|
||||
u({ checkingStatus: false, initialized: false });
|
||||
@@ -35,18 +48,25 @@ export function createGitHandlers(getState, setState) {
|
||||
return;
|
||||
}
|
||||
u({ checkingStatus: true });
|
||||
if (!s().envId) {
|
||||
u({ initialized: false, checkingStatus: false });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await gitService.getBranches(s().dashboardId, s().envId);
|
||||
u({ initialized: true });
|
||||
await this.loadWorkspace();
|
||||
await handlers.loadWorkspace();
|
||||
} catch (_e) {
|
||||
u({ initialized: false });
|
||||
try { const configs = await gitService.getConfigs(); u({ configs }); } catch (_e2) { u({ configs: [] }); }
|
||||
const defaultConfig = resolveDefaultConfig(s().configs, s().selectedConfigId);
|
||||
if (defaultConfig?.id) u({ selectedConfigId: defaultConfig.id });
|
||||
} finally { u({ checkingStatus: false }); }
|
||||
},
|
||||
// #endregion checkStatus
|
||||
|
||||
// #region loadWorkspace [C:2] [TYPE Function]
|
||||
// @BRIEF Load Git workspace status and diff for initialized repository.
|
||||
// @PRE initialized is true; envId is required when ref is slug.
|
||||
// @POST workspaceStatus and workspaceDiff are updated; currentBranch synced from server.
|
||||
// @SIDE_EFFECT Fetches /status and /diff for the dashboard repository.
|
||||
async loadWorkspace() {
|
||||
if (!s().initialized) return;
|
||||
u({ workspaceLoading: true });
|
||||
@@ -59,18 +79,29 @@ export function createGitHandlers(getState, setState) {
|
||||
s().toast(e.message || 'Не удалось загрузить изменения', 'error');
|
||||
} finally { u({ workspaceLoading: false }); }
|
||||
},
|
||||
// #endregion loadWorkspace
|
||||
|
||||
// #region handleSync [C:2] [TYPE Function]
|
||||
// @BRIEF Synchronize local dashboard state with Git repository.
|
||||
// @PRE dashboardId is a non-numeric slug; envId required when ref is slug.
|
||||
// @POST Dashboard state synced to Git workspace; workspace reloaded.
|
||||
// @SIDE_EFFECT POSTs to /git/repositories/{ref}/sync; reloads workspace on success.
|
||||
async handleSync() {
|
||||
if (isNumericDashboardRef(s().dashboardId)) { s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error'); return; }
|
||||
u({ loading: true });
|
||||
try {
|
||||
const sourceEnvId = localStorage.getItem('selected_env_id');
|
||||
const sourceEnvId = s().envId || localStorage.getItem('selected_env_id');
|
||||
await gitService.sync(s().dashboardId, sourceEnvId, s().envId);
|
||||
s().toast(s().t?.git?.sync_success || 'Состояние дашборда синхронизировано с Git', 'success');
|
||||
await this.loadWorkspace();
|
||||
await handlers.loadWorkspace();
|
||||
} catch (e) { s().toast(e.message, 'error'); } finally { u({ loading: false }); }
|
||||
},
|
||||
// #endregion handleSync
|
||||
|
||||
// #region handleGenerateMessage [C:2] [TYPE Function]
|
||||
// @BRIEF Generate AI commit message from dashboard diff.
|
||||
// @SIDE_EFFECT POSTs to /git/repositories/{ref}/generate-message.
|
||||
// @POST commitMessage state updated with generated text.
|
||||
async handleGenerateMessage() {
|
||||
u({ generatingMessage: true });
|
||||
try {
|
||||
@@ -79,7 +110,13 @@ export function createGitHandlers(getState, setState) {
|
||||
s().toast(s().t?.git?.commit_message_generated || 'Сообщение для коммита сгенерировано', 'success');
|
||||
} catch (e) { s().toast(e.message || s().t?.git?.commit_message_failed || 'Не удалось сгенерировать сообщение', 'error'); } finally { u({ generatingMessage: false }); }
|
||||
},
|
||||
// #endregion handleGenerateMessage
|
||||
|
||||
// #region handleCommit [C:2] [TYPE Function]
|
||||
// @BRIEF Stage and commit workspace changes, optionally push.
|
||||
// @PRE commitMessage is non-empty; hasWorkspaceChanges is true; envId required when ref is slug.
|
||||
// @POST Changes committed (and pushed if autoPushAfterCommit); workspace reloaded.
|
||||
// @SIDE_EFFECT POSTs to /commit and optionally /push.
|
||||
async handleCommit() {
|
||||
if (!s().commitMessage || !s().hasWorkspaceChanges) return;
|
||||
u({ committing: true });
|
||||
@@ -90,22 +127,33 @@ export function createGitHandlers(getState, setState) {
|
||||
s().toast(s().t?.git?.commit_and_push_success || 'Коммит создан и отправлен в remote', 'success');
|
||||
} else { s().toast(s().t?.git?.commit_success || 'Коммит успешно создан', 'success'); }
|
||||
u({ commitMessage: '' });
|
||||
await this.loadWorkspace();
|
||||
await handlers.loadWorkspace();
|
||||
} catch (e) { s().toast(e.message, 'error'); } finally { u({ committing: false }); }
|
||||
},
|
||||
// #endregion handleCommit
|
||||
|
||||
// #region openUnfinishedMergeDialogFromError [C:2] [TYPE Function]
|
||||
// @BRIEF Parse error for unfinished merge context and open dialog.
|
||||
// @PRE error contains GIT_UNFINISHED_MERGE error_code or related detail.
|
||||
// @POST Returns true and opens dialog if merge context found; false otherwise.
|
||||
openUnfinishedMergeDialogFromError(error) {
|
||||
const context = extractUnfinishedMergeContext(error);
|
||||
if (!context) return false;
|
||||
u({ unfinishedMergeContext: context, showUnfinishedMergeDialog: true });
|
||||
return true;
|
||||
},
|
||||
// #endregion openUnfinishedMergeDialogFromError
|
||||
|
||||
// #region loadMergeRecoveryState [C:2] [TYPE Function]
|
||||
// @BRIEF Load merge recovery state from backend for unfinished merge.
|
||||
// @PRE Repository exists; envId required when ref is slug.
|
||||
// @POST unfinishedMergeContext is populated; dialog closed if no merge in progress.
|
||||
// @SIDE_EFFECT GETs /merge/status from backend.
|
||||
async loadMergeRecoveryState() {
|
||||
u({ mergeRecoveryLoading: true });
|
||||
try {
|
||||
const status = await gitService.getMergeStatus(s().dashboardId, s().envId);
|
||||
if (!status?.has_unfinished_merge) { this.closeUnfinishedMergeDialog(); return; }
|
||||
if (!status?.has_unfinished_merge) { handlers.closeUnfinishedMergeDialog(); return; }
|
||||
u({
|
||||
unfinishedMergeContext: {
|
||||
...(s().unfinishedMergeContext || {}),
|
||||
@@ -122,11 +170,21 @@ export function createGitHandlers(getState, setState) {
|
||||
});
|
||||
} catch (e) { s().toast(e.message || 'Failed to load merge status', 'error'); } finally { u({ mergeRecoveryLoading: false }); }
|
||||
},
|
||||
// #endregion loadMergeRecoveryState
|
||||
|
||||
// #region closeUnfinishedMergeDialog [C:1] [TYPE Function]
|
||||
// @BRIEF Close unfinished merge dialog and reset merge state.
|
||||
// @POST Dialog hidden; mergeConflicts, showConflictResolver reset.
|
||||
closeUnfinishedMergeDialog() {
|
||||
u({ showUnfinishedMergeDialog: false, unfinishedMergeContext: null, mergeConflicts: [], showConflictResolver: false });
|
||||
},
|
||||
// #endregion closeUnfinishedMergeDialog
|
||||
|
||||
// #region handleOpenConflictResolver [C:2] [TYPE Function]
|
||||
// @BRIEF Load merge conflicts and open conflict resolver UI.
|
||||
// @PRE Unfinished merge is in progress; envId required when ref is slug.
|
||||
// @POST mergeConflicts populated; showConflictResolver set to true if conflicts found.
|
||||
// @SIDE_EFFECT GETs /merge/conflicts from backend.
|
||||
async handleOpenConflictResolver() {
|
||||
u({ mergeRecoveryLoading: true });
|
||||
try {
|
||||
@@ -135,7 +193,12 @@ export function createGitHandlers(getState, setState) {
|
||||
u({ mergeConflicts: mc, showConflictResolver: true });
|
||||
} catch (e) { s().toast(e.message || 'Failed to load merge conflicts', 'error'); } finally { u({ mergeRecoveryLoading: false }); }
|
||||
},
|
||||
// #endregion handleOpenConflictResolver
|
||||
|
||||
// #region handleResolveConflicts [C:2] [TYPE Function]
|
||||
// @BRIEF Apply conflict resolutions and stage resolved files.
|
||||
// @PRE event.detail maps file paths to resolution strategies; envId required when ref is slug.
|
||||
// @POST Conflicts resolved and staged; resolver closed; workspace reloaded.
|
||||
async handleResolveConflicts(event) {
|
||||
const detail = event?.detail || {};
|
||||
const resolutions = Object.entries(detail).map(([fp, r]) => ({ file_path: fp, resolution: r }));
|
||||
@@ -145,41 +208,60 @@ export function createGitHandlers(getState, setState) {
|
||||
await gitService.resolveMergeConflicts(s().dashboardId, resolutions, s().envId);
|
||||
s().toast(s().t?.git?.unfinished_merge?.resolve_success || 'Conflicts were resolved and staged', 'success');
|
||||
u({ showConflictResolver: false });
|
||||
await this.loadMergeRecoveryState();
|
||||
await this.loadWorkspace();
|
||||
await handlers.loadMergeRecoveryState();
|
||||
await handlers.loadWorkspace();
|
||||
} catch (e) { s().toast(e.message || 'Failed to resolve conflicts', 'error'); } finally { u({ mergeResolveInProgress: false }); }
|
||||
},
|
||||
// #endregion handleResolveConflicts
|
||||
|
||||
// #region handleAbortUnfinishedMerge [C:2] [TYPE Function]
|
||||
// @BRIEF Abort current unfinished merge.
|
||||
// @PRE envId required when ref is slug.
|
||||
// @POST Merge aborted; dialog closed; workspace reloaded.
|
||||
async handleAbortUnfinishedMerge() {
|
||||
u({ mergeAbortInProgress: true });
|
||||
try {
|
||||
await gitService.abortMerge(s().dashboardId, s().envId);
|
||||
s().toast(s().t?.git?.unfinished_merge?.abort_success || 'Merge was aborted', 'success');
|
||||
this.closeUnfinishedMergeDialog();
|
||||
await this.loadWorkspace();
|
||||
handlers.closeUnfinishedMergeDialog();
|
||||
await handlers.loadWorkspace();
|
||||
} catch (e) { s().toast(e.message || 'Failed to abort merge', 'error'); } finally { u({ mergeAbortInProgress: false }); }
|
||||
},
|
||||
// #endregion handleAbortUnfinishedMerge
|
||||
|
||||
// #region handleContinueUnfinishedMerge [C:2] [TYPE Function]
|
||||
// @BRIEF Finalize unfinished merge by creating merge commit.
|
||||
// @PRE All conflicts resolved; envId required when ref is slug.
|
||||
// @POST Merge commit created; dialog closed; workspace reloaded.
|
||||
async handleContinueUnfinishedMerge() {
|
||||
u({ mergeContinueInProgress: true });
|
||||
try {
|
||||
await gitService.continueMerge(s().dashboardId, '', s().envId);
|
||||
s().toast(s().t?.git?.unfinished_merge?.continue_success || 'Merge commit created successfully', 'success');
|
||||
this.closeUnfinishedMergeDialog();
|
||||
await this.loadWorkspace();
|
||||
handlers.closeUnfinishedMergeDialog();
|
||||
await handlers.loadWorkspace();
|
||||
} catch (e) {
|
||||
s().toast(e.message || 'Failed to continue merge', 'error');
|
||||
await this.loadMergeRecoveryState();
|
||||
await handlers.loadMergeRecoveryState();
|
||||
} finally { u({ mergeContinueInProgress: false }); }
|
||||
},
|
||||
// #endregion handleContinueUnfinishedMerge
|
||||
|
||||
// #region getUnfinishedMergeCommandsText [C:1] [TYPE Function]
|
||||
// @BRIEF Get shell commands for manual merge recovery.
|
||||
// @POST Returns concatenated command string or empty string.
|
||||
getUnfinishedMergeCommandsText() {
|
||||
if (!s().unfinishedMergeContext?.commands?.length) return '';
|
||||
return s().unfinishedMergeContext.commands.join('\n');
|
||||
},
|
||||
// #endregion getUnfinishedMergeCommandsText
|
||||
|
||||
// #region handleCopyUnfinishedMergeCommands [C:1] [TYPE Function]
|
||||
// @BRIEF Copy merge recovery commands to clipboard.
|
||||
// @PRE commands exist in unfinishedMergeContext.
|
||||
// @POST Commands copied to clipboard; toast shown.
|
||||
async handleCopyUnfinishedMergeCommands() {
|
||||
const text = this.getUnfinishedMergeCommandsText();
|
||||
const text = handlers.getUnfinishedMergeCommandsText();
|
||||
if (!text) { s().toast(s().t?.git?.unfinished_merge?.copy_empty || 'Команды для копирования отсутствуют', 'warning'); return; }
|
||||
u({ copyingUnfinishedMergeCommands: true });
|
||||
try {
|
||||
@@ -187,29 +269,46 @@ export function createGitHandlers(getState, setState) {
|
||||
s().toast(s().t?.git?.unfinished_merge?.copy_success || 'Команды скопированы в буфер обмена', 'success');
|
||||
} catch (_e) { s().toast(s().t?.git?.unfinished_merge?.copy_failed || 'Не удалось скопировать команды', 'error'); } finally { u({ copyingUnfinishedMergeCommands: false }); }
|
||||
},
|
||||
// #endregion handleCopyUnfinishedMergeCommands
|
||||
|
||||
// #region handlePull [C:2] [TYPE Function]
|
||||
// @BRIEF Pull changes from remote repository.
|
||||
// @PRE Remote 'origin' configured; envId required when ref is slug.
|
||||
// @POST Local repository updated with remote changes; workspace reloaded.
|
||||
// @SIDE_EFFECT POSTs to /pull; may open merge dialog on GIT_UNFINISHED_MERGE.
|
||||
async handlePull() {
|
||||
u({ isPulling: true });
|
||||
try {
|
||||
await gitService.pull(s().dashboardId, s().envId);
|
||||
s().toast(s().t?.git?.pull_success || 'Изменения получены из Git', 'success');
|
||||
await this.loadWorkspace();
|
||||
await handlers.loadWorkspace();
|
||||
} catch (e) {
|
||||
const handled = this.openUnfinishedMergeDialogFromError(e);
|
||||
if (handled) await this.loadMergeRecoveryState();
|
||||
const handled = handlers.openUnfinishedMergeDialogFromError(e);
|
||||
if (handled) await handlers.loadMergeRecoveryState();
|
||||
else s().toast(e.message, 'error');
|
||||
} finally { u({ isPulling: false }); }
|
||||
},
|
||||
// #endregion handlePull
|
||||
|
||||
// #region handlePush [C:2] [TYPE Function]
|
||||
// @BRIEF Push local commits to remote repository.
|
||||
// @PRE Remote 'origin' configured; envId required when ref is slug.
|
||||
// @POST Remote updated with local commits; workspace reloaded.
|
||||
async handlePush() {
|
||||
u({ isPushing: true });
|
||||
try {
|
||||
await gitService.push(s().dashboardId, s().envId);
|
||||
s().toast(s().t?.git?.push_success || 'Изменения отправлены в Git', 'success');
|
||||
await this.loadWorkspace();
|
||||
await handlers.loadWorkspace();
|
||||
} catch (e) { s().toast(e.message, 'error'); } finally { u({ isPushing: false }); }
|
||||
},
|
||||
// #endregion handlePush
|
||||
|
||||
// #region handlePromote [C:3] [TYPE Function]
|
||||
// @BRIEF Promote changes between branches via MR or direct merge.
|
||||
// @PRE promoteFromBranch and promoteToBranch differ; direct mode requires reason; envId required when ref is slug.
|
||||
// @POST MR created on Git server (MR mode) or direct merge executed (direct mode).
|
||||
// @SIDE_EFFECT Opens MR URL in new tab (MR mode); writes policy violation to logs (direct mode).
|
||||
async handlePromote() {
|
||||
const st = s();
|
||||
if (!st.promoteFromBranch || !st.promoteToBranch || st.promoteFromBranch === st.promoteToBranch) { st.toast('Выберите разные исходную и целевую ветки', 'error'); return; }
|
||||
@@ -226,7 +325,12 @@ export function createGitHandlers(getState, setState) {
|
||||
else { if (response?.url) window.open(response.url, '_blank', 'noopener,noreferrer'); st.toast('Merge Request создан на Git сервере', 'success'); }
|
||||
} catch (e) { st.toast(e.message, 'error'); } finally { u({ promoting: false }); }
|
||||
},
|
||||
// #endregion handlePromote
|
||||
|
||||
// #region openDeployModal [C:2] [TYPE Function]
|
||||
// @BRIEF Open deployment target modal with PROD confirmation.
|
||||
// @PRE dashboardId is a valid slug.
|
||||
// @POST showDeployModal set to true if PROD confirmation passes.
|
||||
openDeployModal() {
|
||||
const st = s();
|
||||
if (st.currentEnvStage === 'PROD') {
|
||||
@@ -236,7 +340,13 @@ export function createGitHandlers(getState, setState) {
|
||||
}
|
||||
u({ showDeployModal: true });
|
||||
},
|
||||
// #endregion openDeployModal
|
||||
|
||||
// #region handleCreateRemoteRepo [C:2] [TYPE Function]
|
||||
// @BRIEF Create a remote repository on the selected Git provider.
|
||||
// @PRE A Git config is selected; user provides a repo name via prompt.
|
||||
// @POST remoteUrl set to created (or existing) repository clone URL.
|
||||
// @SIDE_EFFECT POSTs to /git/config/{id}/repositories; prompts user for repo name.
|
||||
async handleCreateRemoteRepo() {
|
||||
const config = resolveDefaultConfig(s().configs, s().selectedConfigId);
|
||||
if (!config) { s().toast(s().t?.git?.init_validation_error || 'Сначала выберите Git сервер', 'error'); return; }
|
||||
@@ -256,20 +366,37 @@ export function createGitHandlers(getState, setState) {
|
||||
if (!url) throw new Error('Remote repository created, but URL is empty');
|
||||
u({ remoteUrl: url });
|
||||
s().toast(`Repository created on ${config.provider}`, 'success');
|
||||
} catch (e) { s().toast(e.message, 'error'); } finally { u({ creatingRemoteRepo: false }); }
|
||||
} catch (e) {
|
||||
if (e?.status === 409 && /already exists/i.test(String(e?.message || ''))) {
|
||||
s().toast(s().t?.git?.repo_already_exists || 'Repository already exists. Enter its URL below and click Init.', 'warning');
|
||||
} else { s().toast(e.message, 'error'); }
|
||||
} finally { u({ creatingRemoteRepo: false }); }
|
||||
},
|
||||
// #endregion handleCreateRemoteRepo
|
||||
|
||||
// #region handleInit [C:3] [TYPE Function]
|
||||
// @BRIEF Initialize local Git repository for the dashboard: clone remote.
|
||||
// @PRE selectedConfigId is set; remoteUrl is set; envId required when ref is slug.
|
||||
// @POST Repository initialized; initialized=true; workspace loaded.
|
||||
// @SIDE_EFFECT POSTs to /git/repositories/{ref}/init; loads workspace on success.
|
||||
async handleInit() {
|
||||
if (!s().selectedConfigId || !s().remoteUrl) { s().toast(s().t?.git?.init_validation_error || 'Заполните все поля', 'error'); return; }
|
||||
// ── Guard: slug-based init requires envId ──
|
||||
if (!s().envId && !isNumericDashboardRef(s().dashboardId)) {
|
||||
s().toast('Environment must be selected to initialize Git for this dashboard.', 'error');
|
||||
return;
|
||||
}
|
||||
u({ loading: true });
|
||||
try {
|
||||
await gitService.initRepository(s().dashboardId, s().selectedConfigId, s().remoteUrl, s().envId);
|
||||
s().toast(s().t?.git?.init_success || 'Репозиторий инициализирован', 'success');
|
||||
const provider = resolveDefaultConfig(s().configs, s().selectedConfigId)?.provider || '';
|
||||
u({ initialized: true, repositoryProvider: provider });
|
||||
await this.loadWorkspace();
|
||||
await handlers.loadWorkspace();
|
||||
} catch (e) { s().toast(e.message, 'error'); } finally { u({ loading: false }); }
|
||||
},
|
||||
// #endregion handleInit
|
||||
};
|
||||
return handlers;
|
||||
}
|
||||
// #endregion UseGitManager
|
||||
|
||||
Reference in New Issue
Block a user