From 477e30a9f20d26adb223884c83735b45cde8a5f3 Mon Sep 17 00:00:00 2001 From: busya Date: Wed, 27 May 2026 23:24:12 +0300 Subject: [PATCH] git --- backend/src/services/git/_branch.py | 51 +++- frontend/src/components/TaskRunner.svelte | 172 +++++++---- frontend/src/components/Toast.svelte | 77 ++++- .../components/backups/BackupManager.svelte | 9 + .../src/components/git/BranchSelector.svelte | 36 ++- .../src/components/git/GitInitPanel.svelte | 2 +- frontend/src/components/git/GitManager.svelte | 139 +++++++-- .../components/git/GitWorkspacePanel.svelte | 284 ++++++++++++++---- frontend/src/components/git/useGitManager.js | 107 ++++++- .../src/components/tools/DebugTool.svelte | 10 + .../src/components/tools/MapperTool.svelte | 218 ++++++++++---- frontend/src/lib/api.js | 18 +- .../dataset-review/SourceIntakePanel.svelte | 2 +- .../__tests__/source_intake_panel.ux.test.js | 1 + .../lib/components/layout/TaskDrawer.svelte | 6 +- .../lib/i18n/locales/en/dataset_review.json | 1 + frontend/src/lib/i18n/locales/en/git.json | 7 + .../lib/i18n/locales/ru/dataset_review.json | 1 + frontend/src/lib/i18n/locales/ru/git.json | 7 + frontend/src/lib/toasts.js | 15 +- frontend/src/lib/ui/index.ts | 2 + frontend/src/routes/+layout.svelte | 17 +- .../src/routes/dashboards/health/+page.svelte | 8 +- frontend/src/routes/datasets/+page.svelte | 7 +- .../dataset_review_workspace.ux.test.js | 1 + .../__tests__/dataset_review_entry.test.js | 1 + .../__tests__/dataset_review_entry.ux.test.js | 1 + frontend/src/routes/migration/+page.svelte | 7 + .../routes/migration/mappings/+page.svelte | 8 + frontend/src/routes/tools/mapper/+page.svelte | 25 +- .../src/routes/validation/[id]/+page.svelte | 9 + .../src/services/__tests__/gitService.test.js | 52 ++-- frontend/src/services/gitService.js | 73 +++-- specs/011-git-integration-dashboard/plan.md | 91 +++++- specs/011-git-integration-dashboard/spec.md | 111 ++++++- specs/011-git-integration-dashboard/tasks.md | 78 ++++- 36 files changed, 1348 insertions(+), 306 deletions(-) diff --git a/backend/src/services/git/_branch.py b/backend/src/services/git/_branch.py index e611d82a..a107abc2 100644 --- a/backend/src/services/git/_branch.py +++ b/backend/src/services/git/_branch.py @@ -8,6 +8,7 @@ import os from fastapi import HTTPException from git import Repo +from git.exc import GitCommandError from src.core.logger import belief_scope, logger @@ -173,12 +174,60 @@ class GitServiceBranchMixin: # @PURPOSE: Switch to a specific branch (concurrent-safe). # @PRE Repository exists and the specified branch name exists. # @POST The repository working directory is updated to the specified branch. + # @SIDE_EFFECT May raise HTTPException(409) if local changes conflict with checkout. + # @SIDE_EFFECT May raise HTTPException(500) if Git operation fails for other reasons. def checkout_branch(self, dashboard_id: int, name: str): with self._locked(dashboard_id): with belief_scope("GitService.checkout_branch"): repo = self.get_repo(dashboard_id) logger.reason(f"Checking out branch {name}", extra={"src": "checkout_branch"}) - repo.git.checkout(name) + try: + repo.git.checkout(name) + except GitCommandError as e: + stderr = str(e.stderr or "") + details = str(e) + lowered = stderr.lower() + if "local changes" in lowered or "would be overwritten" in lowered: + # Parse the list of files from stderr + files = [] + for line in stderr.split("\n"): + stripped = line.strip() + if stripped.startswith("\t") and ( + stripped.endswith(".yaml") or "/" in stripped + ): + files.append(stripped.strip()) + raise HTTPException( + status_code=409, + detail={ + "error_code": "GIT_CHECKOUT_LOCAL_CHANGES", + "message": ( + f"Невозможно переключиться на ветку '{name}' — " + f"локальные изменения будут перезаписаны. " + f"Зафиксируйте или отложите изменения." + ), + "message_en": ( + f"Cannot checkout branch '{name}' — " + f"local changes would be overwritten. " + f"Commit or stash your changes first." + ), + "files": files, + "next_steps": [ + "Зафиксируйте изменения (Commit) в текущей ветке перед переключением", + "Отложите изменения через Stash (вручную: git stash)", + "Отмените локальные изменения, если они не нужны", + ], + "next_steps_en": [ + "Commit your changes in the current branch before switching", + "Stash your changes manually: git stash", + "Discard local changes if not needed", + ], + }, + ) + logger.error(f"[checkout_branch][Coherence:Failed] {e}") + raise HTTPException( + status_code=500, + detail=f"Git checkout failed: {details}", + ) # endregion checkout_branch # region commit_changes [C:4] [TYPE Function] [SEMANTICS git,commit,stage,lock] diff --git a/frontend/src/components/TaskRunner.svelte b/frontend/src/components/TaskRunner.svelte index c487ec12..a8cbebb1 100755 --- a/frontend/src/components/TaskRunner.svelte +++ b/frontend/src/components/TaskRunner.svelte @@ -323,72 +323,122 @@ -
- {#if $selectedTask} -
-

{$t.tasks?.task_label}: {$selectedTask.plugin_id}

-
- {#if connectionStatus === 'connecting'} - - - - - {$t.tasks?.connecting} - {:else if connectionStatus === 'connected'} - - {$t.tasks?.live} - {:else if connectionStatus === 'completed'} - - {$t.tasks?.completed} - {:else if connectionStatus === 'awaiting_mapping'} - - {$t.tasks?.awaiting_mapping} - {:else if connectionStatus === 'awaiting_input'} - - {$t.tasks?.awaiting_input} - {:else} - - {$t.tasks?.disconnected} - {/if} -
-
- - -
-
- {$t.tasks?.details_parameters} -
-
-
{$t.common?.id}: {$selectedTask.id}
-
{$t.dashboard?.status}: {$selectedTask.status}
-
{$t.tasks?.started_label}: {new Date($selectedTask.started_at || $selectedTask.created_at || Date.now()).toLocaleString()}
-
{$t.tasks?.plugin}: {$selectedTask.plugin_id}
-
-
- {$t.tasks?.parameters}: -
{JSON.stringify($selectedTask.params, null, 2)}
-
+
+ +
+
+
+
+ + + +
-
-
- -
- +
+

{$t.tasks?.task_label}

+

Мониторинг выполнения задач

+
+
- {#if waitingForData && connectionStatus === 'connected'} -
- {$t.tasks?.waiting_logs} + {#if $selectedTask} +
+ {#if connectionStatus === 'connecting'} + + + + + {$t.tasks?.connecting} + {:else if connectionStatus === 'connected'} + + {$t.tasks?.live} + {:else if connectionStatus === 'completed'} + + {$t.tasks?.completed} + {:else if connectionStatus === 'awaiting_mapping'} + + {$t.tasks?.awaiting_mapping} + {:else if connectionStatus === 'awaiting_input'} + + {$t.tasks?.awaiting_input} + {:else} + + {$t.tasks?.disconnected} + {/if}
{/if}
- {:else} -

{$t.tasks?.select_task}

- {/if} +
+ + +
+ {#if $selectedTask} + +
+
+ {$t.tasks?.details_parameters} +
+
+
+ {$t.common?.id}: + {$selectedTask.id} +
+
+ {$t.dashboard?.status}: + {$selectedTask.status} +
+
+ {$t.tasks?.started_label}: + {new Date($selectedTask.started_at || $selectedTask.created_at || Date.now()).toLocaleString()} +
+
+ {$t.tasks?.plugin}: + {$selectedTask.plugin_id} +
+
+
+ {$t.tasks?.parameters}: +
{JSON.stringify($selectedTask.params, null, 2)}
+
+
+
+
+ + +
+ + + {#if waitingForData && connectionStatus === 'connected'} +
+ + + + + {$t.tasks?.waiting_logs} +
+ {/if} +
+ {:else} + +
+
+ + + + + + + +
+

{$t.tasks?.select_task}

+

Запустите задачу маппинга, чтобы увидеть логи и детали выполнения

+
+ {/if} +
- + - -
- {#each $toasts as toast (toast.id)} - + + +
+ {#each $toasts.filter(t => !t.persistent) as toast (toast.id)} +
{toast.message}
diff --git a/frontend/src/components/backups/BackupManager.svelte b/frontend/src/components/backups/BackupManager.svelte index 09e58424..f2861b1c 100644 --- a/frontend/src/components/backups/BackupManager.svelte +++ b/frontend/src/components/backups/BackupManager.svelte @@ -83,6 +83,15 @@ requestApi(filesUrl) ]); environments = envsData; + // Pre-fill with active environment from global context + if (!selectedEnvId) { + const { get } = await import('svelte/store'); + const envCtx = await import('$lib/stores/environmentContext.js'); + const state = get(envCtx.environmentContextStore); + if (state.selectedEnvId && environments.some((env) => env.id === state.selectedEnvId)) { + selectedEnvId = state.selectedEnvId; + } + } backups = (storageData || []).map((file: any) => ({ id: file.path, diff --git a/frontend/src/components/git/BranchSelector.svelte b/frontend/src/components/git/BranchSelector.svelte index b609fcc5..3b8add99 100644 --- a/frontend/src/components/git/BranchSelector.svelte +++ b/frontend/src/components/git/BranchSelector.svelte @@ -32,6 +32,7 @@ let loading = $state(false); let showCreate = $state(false); let newBranchName = $state(''); + let branchError = $state(null); // #endregion state // #region loadBranches [C:2] [TYPE Function] @@ -68,7 +69,9 @@ // @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. + // @UX_STATE Error -> Inline error message below the dropdown; persistent toast above modal with full details. async function handleCheckout(branchName) { + branchError = null; console.log(`[EXT:frontend:BranchSelector][Action] Checking out branch ${branchName}`); try { await gitService.checkoutBranch(dashboardId, branchName, envId); @@ -78,7 +81,15 @@ console.log(`[EXT:frontend:BranchSelector][Coherence:OK] Checked out ${branchName}`); } catch (e) { console.error(`[EXT:frontend:BranchSelector][Coherence:Failed] ${e.message}`); - toast(e.message, 'error'); + const detail = typeof e?.detail === 'object' ? e.detail : {}; + const nextSteps = detail?.next_steps || []; + const shortMsg = detail?.message || detail?.message_en || 'Ошибка переключения ветки'; + branchError = { + message: shortMsg, + next_steps: nextSteps, + }; + // Persistent toast above modal with the full technical error + toast(shortMsg, 'warning', 0); } } // #endregion handleCheckout @@ -90,6 +101,7 @@ // @SIDE_EFFECT POSTs to /git/repositories/{ref}/branches. async function handleCreate() { if (!newBranchName) return; + branchError = null; console.log(`[EXT:frontend:BranchSelector][Action] Creating branch ${newBranchName} from ${currentBranch}`); try { await gitService.createBranch(dashboardId, newBranchName, currentBranch, envId); @@ -100,6 +112,7 @@ console.log(`[EXT:frontend:BranchSelector][Coherence:OK] Branch created`); } catch (e) { console.error(`[EXT:frontend:BranchSelector][Coherence:Failed] ${e.message}`); + branchError = { message: e.message || 'Ошибка создания ветки', next_steps: [] }; toast(e.message, 'error'); } } @@ -133,6 +146,27 @@
+ {#if branchError} + + {/if} + {#if showCreate}
diff --git a/frontend/src/components/git/GitInitPanel.svelte b/frontend/src/components/git/GitInitPanel.svelte index ce5d2b20..b1b56ff3 100644 --- a/frontend/src/components/git/GitInitPanel.svelte +++ b/frontend/src/components/git/GitInitPanel.svelte @@ -7,7 +7,7 @@ {#if show} -
{ if (e.key === 'Escape') closeModal(); }} role="button" tabindex="0" aria-label={$t.common?.close || 'Close'}> -