trident-git 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -0
- package/.next/app-path-routes-manifest.json +23 -0
- package/.next/build-manifest.json +20 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +73 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +282 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.js +330 -0
- package/.next/required-server-files.json +330 -0
- package/.next/routes-manifest.json +170 -0
- package/.next/server/app/_global-error/page.js +3 -0
- package/.next/server/app/_global-error/page.js.nft.json +1 -0
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_global-error.html +2 -0
- package/.next/server/app/_global-error.meta +16 -0
- package/.next/server/app/_global-error.rsc +12 -0
- package/.next/server/app/_global-error.segments/_full.segment.rsc +12 -0
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +4 -0
- package/.next/server/app/_global-error.segments/_head.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_index.segment.rsc +4 -0
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
- package/.next/server/app/_not-found/page.js +2 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +16 -0
- package/.next/server/app/_not-found.rsc +19 -0
- package/.next/server/app/_not-found.segments/_full.segment.rsc +19 -0
- package/.next/server/app/_not-found.segments/_head.segment.rsc +6 -0
- package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -0
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -0
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +4 -0
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +6 -0
- package/.next/server/app/api/credentials/route.js +1 -0
- package/.next/server/app/api/credentials/route.js.nft.json +1 -0
- package/.next/server/app/api/credentials/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/custom-scripts/route.js +3 -0
- package/.next/server/app/api/custom-scripts/route.js.nft.json +1 -0
- package/.next/server/app/api/custom-scripts/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/fs/route.js +1 -0
- package/.next/server/app/api/fs/route.js.nft.json +1 -0
- package/.next/server/app/api/fs/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/git/action/route.js +1 -0
- package/.next/server/app/api/git/action/route.js.nft.json +1 -0
- package/.next/server/app/api/git/action/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/git/branches/route.js +1 -0
- package/.next/server/app/api/git/branches/route.js.nft.json +1 -0
- package/.next/server/app/api/git/branches/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/git/diff/route.js +1 -0
- package/.next/server/app/api/git/diff/route.js.nft.json +1 -0
- package/.next/server/app/api/git/diff/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/git/log/route.js +1 -0
- package/.next/server/app/api/git/log/route.js.nft.json +1 -0
- package/.next/server/app/api/git/log/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/git/status/route.js +1 -0
- package/.next/server/app/api/git/status/route.js.nft.json +1 -0
- package/.next/server/app/api/git/status/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/repos/route.js +1 -0
- package/.next/server/app/api/repos/route.js.nft.json +1 -0
- package/.next/server/app/api/repos/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/settings/route.js +1 -0
- package/.next/server/app/api/settings/route.js.nft.json +1 -0
- package/.next/server/app/api/settings/route_client-reference-manifest.js +1 -0
- package/.next/server/app/credentials/page.js +4 -0
- package/.next/server/app/credentials/page.js.nft.json +1 -0
- package/.next/server/app/credentials/page_client-reference-manifest.js +1 -0
- package/.next/server/app/credentials.html +1 -0
- package/.next/server/app/credentials.meta +15 -0
- package/.next/server/app/credentials.rsc +23 -0
- package/.next/server/app/credentials.segments/_full.segment.rsc +23 -0
- package/.next/server/app/credentials.segments/_head.segment.rsc +6 -0
- package/.next/server/app/credentials.segments/_index.segment.rsc +8 -0
- package/.next/server/app/credentials.segments/_tree.segment.rsc +6 -0
- package/.next/server/app/credentials.segments/credentials/__PAGE__.segment.rsc +9 -0
- package/.next/server/app/credentials.segments/credentials.segment.rsc +4 -0
- package/.next/server/app/icon.png/route.js +1 -0
- package/.next/server/app/icon.png/route.js.nft.json +1 -0
- package/.next/server/app/icon.png.meta +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +14 -0
- package/.next/server/app/index.rsc +20 -0
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -0
- package/.next/server/app/index.segments/_full.segment.rsc +20 -0
- package/.next/server/app/index.segments/_head.segment.rsc +6 -0
- package/.next/server/app/index.segments/_index.segment.rsc +8 -0
- package/.next/server/app/index.segments/_tree.segment.rsc +6 -0
- package/.next/server/app/page.js +2 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/workspace/changes/page.js +4 -0
- package/.next/server/app/workspace/changes/page.js.nft.json +1 -0
- package/.next/server/app/workspace/changes/page_client-reference-manifest.js +1 -0
- package/.next/server/app/workspace/changes.html +1 -0
- package/.next/server/app/workspace/changes.meta +16 -0
- package/.next/server/app/workspace/changes.rsc +26 -0
- package/.next/server/app/workspace/changes.segments/_full.segment.rsc +26 -0
- package/.next/server/app/workspace/changes.segments/_head.segment.rsc +6 -0
- package/.next/server/app/workspace/changes.segments/_index.segment.rsc +8 -0
- package/.next/server/app/workspace/changes.segments/_tree.segment.rsc +7 -0
- package/.next/server/app/workspace/changes.segments/workspace/changes/__PAGE__.segment.rsc +10 -0
- package/.next/server/app/workspace/changes.segments/workspace/changes.segment.rsc +4 -0
- package/.next/server/app/workspace/changes.segments/workspace.segment.rsc +7 -0
- package/.next/server/app/workspace/custom-scripts/page.js +2 -0
- package/.next/server/app/workspace/custom-scripts/page.js.nft.json +1 -0
- package/.next/server/app/workspace/custom-scripts/page_client-reference-manifest.js +1 -0
- package/.next/server/app/workspace/custom-scripts.html +1 -0
- package/.next/server/app/workspace/custom-scripts.meta +16 -0
- package/.next/server/app/workspace/custom-scripts.rsc +25 -0
- package/.next/server/app/workspace/custom-scripts.segments/_full.segment.rsc +25 -0
- package/.next/server/app/workspace/custom-scripts.segments/_head.segment.rsc +6 -0
- package/.next/server/app/workspace/custom-scripts.segments/_index.segment.rsc +8 -0
- package/.next/server/app/workspace/custom-scripts.segments/_tree.segment.rsc +6 -0
- package/.next/server/app/workspace/custom-scripts.segments/workspace/custom-scripts/__PAGE__.segment.rsc +9 -0
- package/.next/server/app/workspace/custom-scripts.segments/workspace/custom-scripts.segment.rsc +4 -0
- package/.next/server/app/workspace/custom-scripts.segments/workspace.segment.rsc +7 -0
- package/.next/server/app/workspace/history/page.js +2 -0
- package/.next/server/app/workspace/history/page.js.nft.json +1 -0
- package/.next/server/app/workspace/history/page_client-reference-manifest.js +1 -0
- package/.next/server/app/workspace/history.html +1 -0
- package/.next/server/app/workspace/history.meta +16 -0
- package/.next/server/app/workspace/history.rsc +26 -0
- package/.next/server/app/workspace/history.segments/_full.segment.rsc +26 -0
- package/.next/server/app/workspace/history.segments/_head.segment.rsc +6 -0
- package/.next/server/app/workspace/history.segments/_index.segment.rsc +8 -0
- package/.next/server/app/workspace/history.segments/_tree.segment.rsc +7 -0
- package/.next/server/app/workspace/history.segments/workspace/history/__PAGE__.segment.rsc +10 -0
- package/.next/server/app/workspace/history.segments/workspace/history.segment.rsc +4 -0
- package/.next/server/app/workspace/history.segments/workspace.segment.rsc +7 -0
- package/.next/server/app/workspace/page.js +2 -0
- package/.next/server/app/workspace/page.js.nft.json +1 -0
- package/.next/server/app/workspace/page_client-reference-manifest.js +1 -0
- package/.next/server/app/workspace/settings/page.js +2 -0
- package/.next/server/app/workspace/settings/page.js.nft.json +1 -0
- package/.next/server/app/workspace/settings/page_client-reference-manifest.js +1 -0
- package/.next/server/app/workspace/settings.html +1 -0
- package/.next/server/app/workspace/settings.meta +16 -0
- package/.next/server/app/workspace/settings.rsc +25 -0
- package/.next/server/app/workspace/settings.segments/_full.segment.rsc +25 -0
- package/.next/server/app/workspace/settings.segments/_head.segment.rsc +6 -0
- package/.next/server/app/workspace/settings.segments/_index.segment.rsc +8 -0
- package/.next/server/app/workspace/settings.segments/_tree.segment.rsc +6 -0
- package/.next/server/app/workspace/settings.segments/workspace/settings/__PAGE__.segment.rsc +9 -0
- package/.next/server/app/workspace/settings.segments/workspace/settings.segment.rsc +4 -0
- package/.next/server/app/workspace/settings.segments/workspace.segment.rsc +7 -0
- package/.next/server/app/workspace/stashes/page.js +2 -0
- package/.next/server/app/workspace/stashes/page.js.nft.json +1 -0
- package/.next/server/app/workspace/stashes/page_client-reference-manifest.js +1 -0
- package/.next/server/app/workspace/stashes.html +1 -0
- package/.next/server/app/workspace/stashes.meta +16 -0
- package/.next/server/app/workspace/stashes.rsc +26 -0
- package/.next/server/app/workspace/stashes.segments/_full.segment.rsc +26 -0
- package/.next/server/app/workspace/stashes.segments/_head.segment.rsc +6 -0
- package/.next/server/app/workspace/stashes.segments/_index.segment.rsc +8 -0
- package/.next/server/app/workspace/stashes.segments/_tree.segment.rsc +7 -0
- package/.next/server/app/workspace/stashes.segments/workspace/stashes/__PAGE__.segment.rsc +10 -0
- package/.next/server/app/workspace/stashes.segments/workspace/stashes.segment.rsc +4 -0
- package/.next/server/app/workspace/stashes.segments/workspace.segment.rsc +7 -0
- package/.next/server/app/workspace.html +1 -0
- package/.next/server/app/workspace.meta +15 -0
- package/.next/server/app/workspace.rsc +26 -0
- package/.next/server/app/workspace.segments/_full.segment.rsc +26 -0
- package/.next/server/app/workspace.segments/_head.segment.rsc +6 -0
- package/.next/server/app/workspace.segments/_index.segment.rsc +8 -0
- package/.next/server/app/workspace.segments/_tree.segment.rsc +7 -0
- package/.next/server/app/workspace.segments/workspace/__PAGE__.segment.rsc +10 -0
- package/.next/server/app/workspace.segments/workspace.segment.rsc +7 -0
- package/.next/server/app-paths-manifest.json +23 -0
- package/.next/server/chunks/116.js +1 -0
- package/.next/server/chunks/170.js +1 -0
- package/.next/server/chunks/309.js +6 -0
- package/.next/server/chunks/356.js +1 -0
- package/.next/server/chunks/392.js +1 -0
- package/.next/server/chunks/443.js +37 -0
- package/.next/server/chunks/445.js +22 -0
- package/.next/server/chunks/471.js +13 -0
- package/.next/server/chunks/557.js +5 -0
- package/.next/server/chunks/680.js +1 -0
- package/.next/server/chunks/739.js +5 -0
- package/.next/server/chunks/781.js +1 -0
- package/.next/server/chunks/813.js +1 -0
- package/.next/server/chunks/816.js +13 -0
- package/.next/server/functions-config-manifest.json +6 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +2 -0
- package/.next/server/pages-manifest.json +4 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/131-6237c77d708182b1.js +1 -0
- package/.next/static/chunks/147-d4cb3e03218d75df.js +6 -0
- package/.next/static/chunks/176-b068120641913226.js +1 -0
- package/.next/static/chunks/200-3664e65687e716a2.js +1 -0
- package/.next/static/chunks/26-d93e82c9c09ff972.js +1 -0
- package/.next/static/chunks/4bd1b696-096d35a2bd1da3af.js +1 -0
- package/.next/static/chunks/500-5d45efbf86b8d36e.js +1 -0
- package/.next/static/chunks/772-b39107b40c31b6b6.js +3 -0
- package/.next/static/chunks/785-e6d755147ff87934.js +1 -0
- package/.next/static/chunks/790-3e8fa6e595d97940.js +6 -0
- package/.next/static/chunks/794-37dad9bbc14b04b8.js +2 -0
- package/.next/static/chunks/841-4d865378216d36eb.js +13 -0
- package/.next/static/chunks/app/_global-error/page-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/_not-found/page-5bea07380837254f.js +1 -0
- package/.next/static/chunks/app/api/credentials/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/api/custom-scripts/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/api/fs/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/api/git/action/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/api/git/branches/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/api/git/diff/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/api/git/log/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/api/git/status/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/api/repos/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/api/settings/route-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/app/credentials/page-23c1807ad9f68a40.js +1 -0
- package/.next/static/chunks/app/layout-a06241c385e50ae0.js +1 -0
- package/.next/static/chunks/app/page-c8404790f5191fd7.js +1 -0
- package/.next/static/chunks/app/workspace/changes/page-8cca17f80e66f534.js +3 -0
- package/.next/static/chunks/app/workspace/custom-scripts/page-ee24b9dd7779244f.js +1 -0
- package/.next/static/chunks/app/workspace/history/page-b7017fea285c804c.js +1 -0
- package/.next/static/chunks/app/workspace/layout-cb86c00fdcd8db08.js +1 -0
- package/.next/static/chunks/app/workspace/page-21211acc90c08509.js +1 -0
- package/.next/static/chunks/app/workspace/settings/page-7ccb58ece31492e9.js +1 -0
- package/.next/static/chunks/app/workspace/stashes/page-0e1b8304c4b0f4f3.js +1 -0
- package/.next/static/chunks/framework-75892d61b920805f.js +1 -0
- package/.next/static/chunks/main-app-221e1b945693a142.js +1 -0
- package/.next/static/chunks/main-f1d55fffd3cc9809.js +5 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-f2a2c324f3740ed8.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-9eb8a19109bbd711.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-7dec27dc3bfe207a.js +1 -0
- package/.next/static/css/36968b90b2339533.css +1 -0
- package/.next/static/css/c56c0e57397a3a60.css +1 -0
- package/.next/static/css/de70bee13400563f.css +1 -0
- package/.next/static/i5Fy6fyWcq44ejS8h_y1c/_buildManifest.js +1 -0
- package/.next/static/i5Fy6fyWcq44ejS8h_y1c/_ssgManifest.js +1 -0
- package/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- package/.next/static/media/747892c23ea88013-s.woff2 +0 -0
- package/.next/static/media/8d697b304b401681-s.woff2 +0 -0
- package/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- package/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
- package/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- package/README.md +2 -0
- package/bin/trident-git.mjs +9 -11
- package/package.json +7 -5
- package/eslint.config.mjs +0 -18
- package/postcss.config.mjs +0 -7
- package/src/app/api/credentials/route.ts +0 -113
- package/src/app/api/custom-scripts/route.ts +0 -203
- package/src/app/api/fs/route.ts +0 -75
- package/src/app/api/git/action/route.ts +0 -383
- package/src/app/api/git/branches/route.ts +0 -20
- package/src/app/api/git/diff/route.ts +0 -104
- package/src/app/api/git/log/route.ts +0 -28
- package/src/app/api/git/status/route.ts +0 -28
- package/src/app/api/repos/route.ts +0 -84
- package/src/app/api/settings/route.ts +0 -37
- package/src/app/credentials/page.tsx +0 -408
- package/src/app/globals.css +0 -109
- package/src/app/layout.tsx +0 -38
- package/src/app/page.tsx +0 -10
- package/src/app/providers.tsx +0 -21
- package/src/app/workspace/changes/page.tsx +0 -27
- package/src/app/workspace/custom-scripts/page.tsx +0 -247
- package/src/app/workspace/history/page.tsx +0 -27
- package/src/app/workspace/layout.tsx +0 -26
- package/src/app/workspace/page.tsx +0 -27
- package/src/app/workspace/settings/page.tsx +0 -233
- package/src/app/workspace/stashes/page.tsx +0 -395
- package/src/components/command-palette.tsx +0 -178
- package/src/components/context-menu.tsx +0 -200
- package/src/components/fs-browser.tsx +0 -154
- package/src/components/git/diff-view.tsx +0 -137
- package/src/components/git/git-graph.tsx +0 -489
- package/src/components/git/grouped-diff-viewer.tsx +0 -332
- package/src/components/git/history-view.tsx +0 -4862
- package/src/components/git/image-diff-view.tsx +0 -342
- package/src/components/git/status-view.tsx +0 -597
- package/src/components/home-settings-modal.tsx +0 -192
- package/src/components/layout/sidebar.tsx +0 -256
- package/src/components/repo-list.tsx +0 -206
- package/src/components/theme-toggle.tsx +0 -37
- package/src/components/toaster.tsx +0 -36
- package/src/components/workspace-repo-open-tracker.tsx +0 -39
- package/src/hooks/use-credentials.ts +0 -123
- package/src/hooks/use-escape-dismiss.ts +0 -72
- package/src/hooks/use-git.ts +0 -448
- package/src/hooks/use-toast.ts +0 -280
- package/src/hooks/use-workspace-title.ts +0 -23
- package/src/lib/api-utils.ts +0 -24
- package/src/lib/branch-colors.ts +0 -98
- package/src/lib/credentials.ts +0 -404
- package/src/lib/git.ts +0 -1510
- package/src/lib/graph-utils.ts +0 -253
- package/src/lib/store.ts +0 -145
- package/src/lib/types.ts +0 -95
- package/src/lib/utils.ts +0 -266
- package/tsconfig.json +0 -34
- /package/{src/app/icon.png → .next/server/app/icon.png.body} +0 -0
|
@@ -1,4862 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useGitLog, useGitBranches, useGitStatus, useGitAction, useCommitDiff, useCommitFileDiff, CommitFile, useRepository, useUpdateRepository, useSettings, useUpdateSettings } from '@/hooks/use-git';
|
|
4
|
-
import { Repository, RepositoryCustomScript, BranchTrackingInfo } from '@/lib/types';
|
|
5
|
-
import { GitGraph, GitGraphHandle } from './git-graph';
|
|
6
|
-
import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
|
7
|
-
import { useTheme } from 'next-themes';
|
|
8
|
-
import { cn, sanitizeBranchName, isFileBinary, isImageFile, getChangedLineCountFromDiff } from '@/lib/utils';
|
|
9
|
-
import { ContextMenu, ContextMenuItem } from '@/components/context-menu';
|
|
10
|
-
import { GroupedDiffViewer } from './grouped-diff-viewer';
|
|
11
|
-
import { ImageDiffView } from './image-diff-view';
|
|
12
|
-
import { useEscapeDismiss } from '@/hooks/use-escape-dismiss';
|
|
13
|
-
import { toast } from '@/hooks/use-toast';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// Visibility state for branches/folders
|
|
17
|
-
type VisibilityState = 'visible' | 'hidden' | null;
|
|
18
|
-
|
|
19
|
-
// Map of path -> visibility state
|
|
20
|
-
type VisibilityMap = Record<string, VisibilityState>;
|
|
21
|
-
|
|
22
|
-
// Tree node type for branch hierarchy
|
|
23
|
-
interface BranchTreeNode {
|
|
24
|
-
name: string;
|
|
25
|
-
fullPath?: string; // Only set for leaf nodes (actual branches)
|
|
26
|
-
children: Map<string, BranchTreeNode>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const MIN_HISTORY_PANEL_HEIGHT = 100;
|
|
30
|
-
const MAX_HISTORY_PANEL_HEIGHT = 900;
|
|
31
|
-
const MIN_COMMIT_DETAILS_MESSAGE_RATIO = 0.15;
|
|
32
|
-
const MAX_COMMIT_DETAILS_MESSAGE_RATIO = 0.75;
|
|
33
|
-
const DEFAULT_COMMIT_DETAILS_MESSAGE_RATIO = 0.28;
|
|
34
|
-
type MergeConflictStatus = 'checking' | 'no-conflict' | 'has-conflicts';
|
|
35
|
-
type CommitRowSelectModifiers = { isMultiSelect: boolean; isRangeSelect: boolean };
|
|
36
|
-
type BranchRowSelectModifiers = { isMultiSelect: boolean; isRangeSelect: boolean };
|
|
37
|
-
type BranchOperation = { sourceBranch: string; targetBranch: string };
|
|
38
|
-
type ScriptExecutionStatus = 'idle' | 'starting' | 'running' | 'completed' | 'failed' | 'canceled';
|
|
39
|
-
type ScriptExecutionState = {
|
|
40
|
-
isOpen: boolean;
|
|
41
|
-
executionId: string | null;
|
|
42
|
-
scriptName: string;
|
|
43
|
-
branchRef: string;
|
|
44
|
-
output: string;
|
|
45
|
-
status: ScriptExecutionStatus;
|
|
46
|
-
error: string | null;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
function clampHistoryPanelHeight(height: number): number {
|
|
50
|
-
return Math.min(Math.max(height, MIN_HISTORY_PANEL_HEIGHT), MAX_HISTORY_PANEL_HEIGHT);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function clampCommitDetailsMessageRatio(ratio: number): number {
|
|
54
|
-
return Math.min(Math.max(ratio, MIN_COMMIT_DETAILS_MESSAGE_RATIO), MAX_COMMIT_DETAILS_MESSAGE_RATIO);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function buildCommitMessage(subject: string, body: string): string {
|
|
58
|
-
const trimmedSubject = subject.trim();
|
|
59
|
-
const normalizedBody = body.replace(/\r\n/g, '\n');
|
|
60
|
-
return normalizedBody.trim() ? `${trimmedSubject}\n\n${normalizedBody}` : trimmedSubject;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function copyText(text: string): Promise<boolean> {
|
|
64
|
-
try {
|
|
65
|
-
await navigator.clipboard.writeText(text);
|
|
66
|
-
return true;
|
|
67
|
-
} catch {
|
|
68
|
-
const textArea = document.createElement('textarea');
|
|
69
|
-
textArea.value = text;
|
|
70
|
-
textArea.style.position = 'fixed';
|
|
71
|
-
textArea.style.left = '-9999px';
|
|
72
|
-
document.body.appendChild(textArea);
|
|
73
|
-
textArea.select();
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
document.execCommand('copy');
|
|
77
|
-
return true;
|
|
78
|
-
} catch {
|
|
79
|
-
return false;
|
|
80
|
-
} finally {
|
|
81
|
-
document.body.removeChild(textArea);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const DEFAULT_SCRIPT_EXECUTION: ScriptExecutionState = {
|
|
87
|
-
isOpen: false,
|
|
88
|
-
executionId: null,
|
|
89
|
-
scriptName: '',
|
|
90
|
-
branchRef: '',
|
|
91
|
-
output: '',
|
|
92
|
-
status: 'idle',
|
|
93
|
-
error: null,
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// File status icon component
|
|
98
|
-
function FileStatusIcon({ status }: { status: string }) {
|
|
99
|
-
switch (status) {
|
|
100
|
-
case 'A':
|
|
101
|
-
return <i className="iconoir-plus-circle text-[16px] text-success" aria-hidden="true" />;
|
|
102
|
-
case 'D':
|
|
103
|
-
return <i className="iconoir-minus-circle text-[16px] text-error" aria-hidden="true" />;
|
|
104
|
-
case 'M':
|
|
105
|
-
return <i className="iconoir-edit-pencil text-[16px] text-warning" aria-hidden="true" />;
|
|
106
|
-
default:
|
|
107
|
-
return <i className="iconoir-page text-[16px] opacity-50" aria-hidden="true" />;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
interface CommitFileTreeNode {
|
|
112
|
-
name: string;
|
|
113
|
-
path: string;
|
|
114
|
-
file?: CommitFile;
|
|
115
|
-
children: Map<string, CommitFileTreeNode>;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function buildCommitFileTree(files: CommitFile[]): CommitFileTreeNode {
|
|
119
|
-
const root: CommitFileTreeNode = {
|
|
120
|
-
name: '',
|
|
121
|
-
path: '',
|
|
122
|
-
children: new Map(),
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
for (const file of files) {
|
|
126
|
-
const parts = file.path.split('/').filter(Boolean);
|
|
127
|
-
let current = root;
|
|
128
|
-
let currentPath = '';
|
|
129
|
-
|
|
130
|
-
for (let i = 0; i < parts.length; i++) {
|
|
131
|
-
const part = parts[i];
|
|
132
|
-
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
133
|
-
|
|
134
|
-
if (!current.children.has(part)) {
|
|
135
|
-
current.children.set(part, {
|
|
136
|
-
name: part,
|
|
137
|
-
path: currentPath,
|
|
138
|
-
children: new Map(),
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
current = current.children.get(part)!;
|
|
143
|
-
|
|
144
|
-
if (i === parts.length - 1) {
|
|
145
|
-
current.file = file;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return root;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function collectCommitFolderPaths(node: CommitFileTreeNode): string[] {
|
|
154
|
-
const paths: string[] = [];
|
|
155
|
-
const children = Array.from(node.children.values());
|
|
156
|
-
|
|
157
|
-
children.forEach((child) => {
|
|
158
|
-
if (child.children.size > 0) {
|
|
159
|
-
paths.push(child.path);
|
|
160
|
-
paths.push(...collectCommitFolderPaths(child));
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
return paths;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function getParentPaths(filePath: string): string[] {
|
|
168
|
-
const parts = filePath.split('/').filter(Boolean);
|
|
169
|
-
const parentPaths: string[] = [];
|
|
170
|
-
|
|
171
|
-
for (let i = 1; i < parts.length; i++) {
|
|
172
|
-
parentPaths.push(parts.slice(0, i).join('/'));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return parentPaths;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function CommitFileTreeItem({
|
|
179
|
-
node,
|
|
180
|
-
selectedFile,
|
|
181
|
-
expandedFolders,
|
|
182
|
-
onToggleFolder,
|
|
183
|
-
onSelectFile,
|
|
184
|
-
depth = 0,
|
|
185
|
-
}: {
|
|
186
|
-
node: CommitFileTreeNode;
|
|
187
|
-
selectedFile: string | null;
|
|
188
|
-
expandedFolders: Set<string>;
|
|
189
|
-
onToggleFolder: (path: string) => void;
|
|
190
|
-
onSelectFile: (path: string) => void;
|
|
191
|
-
depth?: number;
|
|
192
|
-
}) {
|
|
193
|
-
const children = Array.from(node.children.values()).sort((a, b) => {
|
|
194
|
-
const aIsFolder = a.children.size > 0;
|
|
195
|
-
const bIsFolder = b.children.size > 0;
|
|
196
|
-
|
|
197
|
-
if (aIsFolder && !bIsFolder) return -1;
|
|
198
|
-
if (!aIsFolder && bIsFolder) return 1;
|
|
199
|
-
return a.name.localeCompare(b.name);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
return (
|
|
203
|
-
<>
|
|
204
|
-
{children.map((child) => {
|
|
205
|
-
const isFolder = child.children.size > 0;
|
|
206
|
-
|
|
207
|
-
if (isFolder) {
|
|
208
|
-
const isExpanded = expandedFolders.has(child.path);
|
|
209
|
-
|
|
210
|
-
return (
|
|
211
|
-
<div key={child.path}>
|
|
212
|
-
<div
|
|
213
|
-
className="flex items-center gap-1 px-2 py-1.5 text-xs rounded cursor-pointer hover:bg-base-200 transition-colors opacity-80"
|
|
214
|
-
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
|
215
|
-
onClick={() => onToggleFolder(child.path)}
|
|
216
|
-
title={child.path}
|
|
217
|
-
>
|
|
218
|
-
<span className="text-[10px] opacity-70">{isExpanded ? '▼' : '▶'}</span>
|
|
219
|
-
<i className="iconoir-folder text-[14px] opacity-70" aria-hidden="true" />
|
|
220
|
-
<span className="truncate flex-1">{child.name}</span>
|
|
221
|
-
</div>
|
|
222
|
-
{isExpanded && (
|
|
223
|
-
<CommitFileTreeItem
|
|
224
|
-
node={child}
|
|
225
|
-
selectedFile={selectedFile}
|
|
226
|
-
expandedFolders={expandedFolders}
|
|
227
|
-
onToggleFolder={onToggleFolder}
|
|
228
|
-
onSelectFile={onSelectFile}
|
|
229
|
-
depth={depth + 1}
|
|
230
|
-
/>
|
|
231
|
-
)}
|
|
232
|
-
</div>
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (!child.file) return null;
|
|
237
|
-
const file = child.file;
|
|
238
|
-
|
|
239
|
-
return (
|
|
240
|
-
<div
|
|
241
|
-
key={child.path}
|
|
242
|
-
className={cn(
|
|
243
|
-
"flex items-center gap-2 px-2 py-1.5 text-xs rounded cursor-pointer hover:bg-base-200 transition-colors",
|
|
244
|
-
selectedFile === file.path && "bg-base-200 font-medium"
|
|
245
|
-
)}
|
|
246
|
-
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
|
247
|
-
onClick={() => onSelectFile(file.path)}
|
|
248
|
-
title={file.path}
|
|
249
|
-
>
|
|
250
|
-
<FileStatusIcon status={file.status} />
|
|
251
|
-
<span className="truncate flex-1 font-mono">{child.name}</span>
|
|
252
|
-
</div>
|
|
253
|
-
);
|
|
254
|
-
})}
|
|
255
|
-
</>
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Component to show commit file diff
|
|
260
|
-
function CommitFileDiffView({ repoPath, commitHash, filePath, splitView }: { repoPath: string; commitHash: string; filePath: string; splitView: boolean }) {
|
|
261
|
-
const { data, isLoading } = useCommitFileDiff(repoPath, commitHash, filePath);
|
|
262
|
-
const { resolvedTheme } = useTheme();
|
|
263
|
-
const [renderAnyway, setRenderAnyway] = useState(false);
|
|
264
|
-
|
|
265
|
-
// Reset renderAnyway when file or commit changes
|
|
266
|
-
useEffect(() => {
|
|
267
|
-
setRenderAnyway(false);
|
|
268
|
-
}, [filePath, commitHash]);
|
|
269
|
-
|
|
270
|
-
if (isLoading) {
|
|
271
|
-
return <div className="flex items-center justify-center p-8"><span className="loading loading-spinner text-base-content/50"></span></div>;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (!data) {
|
|
275
|
-
return <div className="flex items-center justify-center p-8 opacity-50">No diff available</div>;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (isImageFile(filePath)) {
|
|
279
|
-
return <ImageDiffView filePath={filePath} imageDiff={data.imageDiff} />;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Check if file is binary (first by extension, then by content if unknown)
|
|
283
|
-
const isBinary = isFileBinary(filePath, data.left, data.right);
|
|
284
|
-
|
|
285
|
-
if (isBinary) {
|
|
286
|
-
return <div className="flex items-center justify-center p-8 opacity-50">Binary file - diff not available</div>;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Large file protection
|
|
290
|
-
const MAX_DIFF_SIZE = 100 * 1024; // 100KB
|
|
291
|
-
const MAX_DIFF_LINES = 3000;
|
|
292
|
-
|
|
293
|
-
const diffContent = data.diff || '';
|
|
294
|
-
const contentSize = diffContent.length;
|
|
295
|
-
const lineCount = getChangedLineCountFromDiff(diffContent);
|
|
296
|
-
|
|
297
|
-
const isLargeDiff = (contentSize > MAX_DIFF_SIZE || lineCount > MAX_DIFF_LINES);
|
|
298
|
-
|
|
299
|
-
if (isLargeDiff && !renderAnyway) {
|
|
300
|
-
return (
|
|
301
|
-
<div className="flex flex-col items-center justify-center h-full gap-4 text-center p-8">
|
|
302
|
-
<i className="iconoir-warning-triangle text-[32px] text-warning" aria-hidden="true" />
|
|
303
|
-
<div className="space-y-2">
|
|
304
|
-
<h3 className="font-bold text-lg">Large Diff Detected</h3>
|
|
305
|
-
<p className="opacity-70">
|
|
306
|
-
This diff is large ({Math.round(contentSize / 1024)}KB, ~{lineCount} changed lines) and may freeze your browser if rendered.
|
|
307
|
-
</p>
|
|
308
|
-
</div>
|
|
309
|
-
<button className="btn btn-outline" onClick={() => setRenderAnyway(true)}>
|
|
310
|
-
Show Diff Anyway
|
|
311
|
-
</button>
|
|
312
|
-
</div>
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return (
|
|
317
|
-
<div className="overflow-auto h-full">
|
|
318
|
-
<GroupedDiffViewer
|
|
319
|
-
oldValue={data.left || ''}
|
|
320
|
-
newValue={data.right || ''}
|
|
321
|
-
splitView={splitView}
|
|
322
|
-
useDarkTheme={resolvedTheme === 'dark'}
|
|
323
|
-
/>
|
|
324
|
-
</div>
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Component to show commit changes
|
|
329
|
-
function CommitChangesView({ repoPath, commitHash }: { repoPath: string; commitHash: string }) {
|
|
330
|
-
const { data, isLoading } = useCommitDiff(repoPath, commitHash);
|
|
331
|
-
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
|
332
|
-
const [collapsedFoldersByCommit, setCollapsedFoldersByCommit] = useState<Record<string, Set<string>>>({});
|
|
333
|
-
const fileTree = useMemo(() => buildCommitFileTree(data?.files ?? []), [data?.files]);
|
|
334
|
-
const allFolderPaths = useMemo(() => collectCommitFolderPaths(fileTree), [fileTree]);
|
|
335
|
-
const collapsedFolders = useMemo(
|
|
336
|
-
() => collapsedFoldersByCommit[commitHash] ?? new Set<string>(),
|
|
337
|
-
[collapsedFoldersByCommit, commitHash]
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
// Storage key for split view preference - same as in DiffView
|
|
341
|
-
const storageKey = 'git-web:diff-view-split';
|
|
342
|
-
|
|
343
|
-
const [splitView, setSplitView] = useState(() => {
|
|
344
|
-
if (typeof window === 'undefined') return true;
|
|
345
|
-
try {
|
|
346
|
-
const stored = localStorage.getItem(storageKey);
|
|
347
|
-
return stored !== null ? JSON.parse(stored) : true;
|
|
348
|
-
} catch (e) {
|
|
349
|
-
console.error('Failed to load split view preference:', e);
|
|
350
|
-
return true;
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// Save split view preference when it changes
|
|
355
|
-
useEffect(() => {
|
|
356
|
-
try {
|
|
357
|
-
localStorage.setItem(storageKey, JSON.stringify(splitView));
|
|
358
|
-
} catch (e) {
|
|
359
|
-
console.error('Failed to save split view preference:', e);
|
|
360
|
-
}
|
|
361
|
-
}, [splitView]);
|
|
362
|
-
|
|
363
|
-
// Reset selected file when commit changes
|
|
364
|
-
useEffect(() => {
|
|
365
|
-
setSelectedFile(null);
|
|
366
|
-
}, [commitHash]);
|
|
367
|
-
|
|
368
|
-
// Auto-select first file when data loads and no file is selected
|
|
369
|
-
useEffect(() => {
|
|
370
|
-
if (!selectedFile && data?.files && data.files.length > 0) {
|
|
371
|
-
setSelectedFile(data.files[0].path);
|
|
372
|
-
}
|
|
373
|
-
}, [data?.files, selectedFile]);
|
|
374
|
-
|
|
375
|
-
const expandedFolders = useMemo(() => {
|
|
376
|
-
const expanded = new Set<string>();
|
|
377
|
-
|
|
378
|
-
allFolderPaths.forEach((path) => {
|
|
379
|
-
if (!collapsedFolders.has(path)) {
|
|
380
|
-
expanded.add(path);
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
if (selectedFile) {
|
|
385
|
-
getParentPaths(selectedFile).forEach((path) => expanded.add(path));
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return expanded;
|
|
389
|
-
}, [allFolderPaths, collapsedFolders, selectedFile]);
|
|
390
|
-
|
|
391
|
-
const handleToggleFolder = useCallback((path: string) => {
|
|
392
|
-
setCollapsedFoldersByCommit((prev) => {
|
|
393
|
-
const current = prev[commitHash] ?? new Set<string>();
|
|
394
|
-
const next = new Set(current);
|
|
395
|
-
if (next.has(path)) {
|
|
396
|
-
next.delete(path);
|
|
397
|
-
} else {
|
|
398
|
-
next.add(path);
|
|
399
|
-
}
|
|
400
|
-
return {
|
|
401
|
-
...prev,
|
|
402
|
-
[commitHash]: next,
|
|
403
|
-
};
|
|
404
|
-
});
|
|
405
|
-
}, [commitHash]);
|
|
406
|
-
|
|
407
|
-
if (isLoading) {
|
|
408
|
-
return <div className="flex items-center justify-center p-8 h-full"><span className="loading loading-spinner text-base-content/50"></span></div>;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (!data || data.files.length === 0) {
|
|
412
|
-
return <div className="flex items-center justify-center p-8 h-full opacity-50">No changes in this commit</div>;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return (
|
|
416
|
-
<div className="flex h-full">
|
|
417
|
-
{/* File list */}
|
|
418
|
-
<div className="w-64 border-r border-base-300 flex flex-col bg-base-200/30 shrink-0">
|
|
419
|
-
<div className="px-3 py-2 text-xs font-bold opacity-70 border-b border-base-300 bg-base-100">
|
|
420
|
-
{data.files.length} file{data.files.length !== 1 ? 's' : ''} changed
|
|
421
|
-
</div>
|
|
422
|
-
<div className="flex-1 overflow-y-auto min-h-0">
|
|
423
|
-
<div className="p-1">
|
|
424
|
-
<CommitFileTreeItem
|
|
425
|
-
node={fileTree}
|
|
426
|
-
selectedFile={selectedFile}
|
|
427
|
-
expandedFolders={expandedFolders}
|
|
428
|
-
onToggleFolder={handleToggleFolder}
|
|
429
|
-
onSelectFile={setSelectedFile}
|
|
430
|
-
/>
|
|
431
|
-
</div>
|
|
432
|
-
</div>
|
|
433
|
-
</div>
|
|
434
|
-
|
|
435
|
-
{/* Diff view */}
|
|
436
|
-
<div className="flex-1 overflow-hidden">
|
|
437
|
-
{selectedFile ? (
|
|
438
|
-
<div className="h-full flex flex-col">
|
|
439
|
-
<div className="px-4 py-2 text-xs font-mono opacity-70 border-b border-base-300 bg-base-100 shrink-0 truncate flex items-center justify-between">
|
|
440
|
-
<span className="truncate">{selectedFile}</span>
|
|
441
|
-
<div className="flex items-center gap-2 shrink-0 ml-4">
|
|
442
|
-
<label htmlFor="commit-diff-split-view" className="text-[10px] uppercase tracking-wider font-bold cursor-pointer opacity-70">Split View</label>
|
|
443
|
-
<input
|
|
444
|
-
type="checkbox"
|
|
445
|
-
id="commit-diff-split-view"
|
|
446
|
-
checked={splitView}
|
|
447
|
-
onChange={(e) => setSplitView(e.target.checked)}
|
|
448
|
-
className="toggle toggle-xs toggle-primary"
|
|
449
|
-
/>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
|
-
<div className="flex-1 overflow-auto diff-viewer-wrapper">
|
|
453
|
-
<CommitFileDiffView repoPath={repoPath} commitHash={commitHash} filePath={selectedFile} splitView={splitView} />
|
|
454
|
-
</div>
|
|
455
|
-
</div>
|
|
456
|
-
) : (
|
|
457
|
-
<div className="flex items-center justify-center h-full opacity-70 text-sm">
|
|
458
|
-
Select a file to view changes
|
|
459
|
-
</div>
|
|
460
|
-
)}
|
|
461
|
-
</div>
|
|
462
|
-
</div>
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Build tree structure from flat branch list
|
|
467
|
-
function buildBranchTree(branches: string[], pathPrefix: string = ''): BranchTreeNode {
|
|
468
|
-
const root: BranchTreeNode = { name: '', children: new Map() };
|
|
469
|
-
|
|
470
|
-
for (const branch of branches) {
|
|
471
|
-
const parts = branch.split('/');
|
|
472
|
-
let current = root;
|
|
473
|
-
|
|
474
|
-
for (let i = 0; i < parts.length; i++) {
|
|
475
|
-
const part = parts[i];
|
|
476
|
-
if (!current.children.has(part)) {
|
|
477
|
-
current.children.set(part, {
|
|
478
|
-
name: part,
|
|
479
|
-
children: new Map(),
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
current = current.children.get(part)!;
|
|
483
|
-
|
|
484
|
-
// If this is the last part, mark it as a leaf with full path
|
|
485
|
-
if (i === parts.length - 1) {
|
|
486
|
-
// Use the full path with prefix for remote branches
|
|
487
|
-
current.fullPath = pathPrefix ? `${pathPrefix}/${branch}` : branch;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return root;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Build tree structure for remote branches, grouped by remote name
|
|
496
|
-
function buildRemoteBranchTree(remotes: Record<string, string[]>): Map<string, BranchTreeNode> {
|
|
497
|
-
const result = new Map<string, BranchTreeNode>();
|
|
498
|
-
|
|
499
|
-
for (const [remoteName, branches] of Object.entries(remotes)) {
|
|
500
|
-
// Build tree for this remote's branches, with full ref path prefix
|
|
501
|
-
result.set(remoteName, buildBranchTree(branches, `remotes/${remoteName}`));
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return result;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Get the effective visibility for a path (considering parent inheritance)
|
|
508
|
-
// groupPath is used for checking group-level visibility (e.g., "__local__" or "__remotes__" or "__remotes__/origin")
|
|
509
|
-
function getEffectiveVisibility(
|
|
510
|
-
path: string,
|
|
511
|
-
visibilityMap: VisibilityMap,
|
|
512
|
-
groupPath?: string
|
|
513
|
-
): VisibilityState {
|
|
514
|
-
// Check if this path has explicit visibility
|
|
515
|
-
if (visibilityMap[path]) {
|
|
516
|
-
return visibilityMap[path];
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Check parent paths for inherited visibility
|
|
520
|
-
const parts = path.split('/');
|
|
521
|
-
for (let i = parts.length - 1; i > 0; i--) {
|
|
522
|
-
const parentPath = parts.slice(0, i).join('/');
|
|
523
|
-
if (visibilityMap[parentPath]) {
|
|
524
|
-
return visibilityMap[parentPath];
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Check group-level visibility
|
|
529
|
-
if (groupPath) {
|
|
530
|
-
// Check if any parent group has visibility set
|
|
531
|
-
const groupParts = groupPath.split('/');
|
|
532
|
-
for (let i = groupParts.length; i > 0; i--) {
|
|
533
|
-
const parentGroupPath = groupParts.slice(0, i).join('/');
|
|
534
|
-
if (visibilityMap[parentGroupPath]) {
|
|
535
|
-
return visibilityMap[parentGroupPath];
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Group header component with visibility controls
|
|
544
|
-
function GroupHeader({
|
|
545
|
-
name,
|
|
546
|
-
groupPath,
|
|
547
|
-
icon,
|
|
548
|
-
isExpanded,
|
|
549
|
-
onToggle,
|
|
550
|
-
visibilityMap,
|
|
551
|
-
onToggleVisibility,
|
|
552
|
-
depth = 0,
|
|
553
|
-
}: {
|
|
554
|
-
name: string;
|
|
555
|
-
groupPath: string;
|
|
556
|
-
icon: React.ReactNode;
|
|
557
|
-
isExpanded: boolean;
|
|
558
|
-
onToggle: () => void;
|
|
559
|
-
visibilityMap: VisibilityMap;
|
|
560
|
-
onToggleVisibility: (path: string, type: 'visible' | 'hidden') => void;
|
|
561
|
-
depth?: number;
|
|
562
|
-
}) {
|
|
563
|
-
const directVisibility = visibilityMap[groupPath];
|
|
564
|
-
// Check parent group visibility for inheritance
|
|
565
|
-
const parentGroupPath = groupPath.includes('/')
|
|
566
|
-
? groupPath.split('/').slice(0, -1).join('/')
|
|
567
|
-
: undefined;
|
|
568
|
-
const parentVisibility = parentGroupPath ? visibilityMap[parentGroupPath] : null;
|
|
569
|
-
const effectiveVisibility = directVisibility || parentVisibility;
|
|
570
|
-
const isInherited = !directVisibility && parentVisibility !== null;
|
|
571
|
-
|
|
572
|
-
return (
|
|
573
|
-
<div
|
|
574
|
-
className={cn(
|
|
575
|
-
"group flex items-center gap-1 px-2 py-1.5 text-sm rounded-md cursor-pointer hover:bg-base-200 transition-colors font-medium",
|
|
576
|
-
)}
|
|
577
|
-
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
|
578
|
-
>
|
|
579
|
-
<div className="flex items-center gap-1.5 flex-1 min-w-0" onClick={onToggle}>
|
|
580
|
-
<span className="text-xs opacity-70">{isExpanded ? '▼' : '▶'}</span>
|
|
581
|
-
<span className="shrink-0">{icon}</span>
|
|
582
|
-
<span className="truncate min-w-0 flex-1">{name}</span>
|
|
583
|
-
</div>
|
|
584
|
-
<div className="flex items-center gap-0.5 ml-auto">
|
|
585
|
-
<VisibilityToggle
|
|
586
|
-
type="visible"
|
|
587
|
-
isActive={directVisibility === 'visible' || (isInherited && effectiveVisibility === 'visible')}
|
|
588
|
-
isInherited={isInherited && effectiveVisibility === 'visible'}
|
|
589
|
-
onClick={(e) => { e.stopPropagation(); onToggleVisibility(groupPath, 'visible'); }}
|
|
590
|
-
showOnHover={directVisibility === 'visible' || (isInherited && effectiveVisibility === 'visible')}
|
|
591
|
-
/>
|
|
592
|
-
<VisibilityToggle
|
|
593
|
-
type="hidden"
|
|
594
|
-
isActive={directVisibility === 'hidden' || (isInherited && effectiveVisibility === 'hidden')}
|
|
595
|
-
isInherited={isInherited && effectiveVisibility === 'hidden'}
|
|
596
|
-
onClick={(e) => { e.stopPropagation(); onToggleVisibility(groupPath, 'hidden'); }}
|
|
597
|
-
showOnHover={directVisibility === 'hidden' || (isInherited && effectiveVisibility === 'hidden')}
|
|
598
|
-
/>
|
|
599
|
-
</div>
|
|
600
|
-
</div>
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
// Visibility toggle button component
|
|
606
|
-
function VisibilityToggle({
|
|
607
|
-
type,
|
|
608
|
-
isActive,
|
|
609
|
-
isInherited,
|
|
610
|
-
onClick,
|
|
611
|
-
showOnHover,
|
|
612
|
-
}: {
|
|
613
|
-
type: 'visible' | 'hidden';
|
|
614
|
-
isActive: boolean;
|
|
615
|
-
isInherited: boolean;
|
|
616
|
-
onClick: (e: React.MouseEvent) => void;
|
|
617
|
-
showOnHover: boolean;
|
|
618
|
-
}) {
|
|
619
|
-
const title = type === 'visible'
|
|
620
|
-
? (isActive ? 'Remove visible filter' : 'Show only this branch')
|
|
621
|
-
: (isActive ? 'Remove hide filter' : 'Hide this branch');
|
|
622
|
-
|
|
623
|
-
return (
|
|
624
|
-
<button
|
|
625
|
-
className={cn(
|
|
626
|
-
"p-0.5 rounded hover:bg-base-300 transition-colors shrink-0 cursor-pointer text-xs",
|
|
627
|
-
isActive && "bg-primary/10",
|
|
628
|
-
isInherited && "opacity-50",
|
|
629
|
-
!isActive && !showOnHover && "opacity-0 group-hover:opacity-100"
|
|
630
|
-
)}
|
|
631
|
-
onClick={onClick}
|
|
632
|
-
title={title}
|
|
633
|
-
>
|
|
634
|
-
{type === 'visible' ? <i className="iconoir-eye text-[14px]" aria-hidden="true" /> : <i className="iconoir-eye-closed text-[14px]" aria-hidden="true" />}
|
|
635
|
-
</button>
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
interface BranchMenuCallbacks {
|
|
640
|
-
onCheckout: (branch: string) => void;
|
|
641
|
-
onCheckoutToLocal: (remoteBranch: string) => void;
|
|
642
|
-
onCreateBranch: (sourceBranch: string) => void;
|
|
643
|
-
onDeleteBranch: (branch: string) => void;
|
|
644
|
-
onDeleteBranches: (branches: string[]) => void;
|
|
645
|
-
onRenameBranch: (branch: string) => void;
|
|
646
|
-
onRenameRemoteBranch: (branch: string) => void;
|
|
647
|
-
onRebase: (operation: BranchOperation) => void;
|
|
648
|
-
onMerge: (operation: BranchOperation) => void;
|
|
649
|
-
onPushToRemote: (branch: string) => void;
|
|
650
|
-
onPullFromRemote: (branch: string) => void;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
interface BranchMenuOptions {
|
|
654
|
-
branchRef: string;
|
|
655
|
-
branchLeafName: string;
|
|
656
|
-
currentBranch?: string;
|
|
657
|
-
isRemote: boolean;
|
|
658
|
-
selectedBranchRefs?: string[];
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function sortBranchTreeChildren(node: BranchTreeNode): BranchTreeNode[] {
|
|
662
|
-
return Array.from(node.children.values()).sort((a, b) => {
|
|
663
|
-
const aIsFolder = a.children.size > 0 && !a.fullPath;
|
|
664
|
-
const bIsFolder = b.children.size > 0 && !b.fullPath;
|
|
665
|
-
if (aIsFolder && !bIsFolder) return -1;
|
|
666
|
-
if (!aIsFolder && bIsFolder) return 1;
|
|
667
|
-
return a.name.localeCompare(b.name);
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function collectVisibleBranchRefs(
|
|
672
|
-
node: BranchTreeNode,
|
|
673
|
-
expandedFolders: Set<string>,
|
|
674
|
-
parentPath = ''
|
|
675
|
-
): string[] {
|
|
676
|
-
const refs: string[] = [];
|
|
677
|
-
const sortedChildren = sortBranchTreeChildren(node);
|
|
678
|
-
|
|
679
|
-
for (const child of sortedChildren) {
|
|
680
|
-
const isLeaf = child.fullPath !== undefined;
|
|
681
|
-
if (isLeaf) {
|
|
682
|
-
refs.push(child.fullPath!);
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
const itemPath = parentPath ? `${parentPath}/${child.name}` : child.name;
|
|
687
|
-
if (expandedFolders.has(itemPath)) {
|
|
688
|
-
refs.push(...collectVisibleBranchRefs(child, expandedFolders, itemPath));
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return refs;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function collectAllBranchRefs(node: BranchTreeNode): string[] {
|
|
696
|
-
const refs: string[] = [];
|
|
697
|
-
const sortedChildren = sortBranchTreeChildren(node);
|
|
698
|
-
|
|
699
|
-
for (const child of sortedChildren) {
|
|
700
|
-
if (child.fullPath) {
|
|
701
|
-
refs.push(child.fullPath);
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
refs.push(...collectAllBranchRefs(child));
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
return refs;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function buildBranchContextMenuItems(
|
|
712
|
-
options: BranchMenuOptions,
|
|
713
|
-
callbacks: BranchMenuCallbacks
|
|
714
|
-
): ContextMenuItem[] {
|
|
715
|
-
const { branchRef, branchLeafName, currentBranch, isRemote, selectedBranchRefs } = options;
|
|
716
|
-
const isCurrent = !isRemote && branchRef === currentBranch;
|
|
717
|
-
const selectedRefs = selectedBranchRefs && selectedBranchRefs.length > 0 ? selectedBranchRefs : [branchRef];
|
|
718
|
-
const hasMultiSelection = selectedRefs.length > 1;
|
|
719
|
-
const menuItems: ContextMenuItem[] = [];
|
|
720
|
-
|
|
721
|
-
if (!isCurrent && !isRemote) menuItems.push({ label: 'Checkout', onClick: () => callbacks.onCheckout(branchRef) });
|
|
722
|
-
if (isRemote) menuItems.push({ label: 'Checkout to local', onClick: () => callbacks.onCheckoutToLocal(branchRef) });
|
|
723
|
-
menuItems.push({ label: 'Create Branch', onClick: () => callbacks.onCreateBranch(branchRef) });
|
|
724
|
-
if (!isRemote) menuItems.push({ label: 'Rename Branch', onClick: () => callbacks.onRenameBranch(branchRef) });
|
|
725
|
-
if (isRemote) menuItems.push({ label: 'Rename branch', onClick: () => callbacks.onRenameRemoteBranch(branchRef) });
|
|
726
|
-
if (!isRemote) menuItems.push({ label: 'Push to Remote', onClick: () => callbacks.onPushToRemote(branchRef) });
|
|
727
|
-
if (!isRemote) menuItems.push({ label: 'Pull from Remote', onClick: () => callbacks.onPullFromRemote(branchRef) });
|
|
728
|
-
if (!isCurrent && currentBranch) {
|
|
729
|
-
menuItems.push({
|
|
730
|
-
label: `Rebase ${currentBranch} onto ${branchLeafName}`,
|
|
731
|
-
labelNode: <>Rebase <span className="font-bold">{currentBranch}</span> onto {branchLeafName}</>,
|
|
732
|
-
onClick: () => callbacks.onRebase({ sourceBranch: currentBranch, targetBranch: branchRef }),
|
|
733
|
-
});
|
|
734
|
-
menuItems.push({
|
|
735
|
-
label: `Rebase ${branchLeafName} onto ${currentBranch}`,
|
|
736
|
-
labelNode: <>Rebase {branchLeafName} onto <span className="font-bold">{currentBranch}</span></>,
|
|
737
|
-
onClick: () => callbacks.onRebase({ sourceBranch: branchRef, targetBranch: currentBranch }),
|
|
738
|
-
});
|
|
739
|
-
menuItems.push({
|
|
740
|
-
label: `Merge ${currentBranch} into ${branchLeafName}`,
|
|
741
|
-
labelNode: <>Merge <span className="font-bold">{currentBranch}</span> into {branchLeafName}</>,
|
|
742
|
-
onClick: () => callbacks.onMerge({ sourceBranch: currentBranch, targetBranch: branchRef }),
|
|
743
|
-
});
|
|
744
|
-
menuItems.push({
|
|
745
|
-
label: `Merge ${branchLeafName} into ${currentBranch}`,
|
|
746
|
-
labelNode: <>Merge {branchLeafName} into <span className="font-bold">{currentBranch}</span></>,
|
|
747
|
-
onClick: () => callbacks.onMerge({ sourceBranch: branchRef, targetBranch: currentBranch }),
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
if (!isCurrent) {
|
|
751
|
-
if (hasMultiSelection) {
|
|
752
|
-
menuItems.push({
|
|
753
|
-
label: `Delete Selected Branches (${selectedRefs.length})`,
|
|
754
|
-
onClick: () => callbacks.onDeleteBranches(selectedRefs),
|
|
755
|
-
danger: true,
|
|
756
|
-
});
|
|
757
|
-
} else {
|
|
758
|
-
menuItems.push({
|
|
759
|
-
label: isRemote ? 'Delete Remote Branch' : 'Delete Branch',
|
|
760
|
-
onClick: () => callbacks.onDeleteBranch(branchRef),
|
|
761
|
-
danger: true,
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
return menuItems;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// Recursive component to render branch tree
|
|
770
|
-
function BranchTreeItem({
|
|
771
|
-
node,
|
|
772
|
-
currentBranch,
|
|
773
|
-
expandedFolders,
|
|
774
|
-
onToggleFolder,
|
|
775
|
-
onCheckout,
|
|
776
|
-
onCheckoutToLocal,
|
|
777
|
-
onCreateBranch,
|
|
778
|
-
onDeleteBranch,
|
|
779
|
-
onRenameBranch,
|
|
780
|
-
onRenameRemoteBranch,
|
|
781
|
-
onRebase,
|
|
782
|
-
onMerge,
|
|
783
|
-
onPushToRemote,
|
|
784
|
-
onPullFromRemote,
|
|
785
|
-
getBranchContextMenuItems,
|
|
786
|
-
onBranchClick,
|
|
787
|
-
onBranchContextMenu,
|
|
788
|
-
onDeleteBranchGroup,
|
|
789
|
-
selectedBranches,
|
|
790
|
-
visibilityMap,
|
|
791
|
-
onToggleVisibility,
|
|
792
|
-
parentPath = '',
|
|
793
|
-
depth = 0,
|
|
794
|
-
groupPath,
|
|
795
|
-
isRemote = false,
|
|
796
|
-
trackingInfo,
|
|
797
|
-
}: {
|
|
798
|
-
node: BranchTreeNode;
|
|
799
|
-
currentBranch?: string;
|
|
800
|
-
expandedFolders: Set<string>;
|
|
801
|
-
onToggleFolder: (path: string) => void;
|
|
802
|
-
onCheckout: (branch: string) => void;
|
|
803
|
-
onCheckoutToLocal: (remoteBranch: string) => void;
|
|
804
|
-
onCreateBranch: () => void;
|
|
805
|
-
onDeleteBranch: (branch: string) => void;
|
|
806
|
-
onRenameBranch: (branch: string) => void;
|
|
807
|
-
onRenameRemoteBranch: (branch: string) => void;
|
|
808
|
-
onRebase: (operation: BranchOperation) => void;
|
|
809
|
-
onMerge: (operation: BranchOperation) => void;
|
|
810
|
-
onPushToRemote: (branch: string) => void;
|
|
811
|
-
onPullFromRemote: (branch: string) => void;
|
|
812
|
-
getBranchContextMenuItems: (options: BranchMenuOptions) => ContextMenuItem[];
|
|
813
|
-
onBranchClick?: (branch: string, modifiers?: BranchRowSelectModifiers) => void;
|
|
814
|
-
onBranchContextMenu?: (branch: string) => void;
|
|
815
|
-
onDeleteBranchGroup: (branches: string[]) => void;
|
|
816
|
-
selectedBranches: Set<string>;
|
|
817
|
-
visibilityMap: VisibilityMap;
|
|
818
|
-
onToggleVisibility: (path: string, type: 'visible' | 'hidden') => void;
|
|
819
|
-
parentPath?: string;
|
|
820
|
-
depth?: number;
|
|
821
|
-
groupPath?: string;
|
|
822
|
-
isRemote?: boolean;
|
|
823
|
-
trackingInfo?: Record<string, BranchTrackingInfo>;
|
|
824
|
-
}) {
|
|
825
|
-
const children = Array.from(node.children.values());
|
|
826
|
-
const sortedChildren = children.sort((a, b) => {
|
|
827
|
-
// Folders (non-leaf) come first, then alphabetical
|
|
828
|
-
const aIsFolder = a.children.size > 0 && !a.fullPath;
|
|
829
|
-
const bIsFolder = b.children.size > 0 && !b.fullPath;
|
|
830
|
-
if (aIsFolder && !bIsFolder) return -1;
|
|
831
|
-
if (!aIsFolder && bIsFolder) return 1;
|
|
832
|
-
return a.name.localeCompare(b.name);
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
return (
|
|
836
|
-
<>
|
|
837
|
-
{sortedChildren.map((child) => {
|
|
838
|
-
const isLeaf = child.fullPath !== undefined;
|
|
839
|
-
const isFolder = child.children.size > 0 && !isLeaf;
|
|
840
|
-
const itemPath = child.fullPath || (parentPath ? `${parentPath}/${child.name}` : child.name);
|
|
841
|
-
const isExpanded = expandedFolders.has(itemPath);
|
|
842
|
-
const isCurrent = isLeaf && child.fullPath === currentBranch;
|
|
843
|
-
|
|
844
|
-
// Get visibility state for this item
|
|
845
|
-
const directVisibility = visibilityMap[itemPath];
|
|
846
|
-
const effectiveVisibility = getEffectiveVisibility(itemPath, visibilityMap, groupPath);
|
|
847
|
-
const isInherited = !directVisibility && effectiveVisibility !== null;
|
|
848
|
-
|
|
849
|
-
if (isFolder) {
|
|
850
|
-
const childBranchRefs = collectAllBranchRefs(child);
|
|
851
|
-
const deletableChildBranchRefs = childBranchRefs.filter((branchRef) => branchRef !== currentBranch);
|
|
852
|
-
|
|
853
|
-
// Render folder
|
|
854
|
-
return (
|
|
855
|
-
<div key={itemPath}>
|
|
856
|
-
<ContextMenu
|
|
857
|
-
items={[
|
|
858
|
-
{
|
|
859
|
-
label: 'Delete',
|
|
860
|
-
onClick: () => onDeleteBranchGroup(deletableChildBranchRefs),
|
|
861
|
-
danger: true,
|
|
862
|
-
disabled: deletableChildBranchRefs.length === 0,
|
|
863
|
-
},
|
|
864
|
-
]}
|
|
865
|
-
>
|
|
866
|
-
<div
|
|
867
|
-
className={cn(
|
|
868
|
-
"group flex items-center gap-1 px-2 py-1.5 text-sm rounded-md cursor-pointer hover:bg-base-200 transition-colors opacity-70 select-none",
|
|
869
|
-
)}
|
|
870
|
-
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
|
871
|
-
>
|
|
872
|
-
<div className="flex items-center gap-1 flex-1 min-w-0" onClick={() => onToggleFolder(itemPath)}>
|
|
873
|
-
<span className="text-xs opacity-70">{isExpanded ? '▼' : '▶'}</span>
|
|
874
|
-
<i className="iconoir-folder text-[16px] shrink-0" aria-hidden="true" />
|
|
875
|
-
<span className="truncate min-w-0 flex-1">{child.name}</span>
|
|
876
|
-
</div>
|
|
877
|
-
<div className="flex items-center gap-0.5 ml-auto">
|
|
878
|
-
<VisibilityToggle
|
|
879
|
-
type="visible"
|
|
880
|
-
isActive={directVisibility === 'visible' || (isInherited && effectiveVisibility === 'visible')}
|
|
881
|
-
isInherited={isInherited && effectiveVisibility === 'visible'}
|
|
882
|
-
onClick={(e) => { e.stopPropagation(); onToggleVisibility(itemPath, 'visible'); }}
|
|
883
|
-
showOnHover={directVisibility === 'visible' || (isInherited && effectiveVisibility === 'visible')}
|
|
884
|
-
/>
|
|
885
|
-
<VisibilityToggle
|
|
886
|
-
type="hidden"
|
|
887
|
-
isActive={directVisibility === 'hidden' || (isInherited && effectiveVisibility === 'hidden')}
|
|
888
|
-
isInherited={isInherited && effectiveVisibility === 'hidden'}
|
|
889
|
-
onClick={(e) => { e.stopPropagation(); onToggleVisibility(itemPath, 'hidden'); }}
|
|
890
|
-
showOnHover={directVisibility === 'hidden' || (isInherited && effectiveVisibility === 'hidden')}
|
|
891
|
-
/>
|
|
892
|
-
</div>
|
|
893
|
-
</div>
|
|
894
|
-
</ContextMenu>
|
|
895
|
-
{isExpanded && (
|
|
896
|
-
<BranchTreeItem
|
|
897
|
-
node={child}
|
|
898
|
-
currentBranch={currentBranch}
|
|
899
|
-
expandedFolders={expandedFolders}
|
|
900
|
-
onToggleFolder={onToggleFolder}
|
|
901
|
-
onCheckout={onCheckout}
|
|
902
|
-
onCheckoutToLocal={onCheckoutToLocal}
|
|
903
|
-
onCreateBranch={onCreateBranch}
|
|
904
|
-
onDeleteBranch={onDeleteBranch}
|
|
905
|
-
onRenameBranch={onRenameBranch}
|
|
906
|
-
onRenameRemoteBranch={onRenameRemoteBranch}
|
|
907
|
-
onRebase={onRebase}
|
|
908
|
-
onMerge={onMerge}
|
|
909
|
-
onPushToRemote={onPushToRemote}
|
|
910
|
-
onPullFromRemote={onPullFromRemote}
|
|
911
|
-
getBranchContextMenuItems={getBranchContextMenuItems}
|
|
912
|
-
onBranchClick={onBranchClick}
|
|
913
|
-
onBranchContextMenu={onBranchContextMenu}
|
|
914
|
-
onDeleteBranchGroup={onDeleteBranchGroup}
|
|
915
|
-
selectedBranches={selectedBranches}
|
|
916
|
-
visibilityMap={visibilityMap}
|
|
917
|
-
onToggleVisibility={onToggleVisibility}
|
|
918
|
-
parentPath={itemPath}
|
|
919
|
-
depth={depth + 1}
|
|
920
|
-
groupPath={groupPath}
|
|
921
|
-
isRemote={isRemote}
|
|
922
|
-
trackingInfo={trackingInfo}
|
|
923
|
-
/>
|
|
924
|
-
)}
|
|
925
|
-
</div>
|
|
926
|
-
);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// Render leaf (actual branch)
|
|
930
|
-
const branchTracking = !isRemote && child.fullPath ? trackingInfo?.[child.fullPath] : undefined;
|
|
931
|
-
const hasDivergence = branchTracking && (branchTracking.ahead > 0 || branchTracking.behind > 0);
|
|
932
|
-
|
|
933
|
-
const menuItems = getBranchContextMenuItems({
|
|
934
|
-
branchRef: child.fullPath!,
|
|
935
|
-
branchLeafName: child.name,
|
|
936
|
-
currentBranch,
|
|
937
|
-
isRemote,
|
|
938
|
-
selectedBranchRefs: selectedBranches.has(child.fullPath!) ? Array.from(selectedBranches) : [child.fullPath!],
|
|
939
|
-
});
|
|
940
|
-
const isSelected = selectedBranches.has(child.fullPath!);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
return (
|
|
944
|
-
<ContextMenu key={child.fullPath} items={menuItems}>
|
|
945
|
-
<div
|
|
946
|
-
className={cn(
|
|
947
|
-
"group flex items-center gap-2 px-2 py-1.5 text-sm rounded-md cursor-pointer transition-colors select-none",
|
|
948
|
-
!isSelected && "hover:bg-base-200",
|
|
949
|
-
isSelected && "bg-primary/10 hover:bg-primary/20",
|
|
950
|
-
isCurrent && "bg-base-200 font-medium text-primary"
|
|
951
|
-
)}
|
|
952
|
-
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
|
953
|
-
onContextMenu={() => onBranchContextMenu?.(child.fullPath!)}
|
|
954
|
-
>
|
|
955
|
-
<div
|
|
956
|
-
className="flex items-center gap-2 flex-1 min-w-0"
|
|
957
|
-
onClick={(e) => onBranchClick?.(child.fullPath!, { isMultiSelect: e.metaKey || e.ctrlKey, isRangeSelect: e.shiftKey })}
|
|
958
|
-
onDoubleClick={() => !isCurrent && onCheckout(child.fullPath!)}
|
|
959
|
-
>
|
|
960
|
-
{isCurrent ? (
|
|
961
|
-
<span className="w-3 h-3 flex items-center justify-center shrink-0">
|
|
962
|
-
<span className="w-2 h-2 rounded-full bg-primary" />
|
|
963
|
-
</span>
|
|
964
|
-
) : isRemote ? (
|
|
965
|
-
<i className="iconoir-globe text-[14px] opacity-50 shrink-0" aria-hidden="true" />
|
|
966
|
-
) : (
|
|
967
|
-
<i className="iconoir-git-branch text-[14px] opacity-50 shrink-0" aria-hidden="true" />
|
|
968
|
-
)}
|
|
969
|
-
<span className="truncate min-w-0 flex-1" title={child.fullPath}>{child.name}</span>
|
|
970
|
-
{hasDivergence && (
|
|
971
|
-
<span
|
|
972
|
-
className="flex items-center gap-1 text-xs opacity-70 shrink-0"
|
|
973
|
-
title={`${branchTracking.ahead} ahead, ${branchTracking.behind} behind ${branchTracking.upstream}`}
|
|
974
|
-
>
|
|
975
|
-
{branchTracking.ahead > 0 && (
|
|
976
|
-
<span className="flex items-center gap-0.5 text-xs">
|
|
977
|
-
<i className="iconoir-arrow-up text-[12px]" aria-hidden="true" />
|
|
978
|
-
<span>{branchTracking.ahead}</span>
|
|
979
|
-
</span>
|
|
980
|
-
)}
|
|
981
|
-
{branchTracking.behind > 0 && (
|
|
982
|
-
<span className="flex items-center gap-0.5 text-xs">
|
|
983
|
-
<i className="iconoir-arrow-down text-[12px]" aria-hidden="true" />
|
|
984
|
-
<span>{branchTracking.behind}</span>
|
|
985
|
-
</span>
|
|
986
|
-
)}
|
|
987
|
-
</span>
|
|
988
|
-
)}
|
|
989
|
-
</div>
|
|
990
|
-
<div className="flex items-center gap-0.5 ml-auto">
|
|
991
|
-
<VisibilityToggle
|
|
992
|
-
type="visible"
|
|
993
|
-
isActive={directVisibility === 'visible' || (isInherited && effectiveVisibility === 'visible')}
|
|
994
|
-
isInherited={isInherited && effectiveVisibility === 'visible'}
|
|
995
|
-
onClick={(e) => { e.stopPropagation(); onToggleVisibility(itemPath, 'visible'); }}
|
|
996
|
-
showOnHover={directVisibility === 'visible' || (isInherited && effectiveVisibility === 'visible')}
|
|
997
|
-
/>
|
|
998
|
-
<VisibilityToggle
|
|
999
|
-
type="hidden"
|
|
1000
|
-
isActive={directVisibility === 'hidden' || (isInherited && effectiveVisibility === 'hidden')}
|
|
1001
|
-
isInherited={isInherited && effectiveVisibility === 'hidden'}
|
|
1002
|
-
onClick={(e) => { e.stopPropagation(); onToggleVisibility(itemPath, 'hidden'); }}
|
|
1003
|
-
showOnHover={directVisibility === 'hidden' || (isInherited && effectiveVisibility === 'hidden')}
|
|
1004
|
-
/>
|
|
1005
|
-
</div>
|
|
1006
|
-
</div>
|
|
1007
|
-
</ContextMenu>
|
|
1008
|
-
);
|
|
1009
|
-
})}
|
|
1010
|
-
</>
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
export function HistoryView({ repoPath }: { repoPath: string }) {
|
|
1015
|
-
const { data: settings } = useSettings();
|
|
1016
|
-
const updateSettings = useUpdateSettings();
|
|
1017
|
-
|
|
1018
|
-
const [limit, setLimit] = useState(100);
|
|
1019
|
-
const { data: log, isLoading, isError, error, refetch, isFetching } = useGitLog(repoPath, limit);
|
|
1020
|
-
const { data: branchData, isLoading: isBranchesLoading } = useGitBranches(repoPath);
|
|
1021
|
-
const { data: statusData } = useGitStatus(repoPath);
|
|
1022
|
-
const [selectedHash, setSelectedHash] = useState<string | null>(null);
|
|
1023
|
-
const [selectedCommitHashes, setSelectedCommitHashes] = useState<string[]>([]);
|
|
1024
|
-
const [selectionAnchorHash, setSelectionAnchorHash] = useState<string | null>(null);
|
|
1025
|
-
|
|
1026
|
-
const selectSingleCommit = useCallback((hash: string | null) => {
|
|
1027
|
-
setSelectedHash(hash);
|
|
1028
|
-
setSelectedCommitHashes(hash ? [hash] : []);
|
|
1029
|
-
setSelectionAnchorHash(hash);
|
|
1030
|
-
}, []);
|
|
1031
|
-
|
|
1032
|
-
// Clear selected commit and close commit details panel when repository changes
|
|
1033
|
-
useEffect(() => {
|
|
1034
|
-
selectSingleCommit(null);
|
|
1035
|
-
setSelectedBranchRefs([]);
|
|
1036
|
-
setBranchSelectionAnchor(null);
|
|
1037
|
-
}, [repoPath, selectSingleCommit]);
|
|
1038
|
-
|
|
1039
|
-
const { mutateAsync: runGitAction } = useGitAction();
|
|
1040
|
-
const [iscreateBranchOpen, setIsCreateBranchOpen] = useState(false);
|
|
1041
|
-
const [newBranchName, setNewBranchName] = useState('');
|
|
1042
|
-
const [createBranchFromRef, setCreateBranchFromRef] = useState<string | null>(null);
|
|
1043
|
-
const [isCreating, setIsCreating] = useState(false);
|
|
1044
|
-
const [isCreateTagOpen, setIsCreateTagOpen] = useState(false);
|
|
1045
|
-
const [createTagCommitHash, setCreateTagCommitHash] = useState<string | null>(null);
|
|
1046
|
-
const [newTagName, setNewTagName] = useState('');
|
|
1047
|
-
const [pushTagToRemote, setPushTagToRemote] = useState(false);
|
|
1048
|
-
const [isCreatingTag, setIsCreatingTag] = useState(false);
|
|
1049
|
-
|
|
1050
|
-
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
|
1051
|
-
const [branchesToDelete, setBranchesToDelete] = useState<string[]>([]);
|
|
1052
|
-
const [deleteRemoteBranch, setDeleteRemoteBranch] = useState(false);
|
|
1053
|
-
const [isDeleting, setIsDeleting] = useState(false);
|
|
1054
|
-
const [isDeleteTagOpen, setIsDeleteTagOpen] = useState(false);
|
|
1055
|
-
const [tagToDelete, setTagToDelete] = useState<string | null>(null);
|
|
1056
|
-
const [deleteRemoteTag, setDeleteRemoteTag] = useState(false);
|
|
1057
|
-
const [isDeletingTag, setIsDeletingTag] = useState(false);
|
|
1058
|
-
|
|
1059
|
-
const [isCherryPickOpen, setIsCherryPickOpen] = useState(false);
|
|
1060
|
-
const [commitsToCherryPick, setCommitsToCherryPick] = useState<{ hash: string; message: string }[]>([]);
|
|
1061
|
-
const [isCherryPicking, setIsCherryPicking] = useState(false);
|
|
1062
|
-
const [isAbortCherryPickOpen, setIsAbortCherryPickOpen] = useState(false);
|
|
1063
|
-
const [isAbortingCherryPick, setIsAbortingCherryPick] = useState(false);
|
|
1064
|
-
|
|
1065
|
-
const [isRenameOpen, setIsRenameOpen] = useState(false);
|
|
1066
|
-
const [branchToRename, setBranchToRename] = useState<string | null>(null);
|
|
1067
|
-
const [remoteBranchToRename, setRemoteBranchToRename] = useState<{ remote: string; branch: string } | null>(null);
|
|
1068
|
-
const [newBranchNameForRename, setNewBranchNameForRename] = useState('');
|
|
1069
|
-
const [renameTrackingRemoteBranch, setRenameTrackingRemoteBranch] = useState(false);
|
|
1070
|
-
const [isRenaming, setIsRenaming] = useState(false);
|
|
1071
|
-
const [isRenameRemoteOpen, setIsRenameRemoteOpen] = useState(false);
|
|
1072
|
-
const [remoteToRename, setRemoteToRename] = useState<string | null>(null);
|
|
1073
|
-
const [newRemoteNameForRename, setNewRemoteNameForRename] = useState('');
|
|
1074
|
-
const [isRenamingRemote, setIsRenamingRemote] = useState(false);
|
|
1075
|
-
const [isDeleteRemoteOpen, setIsDeleteRemoteOpen] = useState(false);
|
|
1076
|
-
const [remoteToDelete, setRemoteToDelete] = useState<string | null>(null);
|
|
1077
|
-
const [isDeletingRemote, setIsDeletingRemote] = useState(false);
|
|
1078
|
-
const [isAddRemoteOpen, setIsAddRemoteOpen] = useState(false);
|
|
1079
|
-
const [newRemoteName, setNewRemoteName] = useState('origin');
|
|
1080
|
-
const [newRemoteUrl, setNewRemoteUrl] = useState('');
|
|
1081
|
-
const [isAddingRemote, setIsAddingRemote] = useState(false);
|
|
1082
|
-
|
|
1083
|
-
const [isRebaseOpen, setIsRebaseOpen] = useState(false);
|
|
1084
|
-
const [rebaseSourceBranch, setRebaseSourceBranch] = useState<string | null>(null);
|
|
1085
|
-
const [rebaseTargetBranch, setRebaseTargetBranch] = useState<string | null>(null);
|
|
1086
|
-
const [rebaseStashChanges, setRebaseStashChanges] = useState(true);
|
|
1087
|
-
const [isRebasing, setIsRebasing] = useState(false);
|
|
1088
|
-
const [rebaseConflictStatus, setRebaseConflictStatus] = useState<MergeConflictStatus>('checking');
|
|
1089
|
-
|
|
1090
|
-
const closeRebaseDialog = useCallback(() => {
|
|
1091
|
-
setIsRebaseOpen(false);
|
|
1092
|
-
setRebaseSourceBranch(null);
|
|
1093
|
-
setRebaseTargetBranch(null);
|
|
1094
|
-
setRebaseConflictStatus('checking');
|
|
1095
|
-
}, []);
|
|
1096
|
-
|
|
1097
|
-
const [isMergeOpen, setIsMergeOpen] = useState(false);
|
|
1098
|
-
const [mergeTargetBranch, setMergeTargetBranch] = useState<string | null>(null);
|
|
1099
|
-
const [mergeSourceBranch, setMergeSourceBranch] = useState<string | null>(null);
|
|
1100
|
-
const [mergeRebaseBeforeMerge, setMergeRebaseBeforeMerge] = useState(false);
|
|
1101
|
-
const [mergeSquash, setMergeSquash] = useState(false);
|
|
1102
|
-
const [mergeFastForward, setMergeFastForward] = useState(false);
|
|
1103
|
-
const [mergeSquashMessage, setMergeSquashMessage] = useState('');
|
|
1104
|
-
const [isMerging, setIsMerging] = useState(false);
|
|
1105
|
-
const [mergeConflictStatus, setMergeConflictStatus] = useState<MergeConflictStatus>('checking');
|
|
1106
|
-
|
|
1107
|
-
const closeMergeDialog = useCallback(() => {
|
|
1108
|
-
setIsMergeOpen(false);
|
|
1109
|
-
setMergeTargetBranch(null);
|
|
1110
|
-
setMergeSourceBranch(null);
|
|
1111
|
-
setMergeConflictStatus('checking');
|
|
1112
|
-
}, []);
|
|
1113
|
-
|
|
1114
|
-
// Push to remote dialog state
|
|
1115
|
-
const [isPushOpen, setIsPushOpen] = useState(false);
|
|
1116
|
-
const [pushBranch, setPushBranch] = useState<string | null>(null);
|
|
1117
|
-
const [pushRemotes, setPushRemotes] = useState<string[]>([]);
|
|
1118
|
-
const [pushSelectedRemote, setPushSelectedRemote] = useState<string>('');
|
|
1119
|
-
const [pushRemoteBranches, setPushRemoteBranches] = useState<string[]>([]);
|
|
1120
|
-
const [pushSelectedRemoteBranch, setPushSelectedRemoteBranch] = useState<string>('');
|
|
1121
|
-
const [pushTrackingBranch, setPushTrackingBranch] = useState<{ remote: string; branch: string } | null>(null);
|
|
1122
|
-
const [pushRebaseFirst, setPushRebaseFirst] = useState(false);
|
|
1123
|
-
const [pushForcePush, setPushForcePush] = useState(false);
|
|
1124
|
-
const [pushLocalOnlyTags, setPushLocalOnlyTags] = useState(true);
|
|
1125
|
-
const [pushSquash, setPushSquash] = useState(false);
|
|
1126
|
-
const [pushSquashMessage, setPushSquashMessage] = useState('');
|
|
1127
|
-
const [isPushing, setIsPushing] = useState(false);
|
|
1128
|
-
const [pushError, setPushError] = useState<string | null>(null);
|
|
1129
|
-
const [pushLoadingRemotes, setPushLoadingRemotes] = useState(false);
|
|
1130
|
-
const [pushLoadingBranches, setPushLoadingBranches] = useState(false);
|
|
1131
|
-
|
|
1132
|
-
// Pull from remote dialog state
|
|
1133
|
-
const [isPullOpen, setIsPullOpen] = useState(false);
|
|
1134
|
-
const [pullBranch, setPullBranch] = useState<string | null>(null);
|
|
1135
|
-
const [pullRemotes, setPullRemotes] = useState<string[]>([]);
|
|
1136
|
-
const [pullSelectedRemote, setPullSelectedRemote] = useState<string>('');
|
|
1137
|
-
const [pullRemoteBranches, setPullRemoteBranches] = useState<string[]>([]);
|
|
1138
|
-
const [pullSelectedRemoteBranch, setPullSelectedRemoteBranch] = useState<string>('');
|
|
1139
|
-
const [pullTrackingBranch, setPullTrackingBranch] = useState<{ remote: string; branch: string } | null>(null);
|
|
1140
|
-
const [pullRebase, setPullRebase] = useState(true);
|
|
1141
|
-
const [isPulling, setIsPulling] = useState(false);
|
|
1142
|
-
const [pullError, setPullError] = useState<string | null>(null);
|
|
1143
|
-
const [pullLoadingRemotes, setPullLoadingRemotes] = useState(false);
|
|
1144
|
-
const [pullLoadingBranches, setPullLoadingBranches] = useState(false);
|
|
1145
|
-
const [isFetchingAllRemotes, setIsFetchingAllRemotes] = useState(false);
|
|
1146
|
-
|
|
1147
|
-
// Checkout to local dialog state
|
|
1148
|
-
const [isCheckoutToLocalOpen, setIsCheckoutToLocalOpen] = useState(false);
|
|
1149
|
-
const [checkoutRemoteBranch, setCheckoutRemoteBranch] = useState<string | null>(null);
|
|
1150
|
-
const [checkoutLocalBranchName, setCheckoutLocalBranchName] = useState('');
|
|
1151
|
-
const [isCheckingOutToLocal, setIsCheckingOutToLocal] = useState(false);
|
|
1152
|
-
|
|
1153
|
-
// Reset to commit dialog state
|
|
1154
|
-
const [isResetOpen, setIsResetOpen] = useState(false);
|
|
1155
|
-
const [resetCommitHash, setResetCommitHash] = useState<string | null>(null);
|
|
1156
|
-
const [isResetting, setIsResetting] = useState(false);
|
|
1157
|
-
const [isRevertOpen, setIsRevertOpen] = useState(false);
|
|
1158
|
-
const [commitToRevert, setCommitToRevert] = useState<{ hash: string; message: string } | null>(null);
|
|
1159
|
-
const [isReverting, setIsReverting] = useState(false);
|
|
1160
|
-
|
|
1161
|
-
const [isRewordOpen, setIsRewordOpen] = useState(false);
|
|
1162
|
-
const [commitToReword, setCommitToReword] = useState<{ hash: string; subject: string; body: string; branch: string } | null>(null);
|
|
1163
|
-
const [newMessageSubject, setNewMessageSubject] = useState('');
|
|
1164
|
-
const [newMessageBody, setNewMessageBody] = useState('');
|
|
1165
|
-
const [isRewording, setIsRewording] = useState(false);
|
|
1166
|
-
|
|
1167
|
-
// Ref for GitGraph to scroll to commits
|
|
1168
|
-
const gitGraphRef = useRef<GitGraphHandle>(null);
|
|
1169
|
-
|
|
1170
|
-
// State for pending scroll to branch commit
|
|
1171
|
-
const [pendingScrollCommit, setPendingScrollCommit] = useState<string | null>(null);
|
|
1172
|
-
const [selectedBranchRefs, setSelectedBranchRefs] = useState<string[]>([]);
|
|
1173
|
-
const [branchSelectionAnchor, setBranchSelectionAnchor] = useState<string | null>(null);
|
|
1174
|
-
const [isBranchPopoverOpen, setIsBranchPopoverOpen] = useState(false);
|
|
1175
|
-
const branchPopoverRef = useRef<HTMLDivElement>(null);
|
|
1176
|
-
const [scriptExecution, setScriptExecution] = useState<ScriptExecutionState>(DEFAULT_SCRIPT_EXECUTION);
|
|
1177
|
-
const [isCancelingScriptExecution, setIsCancelingScriptExecution] = useState(false);
|
|
1178
|
-
const [isCopyingScriptOutput, setIsCopyingScriptOutput] = useState(false);
|
|
1179
|
-
const [didCopyScriptOutput, setDidCopyScriptOutput] = useState(false);
|
|
1180
|
-
const isScriptExecutionRunning = scriptExecution.status === 'starting' || scriptExecution.status === 'running';
|
|
1181
|
-
const isScriptExecutionFinished = scriptExecution.status === 'completed' || scriptExecution.status === 'failed' || scriptExecution.status === 'canceled';
|
|
1182
|
-
|
|
1183
|
-
const closeRenameBranchDialog = useCallback(() => {
|
|
1184
|
-
setIsRenameOpen(false);
|
|
1185
|
-
setBranchToRename(null);
|
|
1186
|
-
setRemoteBranchToRename(null);
|
|
1187
|
-
setNewBranchNameForRename('');
|
|
1188
|
-
setRenameTrackingRemoteBranch(false);
|
|
1189
|
-
}, []);
|
|
1190
|
-
|
|
1191
|
-
const closeRenameRemoteDialog = useCallback(() => {
|
|
1192
|
-
setIsRenameRemoteOpen(false);
|
|
1193
|
-
setRemoteToRename(null);
|
|
1194
|
-
setNewRemoteNameForRename('');
|
|
1195
|
-
}, []);
|
|
1196
|
-
|
|
1197
|
-
const closeDeleteRemoteDialog = useCallback(() => {
|
|
1198
|
-
setIsDeleteRemoteOpen(false);
|
|
1199
|
-
setRemoteToDelete(null);
|
|
1200
|
-
}, []);
|
|
1201
|
-
|
|
1202
|
-
const closeAddRemoteDialog = useCallback(() => {
|
|
1203
|
-
setIsAddRemoteOpen(false);
|
|
1204
|
-
setNewRemoteName('origin');
|
|
1205
|
-
setNewRemoteUrl('');
|
|
1206
|
-
}, []);
|
|
1207
|
-
|
|
1208
|
-
const closeRewordDialog = useCallback(() => {
|
|
1209
|
-
setIsRewordOpen(false);
|
|
1210
|
-
setCommitToReword(null);
|
|
1211
|
-
setNewMessageSubject('');
|
|
1212
|
-
setNewMessageBody('');
|
|
1213
|
-
}, []);
|
|
1214
|
-
|
|
1215
|
-
const closeTopPopup = useCallback(() => {
|
|
1216
|
-
if (isAbortCherryPickOpen) {
|
|
1217
|
-
setIsAbortCherryPickOpen(false);
|
|
1218
|
-
setCommitsToCherryPick([]);
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (isCheckoutToLocalOpen) {
|
|
1222
|
-
setIsCheckoutToLocalOpen(false);
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
if (iscreateBranchOpen) {
|
|
1226
|
-
setIsCreateBranchOpen(false);
|
|
1227
|
-
setCreateBranchFromRef(null);
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
if (isCreateTagOpen) {
|
|
1231
|
-
setIsCreateTagOpen(false);
|
|
1232
|
-
setCreateTagCommitHash(null);
|
|
1233
|
-
setNewTagName('');
|
|
1234
|
-
setPushTagToRemote(false);
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
if (isPullOpen) {
|
|
1238
|
-
setIsPullOpen(false);
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
if (isPushOpen) {
|
|
1242
|
-
setIsPushOpen(false);
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
if (isMergeOpen) {
|
|
1246
|
-
closeMergeDialog();
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
if (isRebaseOpen) {
|
|
1250
|
-
closeRebaseDialog();
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
if (isRenameOpen) {
|
|
1254
|
-
closeRenameBranchDialog();
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
if (isRenameRemoteOpen) {
|
|
1258
|
-
closeRenameRemoteDialog();
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
if (isDeleteRemoteOpen) {
|
|
1262
|
-
closeDeleteRemoteDialog();
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
if (isAddRemoteOpen) {
|
|
1266
|
-
closeAddRemoteDialog();
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
if (isCherryPickOpen) {
|
|
1270
|
-
setIsCherryPickOpen(false);
|
|
1271
|
-
setCommitsToCherryPick([]);
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
if (isDeleteOpen) {
|
|
1275
|
-
setIsDeleteOpen(false);
|
|
1276
|
-
setBranchesToDelete([]);
|
|
1277
|
-
setDeleteRemoteBranch(false);
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
if (isDeleteTagOpen) {
|
|
1281
|
-
setIsDeleteTagOpen(false);
|
|
1282
|
-
setTagToDelete(null);
|
|
1283
|
-
setDeleteRemoteTag(false);
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
if (isRewordOpen) {
|
|
1287
|
-
closeRewordDialog();
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
if (isResetOpen) {
|
|
1291
|
-
setIsResetOpen(false);
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
if (isRevertOpen) {
|
|
1295
|
-
setIsRevertOpen(false);
|
|
1296
|
-
setCommitToRevert(null);
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
if (isBranchPopoverOpen) {
|
|
1300
|
-
setIsBranchPopoverOpen(false);
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
if (scriptExecution.isOpen && isScriptExecutionFinished) {
|
|
1304
|
-
setScriptExecution(DEFAULT_SCRIPT_EXECUTION);
|
|
1305
|
-
setDidCopyScriptOutput(false);
|
|
1306
|
-
setIsCancelingScriptExecution(false);
|
|
1307
|
-
}
|
|
1308
|
-
}, [
|
|
1309
|
-
isAbortCherryPickOpen,
|
|
1310
|
-
isCheckoutToLocalOpen,
|
|
1311
|
-
iscreateBranchOpen,
|
|
1312
|
-
isCreateTagOpen,
|
|
1313
|
-
isPullOpen,
|
|
1314
|
-
isPushOpen,
|
|
1315
|
-
closeMergeDialog,
|
|
1316
|
-
isMergeOpen,
|
|
1317
|
-
closeRebaseDialog,
|
|
1318
|
-
isRebaseOpen,
|
|
1319
|
-
isRenameOpen,
|
|
1320
|
-
closeRenameBranchDialog,
|
|
1321
|
-
isRenameRemoteOpen,
|
|
1322
|
-
closeRenameRemoteDialog,
|
|
1323
|
-
isDeleteRemoteOpen,
|
|
1324
|
-
closeDeleteRemoteDialog,
|
|
1325
|
-
isAddRemoteOpen,
|
|
1326
|
-
closeAddRemoteDialog,
|
|
1327
|
-
isCherryPickOpen,
|
|
1328
|
-
isDeleteOpen,
|
|
1329
|
-
isDeleteTagOpen,
|
|
1330
|
-
isRewordOpen,
|
|
1331
|
-
closeRewordDialog,
|
|
1332
|
-
isResetOpen,
|
|
1333
|
-
isRevertOpen,
|
|
1334
|
-
isBranchPopoverOpen,
|
|
1335
|
-
scriptExecution.isOpen,
|
|
1336
|
-
isScriptExecutionFinished,
|
|
1337
|
-
]);
|
|
1338
|
-
|
|
1339
|
-
const isAnyPopupOpen =
|
|
1340
|
-
isResetOpen ||
|
|
1341
|
-
isRevertOpen ||
|
|
1342
|
-
isRewordOpen ||
|
|
1343
|
-
isDeleteOpen ||
|
|
1344
|
-
isDeleteTagOpen ||
|
|
1345
|
-
isAbortCherryPickOpen ||
|
|
1346
|
-
isCherryPickOpen ||
|
|
1347
|
-
isRenameOpen ||
|
|
1348
|
-
isRenameRemoteOpen ||
|
|
1349
|
-
isDeleteRemoteOpen ||
|
|
1350
|
-
isAddRemoteOpen ||
|
|
1351
|
-
isRebaseOpen ||
|
|
1352
|
-
isMergeOpen ||
|
|
1353
|
-
isPushOpen ||
|
|
1354
|
-
isPullOpen ||
|
|
1355
|
-
iscreateBranchOpen ||
|
|
1356
|
-
isCreateTagOpen ||
|
|
1357
|
-
isCheckoutToLocalOpen ||
|
|
1358
|
-
isBranchPopoverOpen ||
|
|
1359
|
-
scriptExecution.isOpen;
|
|
1360
|
-
|
|
1361
|
-
useEscapeDismiss(isAnyPopupOpen, closeTopPopup);
|
|
1362
|
-
|
|
1363
|
-
// Resizable bottom panel state - load from global settings or fallback to localStorage
|
|
1364
|
-
const panelHeightStorageKey = 'git-web:history-panel-height';
|
|
1365
|
-
const commitDetailsMessageRatioStorageKey = 'git-web:history-commit-details-message-ratio';
|
|
1366
|
-
const [panelHeight, setPanelHeight] = useState(300);
|
|
1367
|
-
const [isResizing, setIsResizing] = useState(false);
|
|
1368
|
-
const resizeRef = useRef<{ startY: number; startHeight: number } | null>(null);
|
|
1369
|
-
const [commitDetailsMessageRatio, setCommitDetailsMessageRatio] = useState(DEFAULT_COMMIT_DETAILS_MESSAGE_RATIO);
|
|
1370
|
-
const [isCommitDetailsRatioResizing, setIsCommitDetailsRatioResizing] = useState(false);
|
|
1371
|
-
const commitDetailsContentRef = useRef<HTMLDivElement | null>(null);
|
|
1372
|
-
|
|
1373
|
-
// Track if user has manually resized the panel to avoid sync loops
|
|
1374
|
-
const userHasResized = useRef(false);
|
|
1375
|
-
|
|
1376
|
-
// Load panel height from settings or localStorage
|
|
1377
|
-
useEffect(() => {
|
|
1378
|
-
if (settings?.historyPanelHeight) {
|
|
1379
|
-
setPanelHeight(clampHistoryPanelHeight(settings.historyPanelHeight));
|
|
1380
|
-
} else {
|
|
1381
|
-
try {
|
|
1382
|
-
const stored = localStorage.getItem(panelHeightStorageKey);
|
|
1383
|
-
if (stored) {
|
|
1384
|
-
const parsed = parseInt(stored, 10);
|
|
1385
|
-
if (!isNaN(parsed) && parsed >= MIN_HISTORY_PANEL_HEIGHT && parsed <= MAX_HISTORY_PANEL_HEIGHT) {
|
|
1386
|
-
setPanelHeight(parsed);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
} catch (e) {
|
|
1390
|
-
console.error('Failed to load panel height from localStorage:', e);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}, [settings?.historyPanelHeight]);
|
|
1394
|
-
|
|
1395
|
-
// Save panel height to localStorage for immediate persistence
|
|
1396
|
-
useEffect(() => {
|
|
1397
|
-
try {
|
|
1398
|
-
localStorage.setItem(panelHeightStorageKey, String(panelHeight));
|
|
1399
|
-
} catch (e) {
|
|
1400
|
-
console.error('Failed to save panel height to localStorage:', e);
|
|
1401
|
-
}
|
|
1402
|
-
}, [panelHeight]);
|
|
1403
|
-
|
|
1404
|
-
// Load commit details message/diff ratio from localStorage
|
|
1405
|
-
useEffect(() => {
|
|
1406
|
-
try {
|
|
1407
|
-
const stored = localStorage.getItem(commitDetailsMessageRatioStorageKey);
|
|
1408
|
-
if (!stored) return;
|
|
1409
|
-
const parsed = parseFloat(stored);
|
|
1410
|
-
if (!Number.isNaN(parsed)) {
|
|
1411
|
-
setCommitDetailsMessageRatio(clampCommitDetailsMessageRatio(parsed));
|
|
1412
|
-
}
|
|
1413
|
-
} catch (e) {
|
|
1414
|
-
console.error('Failed to load commit details ratio from localStorage:', e);
|
|
1415
|
-
}
|
|
1416
|
-
}, []);
|
|
1417
|
-
|
|
1418
|
-
// Persist commit details message/diff ratio in localStorage
|
|
1419
|
-
useEffect(() => {
|
|
1420
|
-
try {
|
|
1421
|
-
localStorage.setItem(commitDetailsMessageRatioStorageKey, String(commitDetailsMessageRatio));
|
|
1422
|
-
} catch (e) {
|
|
1423
|
-
console.error('Failed to save commit details ratio to localStorage:', e);
|
|
1424
|
-
}
|
|
1425
|
-
}, [commitDetailsMessageRatio]);
|
|
1426
|
-
|
|
1427
|
-
// Sync to global settings when resizing stops
|
|
1428
|
-
useEffect(() => {
|
|
1429
|
-
if (!isResizing && userHasResized.current) {
|
|
1430
|
-
updateSettings.mutate({ historyPanelHeight: panelHeight });
|
|
1431
|
-
userHasResized.current = false;
|
|
1432
|
-
}
|
|
1433
|
-
}, [isResizing, panelHeight, updateSettings]);
|
|
1434
|
-
|
|
1435
|
-
// Handle resize drag
|
|
1436
|
-
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
|
1437
|
-
e.preventDefault();
|
|
1438
|
-
setIsResizing(true);
|
|
1439
|
-
resizeRef.current = { startY: e.clientY, startHeight: panelHeight };
|
|
1440
|
-
}, [panelHeight]);
|
|
1441
|
-
|
|
1442
|
-
useEffect(() => {
|
|
1443
|
-
if (!isResizing) return;
|
|
1444
|
-
|
|
1445
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
1446
|
-
if (!resizeRef.current) return;
|
|
1447
|
-
const delta = resizeRef.current.startY - e.clientY;
|
|
1448
|
-
const newHeight = clampHistoryPanelHeight(resizeRef.current.startHeight + delta);
|
|
1449
|
-
setPanelHeight(newHeight);
|
|
1450
|
-
userHasResized.current = true;
|
|
1451
|
-
};
|
|
1452
|
-
|
|
1453
|
-
const handleMouseUp = () => {
|
|
1454
|
-
setIsResizing(false);
|
|
1455
|
-
resizeRef.current = null;
|
|
1456
|
-
};
|
|
1457
|
-
|
|
1458
|
-
document.addEventListener('mousemove', handleMouseMove);
|
|
1459
|
-
document.addEventListener('mouseup', handleMouseUp);
|
|
1460
|
-
|
|
1461
|
-
return () => {
|
|
1462
|
-
document.removeEventListener('mousemove', handleMouseMove);
|
|
1463
|
-
document.removeEventListener('mouseup', handleMouseUp);
|
|
1464
|
-
};
|
|
1465
|
-
}, [isResizing]);
|
|
1466
|
-
|
|
1467
|
-
const handleCommitDetailsRatioResizeStart = useCallback((e: React.MouseEvent) => {
|
|
1468
|
-
e.preventDefault();
|
|
1469
|
-
setIsCommitDetailsRatioResizing(true);
|
|
1470
|
-
}, []);
|
|
1471
|
-
|
|
1472
|
-
useEffect(() => {
|
|
1473
|
-
if (!isCommitDetailsRatioResizing) return;
|
|
1474
|
-
|
|
1475
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
1476
|
-
const container = commitDetailsContentRef.current;
|
|
1477
|
-
if (!container) return;
|
|
1478
|
-
const rect = container.getBoundingClientRect();
|
|
1479
|
-
if (rect.height <= 0) return;
|
|
1480
|
-
|
|
1481
|
-
const nextRatio = clampCommitDetailsMessageRatio((e.clientY - rect.top) / rect.height);
|
|
1482
|
-
setCommitDetailsMessageRatio(nextRatio);
|
|
1483
|
-
};
|
|
1484
|
-
|
|
1485
|
-
const handleMouseUp = () => {
|
|
1486
|
-
setIsCommitDetailsRatioResizing(false);
|
|
1487
|
-
};
|
|
1488
|
-
|
|
1489
|
-
document.addEventListener('mousemove', handleMouseMove);
|
|
1490
|
-
document.addEventListener('mouseup', handleMouseUp);
|
|
1491
|
-
document.body.style.cursor = 'ns-resize';
|
|
1492
|
-
document.body.style.userSelect = 'none';
|
|
1493
|
-
|
|
1494
|
-
return () => {
|
|
1495
|
-
document.removeEventListener('mousemove', handleMouseMove);
|
|
1496
|
-
document.removeEventListener('mouseup', handleMouseUp);
|
|
1497
|
-
document.body.style.cursor = '';
|
|
1498
|
-
document.body.style.userSelect = '';
|
|
1499
|
-
};
|
|
1500
|
-
}, [isCommitDetailsRatioResizing]);
|
|
1501
|
-
|
|
1502
|
-
useEffect(() => {
|
|
1503
|
-
if (!isBranchPopoverOpen) return;
|
|
1504
|
-
|
|
1505
|
-
const handleMouseDown = (event: MouseEvent) => {
|
|
1506
|
-
if (branchPopoverRef.current && !branchPopoverRef.current.contains(event.target as Node)) {
|
|
1507
|
-
setIsBranchPopoverOpen(false);
|
|
1508
|
-
}
|
|
1509
|
-
};
|
|
1510
|
-
|
|
1511
|
-
document.addEventListener('mousedown', handleMouseDown);
|
|
1512
|
-
|
|
1513
|
-
return () => {
|
|
1514
|
-
document.removeEventListener('mousedown', handleMouseDown);
|
|
1515
|
-
};
|
|
1516
|
-
}, [isBranchPopoverOpen]);
|
|
1517
|
-
|
|
1518
|
-
// Build branch trees for local and remote branches
|
|
1519
|
-
const localBranchTree = useMemo(() => {
|
|
1520
|
-
if (!branchData?.branches) return null;
|
|
1521
|
-
return buildBranchTree(branchData.branches);
|
|
1522
|
-
}, [branchData?.branches]);
|
|
1523
|
-
|
|
1524
|
-
const remoteBranchTrees = useMemo(() => {
|
|
1525
|
-
if (!branchData?.remotes) return null;
|
|
1526
|
-
return buildRemoteBranchTree(branchData.remotes);
|
|
1527
|
-
}, [branchData?.remotes]);
|
|
1528
|
-
|
|
1529
|
-
const allBranchRefs = useMemo(() => {
|
|
1530
|
-
const refs = new Set<string>();
|
|
1531
|
-
for (const branch of branchData?.branches ?? []) {
|
|
1532
|
-
refs.add(branch);
|
|
1533
|
-
}
|
|
1534
|
-
for (const [remoteName, branches] of Object.entries(branchData?.remotes ?? {})) {
|
|
1535
|
-
for (const branch of branches) {
|
|
1536
|
-
refs.add(`remotes/${remoteName}/${branch}`);
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
return refs;
|
|
1540
|
-
}, [branchData?.branches, branchData?.remotes]);
|
|
1541
|
-
|
|
1542
|
-
const selectedBranchSet = useMemo(() => new Set(selectedBranchRefs), [selectedBranchRefs]);
|
|
1543
|
-
|
|
1544
|
-
const repository = useRepository(repoPath);
|
|
1545
|
-
const updateRepository = useUpdateRepository();
|
|
1546
|
-
const customBranchScripts = useMemo(() => {
|
|
1547
|
-
const scripts = repository?.customScripts ?? [];
|
|
1548
|
-
return scripts.filter((script) => (
|
|
1549
|
-
script.target === 'branch' &&
|
|
1550
|
-
script.action === 'run-bash-script' &&
|
|
1551
|
-
script.name.trim().length > 0 &&
|
|
1552
|
-
script.content.trim().length > 0
|
|
1553
|
-
));
|
|
1554
|
-
}, [repository?.customScripts]);
|
|
1555
|
-
|
|
1556
|
-
// Group expanded state (for "Branches" and "Remotes" group headers)
|
|
1557
|
-
const [localGroupExpanded, setLocalGroupExpanded] = useState(true);
|
|
1558
|
-
const [remotesGroupExpanded, setRemotesGroupExpanded] = useState(true);
|
|
1559
|
-
|
|
1560
|
-
// Visibility state for branches/folders
|
|
1561
|
-
const [visibilityMap, setVisibilityMap] = useState<VisibilityMap>({});
|
|
1562
|
-
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
|
1563
|
-
|
|
1564
|
-
const orderedVisibleBranchRefs = useMemo(() => {
|
|
1565
|
-
const ordered: string[] = [];
|
|
1566
|
-
if (localGroupExpanded && localBranchTree) {
|
|
1567
|
-
ordered.push(...collectVisibleBranchRefs(localBranchTree, expandedFolders));
|
|
1568
|
-
}
|
|
1569
|
-
if (remotesGroupExpanded && remoteBranchTrees) {
|
|
1570
|
-
for (const [remoteName, tree] of Array.from(remoteBranchTrees.entries())) {
|
|
1571
|
-
const remoteGroupPath = `__remotes__/${remoteName}`;
|
|
1572
|
-
if (!expandedFolders.has(remoteGroupPath)) continue;
|
|
1573
|
-
ordered.push(...collectVisibleBranchRefs(tree, expandedFolders, `remotes/${remoteName}`));
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
return ordered;
|
|
1577
|
-
}, [expandedFolders, localBranchTree, localGroupExpanded, remoteBranchTrees, remotesGroupExpanded]);
|
|
1578
|
-
|
|
1579
|
-
useEffect(() => {
|
|
1580
|
-
if (selectedBranchRefs.length === 0) return;
|
|
1581
|
-
const nextSelected = selectedBranchRefs.filter((branch) => allBranchRefs.has(branch));
|
|
1582
|
-
if (nextSelected.length !== selectedBranchRefs.length) {
|
|
1583
|
-
setSelectedBranchRefs(nextSelected);
|
|
1584
|
-
}
|
|
1585
|
-
if (branchSelectionAnchor && !allBranchRefs.has(branchSelectionAnchor)) {
|
|
1586
|
-
setBranchSelectionAnchor(nextSelected.length > 0 ? nextSelected[nextSelected.length - 1] : null);
|
|
1587
|
-
}
|
|
1588
|
-
}, [allBranchRefs, branchSelectionAnchor, selectedBranchRefs]);
|
|
1589
|
-
|
|
1590
|
-
// Load settings from repository data when it's available
|
|
1591
|
-
const lastInitializedRepo = useRef<string | null>(null);
|
|
1592
|
-
useEffect(() => {
|
|
1593
|
-
if (repository && repository.path !== lastInitializedRepo.current) {
|
|
1594
|
-
lastInitializedRepo.current = repository.path;
|
|
1595
|
-
if (repository.localGroupExpanded !== undefined) setLocalGroupExpanded(repository.localGroupExpanded);
|
|
1596
|
-
if (repository.remotesGroupExpanded !== undefined) setRemotesGroupExpanded(repository.remotesGroupExpanded);
|
|
1597
|
-
if (repository.expandedFolders) setExpandedFolders(new Set(repository.expandedFolders));
|
|
1598
|
-
if (repository.visibilityMap) setVisibilityMap(repository.visibilityMap as VisibilityMap);
|
|
1599
|
-
}
|
|
1600
|
-
}, [repository]);
|
|
1601
|
-
|
|
1602
|
-
// Helper to save settings to the backend
|
|
1603
|
-
const saveSettings = useCallback((updates: Partial<Repository>) => {
|
|
1604
|
-
updateRepository.mutate({
|
|
1605
|
-
path: repoPath,
|
|
1606
|
-
updates
|
|
1607
|
-
});
|
|
1608
|
-
}, [repoPath, updateRepository]);
|
|
1609
|
-
|
|
1610
|
-
const handleToggleLocalGroup = useCallback(() => {
|
|
1611
|
-
const newValue = !localGroupExpanded;
|
|
1612
|
-
setLocalGroupExpanded(newValue);
|
|
1613
|
-
saveSettings({ localGroupExpanded: newValue });
|
|
1614
|
-
}, [localGroupExpanded, saveSettings]);
|
|
1615
|
-
|
|
1616
|
-
const handleToggleRemotesGroup = useCallback(() => {
|
|
1617
|
-
const newValue = !remotesGroupExpanded;
|
|
1618
|
-
setRemotesGroupExpanded(newValue);
|
|
1619
|
-
saveSettings({ remotesGroupExpanded: newValue });
|
|
1620
|
-
}, [remotesGroupExpanded, saveSettings]);
|
|
1621
|
-
|
|
1622
|
-
// Toggle folder expansion
|
|
1623
|
-
const toggleFolder = useCallback((path: string) => {
|
|
1624
|
-
setExpandedFolders(prev => {
|
|
1625
|
-
const next = new Set(prev);
|
|
1626
|
-
if (next.has(path)) {
|
|
1627
|
-
next.delete(path);
|
|
1628
|
-
} else {
|
|
1629
|
-
next.add(path);
|
|
1630
|
-
}
|
|
1631
|
-
saveSettings({ expandedFolders: Array.from(next) });
|
|
1632
|
-
return next;
|
|
1633
|
-
});
|
|
1634
|
-
}, [saveSettings]);
|
|
1635
|
-
|
|
1636
|
-
// Toggle visibility for a path
|
|
1637
|
-
const handleToggleVisibility = useCallback((path: string, type: 'visible' | 'hidden') => {
|
|
1638
|
-
setVisibilityMap(prev => {
|
|
1639
|
-
const next = { ...prev };
|
|
1640
|
-
// If currently set to this type, remove it (toggle off)
|
|
1641
|
-
if (next[path] === type) {
|
|
1642
|
-
delete next[path];
|
|
1643
|
-
} else {
|
|
1644
|
-
// Set to this type (auto-removes the other one since we're replacing)
|
|
1645
|
-
next[path] = type;
|
|
1646
|
-
}
|
|
1647
|
-
const persistedVisibilityMap: Record<string, 'visible' | 'hidden'> = {};
|
|
1648
|
-
Object.entries(next).forEach(([key, value]) => {
|
|
1649
|
-
if (value === 'visible' || value === 'hidden') {
|
|
1650
|
-
persistedVisibilityMap[key] = value;
|
|
1651
|
-
}
|
|
1652
|
-
});
|
|
1653
|
-
saveSettings({ visibilityMap: persistedVisibilityMap });
|
|
1654
|
-
return next;
|
|
1655
|
-
});
|
|
1656
|
-
}, [saveSettings]);
|
|
1657
|
-
|
|
1658
|
-
// Clear all visibility filters
|
|
1659
|
-
const handleClearAllFilters = useCallback(() => {
|
|
1660
|
-
setVisibilityMap({});
|
|
1661
|
-
saveSettings({ visibilityMap: {} });
|
|
1662
|
-
}, [saveSettings]);
|
|
1663
|
-
|
|
1664
|
-
// Helper to get effective visibility for a branch considering group paths
|
|
1665
|
-
const getBranchEffectiveVisibility = useCallback((branch: string, isRemoteBranch: boolean) => {
|
|
1666
|
-
// First check the branch itself
|
|
1667
|
-
const directVis = getEffectiveVisibility(branch, visibilityMap);
|
|
1668
|
-
if (directVis) return directVis;
|
|
1669
|
-
|
|
1670
|
-
// Check group-level visibility
|
|
1671
|
-
if (isRemoteBranch) {
|
|
1672
|
-
// Remote branch format: remotes/origin/branch-name
|
|
1673
|
-
const parts = branch.split('/');
|
|
1674
|
-
if (parts.length >= 2 && parts[0] === 'remotes') {
|
|
1675
|
-
const remoteName = parts[1];
|
|
1676
|
-
// Check remote-specific group
|
|
1677
|
-
const remoteGroupVis = visibilityMap[`__remotes__/${remoteName}`];
|
|
1678
|
-
if (remoteGroupVis) return remoteGroupVis;
|
|
1679
|
-
// Check all remotes group
|
|
1680
|
-
const remotesVis = visibilityMap['__remotes__'];
|
|
1681
|
-
if (remotesVis) return remotesVis;
|
|
1682
|
-
}
|
|
1683
|
-
} else {
|
|
1684
|
-
// Local branch - check __local__ group
|
|
1685
|
-
const localVis = visibilityMap['__local__'];
|
|
1686
|
-
if (localVis) return localVis;
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
return null;
|
|
1690
|
-
}, [visibilityMap]);
|
|
1691
|
-
|
|
1692
|
-
// Compute which branches should be visible based on visibility map
|
|
1693
|
-
const filteredCommits = useMemo(() => {
|
|
1694
|
-
if (!log?.all || !branchData?.branches || !branchData?.branchCommits) return log?.all || [];
|
|
1695
|
-
|
|
1696
|
-
const hasVisibleMarkers = Object.values(visibilityMap).some(v => v === 'visible');
|
|
1697
|
-
const hasHiddenMarkers = Object.values(visibilityMap).some(v => v === 'hidden');
|
|
1698
|
-
|
|
1699
|
-
// If no visibility markers are set, show all commits
|
|
1700
|
-
if (!hasVisibleMarkers && !hasHiddenMarkers) {
|
|
1701
|
-
return log.all;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// Get all branches (local + remote)
|
|
1705
|
-
const allBranches: { branch: string; isRemote: boolean }[] = [
|
|
1706
|
-
...branchData.branches.map(b => ({ branch: b, isRemote: false })),
|
|
1707
|
-
];
|
|
1708
|
-
|
|
1709
|
-
// Add remote branches
|
|
1710
|
-
if (branchData.remotes) {
|
|
1711
|
-
for (const [remoteName, branches] of Object.entries(branchData.remotes)) {
|
|
1712
|
-
for (const branch of branches) {
|
|
1713
|
-
allBranches.push({ branch: `remotes/${remoteName}/${branch}`, isRemote: true });
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
// Calculate effective visibility for each branch
|
|
1719
|
-
const visibleBranches = new Set<string>();
|
|
1720
|
-
const hiddenBranchesSet = new Set<string>();
|
|
1721
|
-
const nonHiddenBranches = new Set<string>(); // Branches that are not hidden (for hidden-only mode)
|
|
1722
|
-
|
|
1723
|
-
for (const { branch, isRemote } of allBranches) {
|
|
1724
|
-
const effectiveVis = getBranchEffectiveVisibility(branch, isRemote);
|
|
1725
|
-
if (effectiveVis === 'visible') {
|
|
1726
|
-
visibleBranches.add(branch);
|
|
1727
|
-
} else if (effectiveVis === 'hidden') {
|
|
1728
|
-
hiddenBranchesSet.add(branch);
|
|
1729
|
-
} else {
|
|
1730
|
-
// No visibility set - this branch is "neutral" (non-hidden)
|
|
1731
|
-
nonHiddenBranches.add(branch);
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
// Build a map from commit hash to commit for quick lookup
|
|
1736
|
-
const commitMap = new Map(log.all.map(c => [c.hash, c]));
|
|
1737
|
-
|
|
1738
|
-
// Helper to mark all ancestors as reachable
|
|
1739
|
-
const markReachable = (startHash: string, reachableSet: Set<string>) => {
|
|
1740
|
-
const stack = [startHash];
|
|
1741
|
-
while (stack.length > 0) {
|
|
1742
|
-
const hash = stack.pop()!;
|
|
1743
|
-
if (reachableSet.has(hash)) continue;
|
|
1744
|
-
|
|
1745
|
-
const commit = commitMap.get(hash);
|
|
1746
|
-
if (!commit) continue;
|
|
1747
|
-
|
|
1748
|
-
reachableSet.add(hash);
|
|
1749
|
-
|
|
1750
|
-
// Add parents to process
|
|
1751
|
-
for (const parentHash of commit.parents || []) {
|
|
1752
|
-
if (!reachableSet.has(parentHash)) {
|
|
1753
|
-
stack.push(parentHash);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
};
|
|
1758
|
-
|
|
1759
|
-
// Find commits reachable from visible branches
|
|
1760
|
-
const reachableFromVisible = new Set<string>();
|
|
1761
|
-
for (const branch of visibleBranches) {
|
|
1762
|
-
const headHash = branchData.branchCommits[branch];
|
|
1763
|
-
if (headHash) {
|
|
1764
|
-
markReachable(headHash, reachableFromVisible);
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
// If there are visible markers, only show commits reachable from visible branches
|
|
1769
|
-
if (hasVisibleMarkers) {
|
|
1770
|
-
return log.all.filter(commit => reachableFromVisible.has(commit.hash));
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
// If only hidden markers exist, show commits reachable from any non-hidden branch
|
|
1774
|
-
// A commit should only be hidden if it's EXCLUSIVELY reachable from hidden branches
|
|
1775
|
-
if (hasHiddenMarkers) {
|
|
1776
|
-
const reachableFromNonHidden = new Set<string>();
|
|
1777
|
-
for (const branch of nonHiddenBranches) {
|
|
1778
|
-
const headHash = branchData.branchCommits[branch];
|
|
1779
|
-
if (headHash) {
|
|
1780
|
-
markReachable(headHash, reachableFromNonHidden);
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
return log.all.filter(commit => reachableFromNonHidden.has(commit.hash));
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
return log.all;
|
|
1788
|
-
}, [log?.all, branchData?.branches, branchData?.branchCommits, branchData?.remotes, visibilityMap, getBranchEffectiveVisibility]);
|
|
1789
|
-
|
|
1790
|
-
const selectedCommitHashSet = useMemo(() => new Set(selectedCommitHashes), [selectedCommitHashes]);
|
|
1791
|
-
const filteredCommitHashes = useMemo(() => filteredCommits.map((commit) => commit.hash), [filteredCommits]);
|
|
1792
|
-
|
|
1793
|
-
useEffect(() => {
|
|
1794
|
-
if (filteredCommitHashes.length === 0) {
|
|
1795
|
-
if (selectedHash || selectedCommitHashes.length > 0 || selectionAnchorHash) {
|
|
1796
|
-
selectSingleCommit(null);
|
|
1797
|
-
}
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
const filteredHashSet = new Set(filteredCommitHashes);
|
|
1802
|
-
const nextSelected = selectedCommitHashes.filter((hash) => filteredHashSet.has(hash));
|
|
1803
|
-
|
|
1804
|
-
if (nextSelected.length !== selectedCommitHashes.length) {
|
|
1805
|
-
setSelectedCommitHashes(nextSelected);
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
if (selectedHash && !filteredHashSet.has(selectedHash)) {
|
|
1809
|
-
setSelectedHash(nextSelected.length > 0 ? nextSelected[nextSelected.length - 1] : null);
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
if (selectionAnchorHash && !filteredHashSet.has(selectionAnchorHash)) {
|
|
1813
|
-
setSelectionAnchorHash(nextSelected.length > 0 ? nextSelected[nextSelected.length - 1] : null);
|
|
1814
|
-
}
|
|
1815
|
-
}, [filteredCommitHashes, selectedHash, selectedCommitHashes, selectionAnchorHash, selectSingleCommit]);
|
|
1816
|
-
|
|
1817
|
-
const handleSelectCommit = useCallback((hash: string, modifiers?: CommitRowSelectModifiers) => {
|
|
1818
|
-
const isRangeSelect = modifiers?.isRangeSelect ?? false;
|
|
1819
|
-
const isMultiSelect = modifiers?.isMultiSelect ?? false;
|
|
1820
|
-
|
|
1821
|
-
if (isRangeSelect) {
|
|
1822
|
-
const anchor = selectionAnchorHash ?? selectedHash ?? hash;
|
|
1823
|
-
const anchorIndex = filteredCommitHashes.indexOf(anchor);
|
|
1824
|
-
const targetIndex = filteredCommitHashes.indexOf(hash);
|
|
1825
|
-
|
|
1826
|
-
if (anchorIndex !== -1 && targetIndex !== -1) {
|
|
1827
|
-
const [start, end] = anchorIndex < targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex];
|
|
1828
|
-
const rangeSelection = filteredCommitHashes.slice(start, end + 1);
|
|
1829
|
-
setSelectedCommitHashes(rangeSelection);
|
|
1830
|
-
setSelectedHash(hash);
|
|
1831
|
-
setSelectionAnchorHash(anchor);
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
if (isMultiSelect) {
|
|
1837
|
-
if (selectedCommitHashSet.has(hash)) {
|
|
1838
|
-
const nextSelected = selectedCommitHashes.filter((selected) => selected !== hash);
|
|
1839
|
-
setSelectedCommitHashes(nextSelected);
|
|
1840
|
-
setSelectedHash((prev) => {
|
|
1841
|
-
if (prev !== hash) return prev;
|
|
1842
|
-
return nextSelected.length > 0 ? nextSelected[nextSelected.length - 1] : null;
|
|
1843
|
-
});
|
|
1844
|
-
if (selectionAnchorHash === hash) {
|
|
1845
|
-
setSelectionAnchorHash(nextSelected.length > 0 ? nextSelected[nextSelected.length - 1] : null);
|
|
1846
|
-
}
|
|
1847
|
-
} else {
|
|
1848
|
-
setSelectedCommitHashes([...selectedCommitHashes, hash]);
|
|
1849
|
-
setSelectedHash(hash);
|
|
1850
|
-
setSelectionAnchorHash(hash);
|
|
1851
|
-
}
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
selectSingleCommit(hash);
|
|
1856
|
-
}, [filteredCommitHashes, selectedCommitHashSet, selectedCommitHashes, selectedHash, selectionAnchorHash, selectSingleCommit]);
|
|
1857
|
-
|
|
1858
|
-
const selectedCommitsForCherryPick = useMemo(
|
|
1859
|
-
() => filteredCommits.filter((commit) => selectedCommitHashSet.has(commit.hash)).reverse(),
|
|
1860
|
-
[filteredCommits, selectedCommitHashSet]
|
|
1861
|
-
);
|
|
1862
|
-
|
|
1863
|
-
// Check if visibility filters are active
|
|
1864
|
-
const hasVisibilityFilters = useMemo(() => {
|
|
1865
|
-
return Object.values(visibilityMap).some(v => v === 'visible' || v === 'hidden');
|
|
1866
|
-
}, [visibilityMap]);
|
|
1867
|
-
|
|
1868
|
-
// Compute hidden branches set for filtering branch tags in git graph
|
|
1869
|
-
const hiddenBranches = useMemo(() => {
|
|
1870
|
-
const hidden = new Set<string>();
|
|
1871
|
-
|
|
1872
|
-
// Check local branches
|
|
1873
|
-
if (branchData?.branches) {
|
|
1874
|
-
for (const branch of branchData.branches) {
|
|
1875
|
-
const effectiveVis = getBranchEffectiveVisibility(branch, false);
|
|
1876
|
-
if (effectiveVis === 'hidden') {
|
|
1877
|
-
hidden.add(branch);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
// Check remote branches
|
|
1883
|
-
if (branchData?.remotes) {
|
|
1884
|
-
for (const [remoteName, branches] of Object.entries(branchData.remotes)) {
|
|
1885
|
-
for (const branch of branches) {
|
|
1886
|
-
const fullRef = `remotes/${remoteName}/${branch}`;
|
|
1887
|
-
const effectiveVis = getBranchEffectiveVisibility(fullRef, true);
|
|
1888
|
-
if (effectiveVis === 'hidden') {
|
|
1889
|
-
hidden.add(fullRef);
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
return hidden;
|
|
1896
|
-
}, [branchData?.branches, branchData?.remotes, getBranchEffectiveVisibility]);
|
|
1897
|
-
|
|
1898
|
-
// Auto-fetch more commits when filtered results are too few
|
|
1899
|
-
const MIN_FILTERED_COMMITS = 50;
|
|
1900
|
-
const MAX_AUTO_FETCH_LIMIT = 5000;
|
|
1901
|
-
|
|
1902
|
-
useEffect(() => {
|
|
1903
|
-
if (
|
|
1904
|
-
hasVisibilityFilters &&
|
|
1905
|
-
filteredCommits.length < MIN_FILTERED_COMMITS &&
|
|
1906
|
-
!isFetching &&
|
|
1907
|
-
limit < MAX_AUTO_FETCH_LIMIT &&
|
|
1908
|
-
log?.all && log.all.length >= limit
|
|
1909
|
-
) {
|
|
1910
|
-
// Fetch more commits - increase limit by 100
|
|
1911
|
-
setLimit(l => Math.min(l + 100, MAX_AUTO_FETCH_LIMIT));
|
|
1912
|
-
}
|
|
1913
|
-
}, [hasVisibilityFilters, filteredCommits.length, isFetching, limit, log?.all]);
|
|
1914
|
-
|
|
1915
|
-
// Handle scrolling to branch commit when it's loaded
|
|
1916
|
-
useEffect(() => {
|
|
1917
|
-
if (!pendingScrollCommit || !log?.all || isFetching) return;
|
|
1918
|
-
|
|
1919
|
-
// Check if commit exists in current loaded commits
|
|
1920
|
-
const commitExists = log.all.some(c => c.hash === pendingScrollCommit);
|
|
1921
|
-
|
|
1922
|
-
if (commitExists) {
|
|
1923
|
-
// Use requestAnimationFrame to ensure DOM is updated
|
|
1924
|
-
requestAnimationFrame(() => {
|
|
1925
|
-
const scrolled = gitGraphRef.current?.scrollToCommit(pendingScrollCommit);
|
|
1926
|
-
if (scrolled) {
|
|
1927
|
-
selectSingleCommit(pendingScrollCommit);
|
|
1928
|
-
setPendingScrollCommit(null);
|
|
1929
|
-
}
|
|
1930
|
-
});
|
|
1931
|
-
} else {
|
|
1932
|
-
// Need to load more commits - increase limit
|
|
1933
|
-
// Set a reasonable max limit to avoid infinite loading
|
|
1934
|
-
if (limit < 5000) {
|
|
1935
|
-
setLimit(l => l + 100);
|
|
1936
|
-
} else {
|
|
1937
|
-
// Give up after 5000 commits
|
|
1938
|
-
console.warn('Could not find commit after loading 5000 commits');
|
|
1939
|
-
setPendingScrollCommit(null);
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
}, [pendingScrollCommit, log?.all, isFetching, limit, selectSingleCommit]);
|
|
1943
|
-
|
|
1944
|
-
const scrollToBranchHeadCommit = useCallback((branch: string) => {
|
|
1945
|
-
if (!branchData?.branchCommits) return;
|
|
1946
|
-
|
|
1947
|
-
const commitHash = branchData.branchCommits[branch];
|
|
1948
|
-
if (!commitHash) return;
|
|
1949
|
-
|
|
1950
|
-
const commitExists = log?.all?.some(c => c.hash === commitHash);
|
|
1951
|
-
if (commitExists && gitGraphRef.current) {
|
|
1952
|
-
const scrolled = gitGraphRef.current.scrollToCommit(commitHash);
|
|
1953
|
-
if (scrolled) {
|
|
1954
|
-
selectSingleCommit(commitHash);
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
setPendingScrollCommit(commitHash);
|
|
1960
|
-
selectSingleCommit(commitHash);
|
|
1961
|
-
}, [branchData?.branchCommits, log?.all, selectSingleCommit]);
|
|
1962
|
-
|
|
1963
|
-
const handleBranchClick = useCallback((branch: string, modifiers?: BranchRowSelectModifiers) => {
|
|
1964
|
-
const isRangeSelect = modifiers?.isRangeSelect ?? false;
|
|
1965
|
-
const isMultiSelect = modifiers?.isMultiSelect ?? false;
|
|
1966
|
-
|
|
1967
|
-
if (isRangeSelect) {
|
|
1968
|
-
const anchor = branchSelectionAnchor ?? branch;
|
|
1969
|
-
const anchorIndex = orderedVisibleBranchRefs.indexOf(anchor);
|
|
1970
|
-
const targetIndex = orderedVisibleBranchRefs.indexOf(branch);
|
|
1971
|
-
if (anchorIndex !== -1 && targetIndex !== -1) {
|
|
1972
|
-
const [start, end] = anchorIndex < targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex];
|
|
1973
|
-
const rangeSelection = orderedVisibleBranchRefs.slice(start, end + 1);
|
|
1974
|
-
const shouldUnselect = selectedBranchSet.has(branch);
|
|
1975
|
-
if (shouldUnselect) {
|
|
1976
|
-
setSelectedBranchRefs(selectedBranchRefs.filter((selected) => !rangeSelection.includes(selected)));
|
|
1977
|
-
} else {
|
|
1978
|
-
setSelectedBranchRefs(Array.from(new Set([...selectedBranchRefs, ...rangeSelection])));
|
|
1979
|
-
}
|
|
1980
|
-
setBranchSelectionAnchor(anchor);
|
|
1981
|
-
} else {
|
|
1982
|
-
setSelectedBranchRefs([branch]);
|
|
1983
|
-
setBranchSelectionAnchor(branch);
|
|
1984
|
-
}
|
|
1985
|
-
return;
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
if (isMultiSelect) {
|
|
1989
|
-
if (selectedBranchSet.has(branch)) {
|
|
1990
|
-
const nextSelected = selectedBranchRefs.filter((selected) => selected !== branch);
|
|
1991
|
-
setSelectedBranchRefs(nextSelected);
|
|
1992
|
-
} else {
|
|
1993
|
-
setSelectedBranchRefs([...selectedBranchRefs, branch]);
|
|
1994
|
-
}
|
|
1995
|
-
setBranchSelectionAnchor(branch);
|
|
1996
|
-
return;
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
setSelectedBranchRefs([branch]);
|
|
2000
|
-
setBranchSelectionAnchor(branch);
|
|
2001
|
-
scrollToBranchHeadCommit(branch);
|
|
2002
|
-
}, [branchSelectionAnchor, orderedVisibleBranchRefs, scrollToBranchHeadCommit, selectedBranchRefs, selectedBranchSet]);
|
|
2003
|
-
|
|
2004
|
-
const handleBranchContextMenu = useCallback((branch: string) => {
|
|
2005
|
-
if (selectedBranchSet.has(branch)) return;
|
|
2006
|
-
setSelectedBranchRefs([branch]);
|
|
2007
|
-
setBranchSelectionAnchor(branch);
|
|
2008
|
-
}, [selectedBranchSet]);
|
|
2009
|
-
|
|
2010
|
-
const handleRunCustomScript = useCallback(async (script: RepositoryCustomScript, branchRef: string) => {
|
|
2011
|
-
setDidCopyScriptOutput(false);
|
|
2012
|
-
setIsCancelingScriptExecution(false);
|
|
2013
|
-
setScriptExecution({
|
|
2014
|
-
isOpen: true,
|
|
2015
|
-
executionId: null,
|
|
2016
|
-
scriptName: script.name,
|
|
2017
|
-
branchRef,
|
|
2018
|
-
output: '',
|
|
2019
|
-
status: 'starting',
|
|
2020
|
-
error: null,
|
|
2021
|
-
});
|
|
2022
|
-
|
|
2023
|
-
try {
|
|
2024
|
-
const response = await fetch('/api/custom-scripts', {
|
|
2025
|
-
method: 'POST',
|
|
2026
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2027
|
-
body: JSON.stringify({
|
|
2028
|
-
command: 'start',
|
|
2029
|
-
repoPath,
|
|
2030
|
-
branchRef,
|
|
2031
|
-
scriptContent: script.content,
|
|
2032
|
-
}),
|
|
2033
|
-
});
|
|
2034
|
-
|
|
2035
|
-
const result = await response.json();
|
|
2036
|
-
if (!response.ok) {
|
|
2037
|
-
throw new Error(result.error || 'Failed to start script execution');
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
const prelude: string[] = [];
|
|
2041
|
-
if (result.previousBranch && result.checkedOutBranch && result.previousBranch !== result.checkedOutBranch) {
|
|
2042
|
-
prelude.push(`[info] Checked out ${result.checkedOutBranch} (from ${result.previousBranch})`);
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
setScriptExecution((prev) => ({
|
|
2046
|
-
...prev,
|
|
2047
|
-
executionId: result.executionId,
|
|
2048
|
-
output: [prelude.join('\n'), result.output].filter(Boolean).join('\n'),
|
|
2049
|
-
status: result.status as ScriptExecutionStatus,
|
|
2050
|
-
error: null,
|
|
2051
|
-
}));
|
|
2052
|
-
} catch (error) {
|
|
2053
|
-
setScriptExecution((prev) => ({
|
|
2054
|
-
...prev,
|
|
2055
|
-
status: 'failed',
|
|
2056
|
-
error: (error as Error).message,
|
|
2057
|
-
output: prev.output
|
|
2058
|
-
? `${prev.output}\n[error] ${(error as Error).message}`
|
|
2059
|
-
: `[error] ${(error as Error).message}`,
|
|
2060
|
-
}));
|
|
2061
|
-
}
|
|
2062
|
-
}, [repoPath]);
|
|
2063
|
-
|
|
2064
|
-
const handleCancelCustomScriptExecution = useCallback(async () => {
|
|
2065
|
-
if (!scriptExecution.executionId || !isScriptExecutionRunning) return;
|
|
2066
|
-
|
|
2067
|
-
setIsCancelingScriptExecution(true);
|
|
2068
|
-
try {
|
|
2069
|
-
const response = await fetch('/api/custom-scripts', {
|
|
2070
|
-
method: 'POST',
|
|
2071
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2072
|
-
body: JSON.stringify({
|
|
2073
|
-
command: 'cancel',
|
|
2074
|
-
executionId: scriptExecution.executionId,
|
|
2075
|
-
}),
|
|
2076
|
-
});
|
|
2077
|
-
const result = await response.json();
|
|
2078
|
-
if (!response.ok) {
|
|
2079
|
-
throw new Error(result.error || 'Failed to cancel script execution');
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
setScriptExecution((prev) => ({
|
|
2083
|
-
...prev,
|
|
2084
|
-
output: result.output,
|
|
2085
|
-
status: result.status as ScriptExecutionStatus,
|
|
2086
|
-
}));
|
|
2087
|
-
|
|
2088
|
-
if (result.status !== 'running' && result.status !== 'starting') {
|
|
2089
|
-
setIsCancelingScriptExecution(false);
|
|
2090
|
-
}
|
|
2091
|
-
} catch (error) {
|
|
2092
|
-
setScriptExecution((prev) => ({
|
|
2093
|
-
...prev,
|
|
2094
|
-
output: `${prev.output}\n[error] ${(error as Error).message}`,
|
|
2095
|
-
}));
|
|
2096
|
-
setIsCancelingScriptExecution(false);
|
|
2097
|
-
}
|
|
2098
|
-
}, [scriptExecution.executionId, isScriptExecutionRunning]);
|
|
2099
|
-
|
|
2100
|
-
const handleCopyCustomScriptOutput = useCallback(async () => {
|
|
2101
|
-
if (isCopyingScriptOutput) return;
|
|
2102
|
-
|
|
2103
|
-
setIsCopyingScriptOutput(true);
|
|
2104
|
-
const copied = await copyText(scriptExecution.output);
|
|
2105
|
-
setIsCopyingScriptOutput(false);
|
|
2106
|
-
setDidCopyScriptOutput(copied);
|
|
2107
|
-
if (copied) {
|
|
2108
|
-
setTimeout(() => setDidCopyScriptOutput(false), 1500);
|
|
2109
|
-
}
|
|
2110
|
-
}, [isCopyingScriptOutput, scriptExecution.output]);
|
|
2111
|
-
|
|
2112
|
-
useEffect(() => {
|
|
2113
|
-
if (!scriptExecution.executionId || !isScriptExecutionRunning) return;
|
|
2114
|
-
|
|
2115
|
-
let disposed = false;
|
|
2116
|
-
let pollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
2117
|
-
|
|
2118
|
-
const poll = async () => {
|
|
2119
|
-
try {
|
|
2120
|
-
const response = await fetch('/api/custom-scripts', {
|
|
2121
|
-
method: 'POST',
|
|
2122
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2123
|
-
body: JSON.stringify({
|
|
2124
|
-
command: 'status',
|
|
2125
|
-
executionId: scriptExecution.executionId,
|
|
2126
|
-
}),
|
|
2127
|
-
});
|
|
2128
|
-
const result = await response.json();
|
|
2129
|
-
|
|
2130
|
-
if (!response.ok) {
|
|
2131
|
-
throw new Error(result.error || 'Failed to fetch script execution output');
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
if (disposed) return;
|
|
2135
|
-
|
|
2136
|
-
const nextStatus = result.status as ScriptExecutionStatus;
|
|
2137
|
-
setScriptExecution((prev) => {
|
|
2138
|
-
if (prev.executionId !== result.executionId) return prev;
|
|
2139
|
-
return {
|
|
2140
|
-
...prev,
|
|
2141
|
-
output: result.output,
|
|
2142
|
-
status: nextStatus,
|
|
2143
|
-
error: null,
|
|
2144
|
-
};
|
|
2145
|
-
});
|
|
2146
|
-
|
|
2147
|
-
if (nextStatus === 'running') {
|
|
2148
|
-
pollTimer = setTimeout(poll, 450);
|
|
2149
|
-
} else {
|
|
2150
|
-
setIsCancelingScriptExecution(false);
|
|
2151
|
-
}
|
|
2152
|
-
} catch (error) {
|
|
2153
|
-
if (disposed) return;
|
|
2154
|
-
setIsCancelingScriptExecution(false);
|
|
2155
|
-
setScriptExecution((prev) => ({
|
|
2156
|
-
...prev,
|
|
2157
|
-
status: 'failed',
|
|
2158
|
-
error: (error as Error).message,
|
|
2159
|
-
output: prev.output
|
|
2160
|
-
? `${prev.output}\n[error] ${(error as Error).message}`
|
|
2161
|
-
: `[error] ${(error as Error).message}`,
|
|
2162
|
-
}));
|
|
2163
|
-
}
|
|
2164
|
-
};
|
|
2165
|
-
|
|
2166
|
-
void poll();
|
|
2167
|
-
|
|
2168
|
-
return () => {
|
|
2169
|
-
disposed = true;
|
|
2170
|
-
if (pollTimer) {
|
|
2171
|
-
clearTimeout(pollTimer);
|
|
2172
|
-
}
|
|
2173
|
-
};
|
|
2174
|
-
}, [scriptExecution.executionId, isScriptExecutionRunning]);
|
|
2175
|
-
|
|
2176
|
-
const confirmDeleteBranches = (branches: string[]) => {
|
|
2177
|
-
const deletableBranches = branches.filter((branch) => branch !== currentBranch);
|
|
2178
|
-
if (deletableBranches.length === 0) return;
|
|
2179
|
-
setBranchesToDelete(deletableBranches);
|
|
2180
|
-
setDeleteRemoteBranch(false);
|
|
2181
|
-
setIsDeleteOpen(true);
|
|
2182
|
-
};
|
|
2183
|
-
|
|
2184
|
-
const confirmDeleteBranch = (branch: string) => {
|
|
2185
|
-
confirmDeleteBranches([branch]);
|
|
2186
|
-
};
|
|
2187
|
-
|
|
2188
|
-
const closeDeleteTagDialog = useCallback(() => {
|
|
2189
|
-
setIsDeleteTagOpen(false);
|
|
2190
|
-
setTagToDelete(null);
|
|
2191
|
-
setDeleteRemoteTag(false);
|
|
2192
|
-
}, []);
|
|
2193
|
-
|
|
2194
|
-
const confirmDeleteTag = useCallback((tagName: string) => {
|
|
2195
|
-
if (!tagName) return;
|
|
2196
|
-
setTagToDelete(tagName);
|
|
2197
|
-
setDeleteRemoteTag(false);
|
|
2198
|
-
setIsDeleteTagOpen(true);
|
|
2199
|
-
}, []);
|
|
2200
|
-
|
|
2201
|
-
const handleDeleteBranch = async () => {
|
|
2202
|
-
if (branchesToDelete.length === 0) return;
|
|
2203
|
-
setIsDeleting(true);
|
|
2204
|
-
try {
|
|
2205
|
-
const deleteRequests = new Map<string, { branchRef: string; run: () => Promise<unknown> }>();
|
|
2206
|
-
const addDeleteRequest = (key: string, branchRef: string, run: () => Promise<unknown>) => {
|
|
2207
|
-
if (deleteRequests.has(key)) return;
|
|
2208
|
-
deleteRequests.set(key, { branchRef, run });
|
|
2209
|
-
};
|
|
2210
|
-
|
|
2211
|
-
const trackingRemotesToDelete = new Set<string>();
|
|
2212
|
-
if (deleteRemoteBranch) {
|
|
2213
|
-
for (const branchRef of branchesToDelete) {
|
|
2214
|
-
if (branchRef.startsWith('remotes/')) continue;
|
|
2215
|
-
const tracking = branchData?.trackingInfo?.[branchRef];
|
|
2216
|
-
if (tracking?.upstream) {
|
|
2217
|
-
trackingRemotesToDelete.add(tracking.upstream);
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
for (const upstream of trackingRemotesToDelete) {
|
|
2223
|
-
const [remote, ...branchParts] = upstream.split('/');
|
|
2224
|
-
const branch = branchParts.join('/');
|
|
2225
|
-
if (remote && branch) {
|
|
2226
|
-
const branchRef = `${remote}/${branch}`;
|
|
2227
|
-
addDeleteRequest(`remote:${branchRef}`, branchRef, () =>
|
|
2228
|
-
runGitAction({
|
|
2229
|
-
repoPath,
|
|
2230
|
-
action: 'delete-remote-branch',
|
|
2231
|
-
data: { remote, branch },
|
|
2232
|
-
suppressErrorToast: true,
|
|
2233
|
-
})
|
|
2234
|
-
);
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
for (const branchRef of branchesToDelete) {
|
|
2239
|
-
if (branchRef.startsWith('remotes/')) {
|
|
2240
|
-
const parts = branchRef.split('/');
|
|
2241
|
-
if (parts.length >= 3) {
|
|
2242
|
-
const remote = parts[1];
|
|
2243
|
-
const branch = parts.slice(2).join('/');
|
|
2244
|
-
const remoteBranchRef = `${remote}/${branch}`;
|
|
2245
|
-
addDeleteRequest(`remote:${remoteBranchRef}`, remoteBranchRef, () =>
|
|
2246
|
-
runGitAction({
|
|
2247
|
-
repoPath,
|
|
2248
|
-
action: 'delete-remote-branch',
|
|
2249
|
-
data: { remote, branch },
|
|
2250
|
-
suppressErrorToast: true,
|
|
2251
|
-
})
|
|
2252
|
-
);
|
|
2253
|
-
}
|
|
2254
|
-
continue;
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
addDeleteRequest(`local:${branchRef}`, branchRef, () =>
|
|
2258
|
-
runGitAction({
|
|
2259
|
-
repoPath,
|
|
2260
|
-
action: 'delete-branch',
|
|
2261
|
-
data: { branch: branchRef },
|
|
2262
|
-
suppressErrorToast: true,
|
|
2263
|
-
})
|
|
2264
|
-
);
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
const requests = Array.from(deleteRequests.values());
|
|
2268
|
-
const results = await Promise.allSettled(requests.map((request) => request.run()));
|
|
2269
|
-
const failedBranches = results.flatMap((result, index) => {
|
|
2270
|
-
if (result.status === 'fulfilled') return [];
|
|
2271
|
-
const failedBranch = requests[index].branchRef;
|
|
2272
|
-
console.error(`Failed to delete branch "${failedBranch}":`, result.reason);
|
|
2273
|
-
return [failedBranch];
|
|
2274
|
-
});
|
|
2275
|
-
|
|
2276
|
-
if (failedBranches.length > 0) {
|
|
2277
|
-
toast({
|
|
2278
|
-
type: 'error',
|
|
2279
|
-
title: failedBranches.length === 1
|
|
2280
|
-
? 'Failed to Delete 1 Branch'
|
|
2281
|
-
: `Failed to Delete ${failedBranches.length} Branches`,
|
|
2282
|
-
description: (
|
|
2283
|
-
<div>
|
|
2284
|
-
<div>The following branches could not be deleted:</div>
|
|
2285
|
-
<ul className="mt-1 max-h-40 overflow-y-auto list-disc pl-5">
|
|
2286
|
-
{failedBranches.map((branch) => (
|
|
2287
|
-
<li key={branch} className="break-all">{branch}</li>
|
|
2288
|
-
))}
|
|
2289
|
-
</ul>
|
|
2290
|
-
</div>
|
|
2291
|
-
),
|
|
2292
|
-
duration: 10000,
|
|
2293
|
-
});
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
setIsDeleteOpen(false);
|
|
2297
|
-
setBranchesToDelete([]);
|
|
2298
|
-
setDeleteRemoteBranch(false);
|
|
2299
|
-
setSelectedBranchRefs([]);
|
|
2300
|
-
setBranchSelectionAnchor(null);
|
|
2301
|
-
} catch (e) {
|
|
2302
|
-
console.error(e);
|
|
2303
|
-
} finally {
|
|
2304
|
-
setIsDeleting(false);
|
|
2305
|
-
}
|
|
2306
|
-
};
|
|
2307
|
-
|
|
2308
|
-
const handleDeleteTag = async () => {
|
|
2309
|
-
if (!tagToDelete) return;
|
|
2310
|
-
setIsDeletingTag(true);
|
|
2311
|
-
try {
|
|
2312
|
-
await runGitAction({
|
|
2313
|
-
repoPath,
|
|
2314
|
-
action: 'delete-tag',
|
|
2315
|
-
data: { tag: tagToDelete },
|
|
2316
|
-
});
|
|
2317
|
-
|
|
2318
|
-
if (deleteRemoteTag && remoteNameForTagDelete) {
|
|
2319
|
-
try {
|
|
2320
|
-
await runGitAction({
|
|
2321
|
-
repoPath,
|
|
2322
|
-
action: 'delete-remote-tag',
|
|
2323
|
-
data: { remote: remoteNameForTagDelete, tag: tagToDelete },
|
|
2324
|
-
});
|
|
2325
|
-
} catch (error) {
|
|
2326
|
-
console.error('Failed to delete remote tag:', error);
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
closeDeleteTagDialog();
|
|
2331
|
-
} catch (e) {
|
|
2332
|
-
console.error(e);
|
|
2333
|
-
} finally {
|
|
2334
|
-
setIsDeletingTag(false);
|
|
2335
|
-
}
|
|
2336
|
-
};
|
|
2337
|
-
|
|
2338
|
-
const confirmRenameBranch = (branch: string) => {
|
|
2339
|
-
setBranchToRename(branch);
|
|
2340
|
-
setRemoteBranchToRename(null);
|
|
2341
|
-
// Pre-fill with current branch name
|
|
2342
|
-
setNewBranchNameForRename(branch);
|
|
2343
|
-
setRenameTrackingRemoteBranch(false);
|
|
2344
|
-
setIsRenameOpen(true);
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
const confirmRenameRemoteBranch = (fullRemoteBranch: string) => {
|
|
2348
|
-
const parts = fullRemoteBranch.split('/');
|
|
2349
|
-
if (parts.length < 3 || parts[0] !== 'remotes') return;
|
|
2350
|
-
|
|
2351
|
-
const remote = parts[1];
|
|
2352
|
-
const branch = parts.slice(2).join('/');
|
|
2353
|
-
if (!remote || !branch) return;
|
|
2354
|
-
|
|
2355
|
-
setBranchToRename(fullRemoteBranch);
|
|
2356
|
-
setRemoteBranchToRename({ remote, branch });
|
|
2357
|
-
setNewBranchNameForRename(branch);
|
|
2358
|
-
setRenameTrackingRemoteBranch(false);
|
|
2359
|
-
setIsRenameOpen(true);
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
const confirmRenameRemote = (remote: string) => {
|
|
2363
|
-
setRemoteToRename(remote);
|
|
2364
|
-
setNewRemoteNameForRename(remote);
|
|
2365
|
-
setIsRenameRemoteOpen(true);
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
const confirmDeleteRemote = (remote: string) => {
|
|
2369
|
-
if (!remote) return;
|
|
2370
|
-
setRemoteToDelete(remote);
|
|
2371
|
-
setIsDeleteRemoteOpen(true);
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
const confirmAddRemote = () => {
|
|
2375
|
-
setNewRemoteName('origin');
|
|
2376
|
-
setNewRemoteUrl('');
|
|
2377
|
-
setIsAddRemoteOpen(true);
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
const handleRenameBranch = async () => {
|
|
2381
|
-
if (!branchToRename || !newBranchNameForRename) return;
|
|
2382
|
-
const isSameName = remoteBranchToRename
|
|
2383
|
-
? remoteBranchToRename.branch === newBranchNameForRename
|
|
2384
|
-
: branchToRename === newBranchNameForRename;
|
|
2385
|
-
|
|
2386
|
-
if (isSameName) {
|
|
2387
|
-
closeRenameBranchDialog();
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
setIsRenaming(true);
|
|
2391
|
-
try {
|
|
2392
|
-
if (remoteBranchToRename) {
|
|
2393
|
-
await runGitAction({
|
|
2394
|
-
repoPath,
|
|
2395
|
-
action: 'rename-remote-branch',
|
|
2396
|
-
data: {
|
|
2397
|
-
remote: remoteBranchToRename.remote,
|
|
2398
|
-
oldName: remoteBranchToRename.branch,
|
|
2399
|
-
newName: newBranchNameForRename,
|
|
2400
|
-
}
|
|
2401
|
-
});
|
|
2402
|
-
} else {
|
|
2403
|
-
await runGitAction({
|
|
2404
|
-
repoPath,
|
|
2405
|
-
action: 'rename-branch',
|
|
2406
|
-
data: {
|
|
2407
|
-
oldName: branchToRename,
|
|
2408
|
-
newName: newBranchNameForRename,
|
|
2409
|
-
renameTrackingRemote: renameTrackingRemoteBranch,
|
|
2410
|
-
}
|
|
2411
|
-
});
|
|
2412
|
-
}
|
|
2413
|
-
closeRenameBranchDialog();
|
|
2414
|
-
} catch (e) {
|
|
2415
|
-
console.error(e);
|
|
2416
|
-
} finally {
|
|
2417
|
-
setIsRenaming(false);
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
const handleRenameRemote = async () => {
|
|
2422
|
-
if (!remoteToRename) return;
|
|
2423
|
-
const trimmedOldName = remoteToRename.trim();
|
|
2424
|
-
const trimmedNewName = newRemoteNameForRename.trim();
|
|
2425
|
-
if (!trimmedOldName || !trimmedNewName) return;
|
|
2426
|
-
|
|
2427
|
-
if (trimmedOldName === trimmedNewName) {
|
|
2428
|
-
closeRenameRemoteDialog();
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
|
-
setIsRenamingRemote(true);
|
|
2433
|
-
try {
|
|
2434
|
-
await runGitAction({
|
|
2435
|
-
repoPath,
|
|
2436
|
-
action: 'rename-remote',
|
|
2437
|
-
data: {
|
|
2438
|
-
oldName: trimmedOldName,
|
|
2439
|
-
newName: trimmedNewName,
|
|
2440
|
-
},
|
|
2441
|
-
});
|
|
2442
|
-
closeRenameRemoteDialog();
|
|
2443
|
-
} catch (e) {
|
|
2444
|
-
console.error(e);
|
|
2445
|
-
} finally {
|
|
2446
|
-
setIsRenamingRemote(false);
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
const handleDeleteRemote = async () => {
|
|
2451
|
-
if (!remoteToDelete) return;
|
|
2452
|
-
const trimmedRemoteName = remoteToDelete.trim();
|
|
2453
|
-
if (!trimmedRemoteName) return;
|
|
2454
|
-
|
|
2455
|
-
setIsDeletingRemote(true);
|
|
2456
|
-
try {
|
|
2457
|
-
await runGitAction({
|
|
2458
|
-
repoPath,
|
|
2459
|
-
action: 'delete-remote',
|
|
2460
|
-
data: {
|
|
2461
|
-
name: trimmedRemoteName,
|
|
2462
|
-
},
|
|
2463
|
-
});
|
|
2464
|
-
closeDeleteRemoteDialog();
|
|
2465
|
-
} catch (e) {
|
|
2466
|
-
console.error(e);
|
|
2467
|
-
} finally {
|
|
2468
|
-
setIsDeletingRemote(false);
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
|
|
2472
|
-
const handleAddRemote = async () => {
|
|
2473
|
-
const trimmedName = newRemoteName.trim();
|
|
2474
|
-
const trimmedUrl = newRemoteUrl.trim();
|
|
2475
|
-
if (!trimmedName || !trimmedUrl) return;
|
|
2476
|
-
|
|
2477
|
-
setIsAddingRemote(true);
|
|
2478
|
-
try {
|
|
2479
|
-
await runGitAction({
|
|
2480
|
-
repoPath,
|
|
2481
|
-
action: 'add-remote',
|
|
2482
|
-
data: {
|
|
2483
|
-
name: trimmedName,
|
|
2484
|
-
url: trimmedUrl,
|
|
2485
|
-
},
|
|
2486
|
-
});
|
|
2487
|
-
closeAddRemoteDialog();
|
|
2488
|
-
} catch (e) {
|
|
2489
|
-
console.error(e);
|
|
2490
|
-
} finally {
|
|
2491
|
-
setIsAddingRemote(false);
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
const confirmRebase = ({ sourceBranch, targetBranch }: BranchOperation) => {
|
|
2496
|
-
setRebaseSourceBranch(sourceBranch);
|
|
2497
|
-
setRebaseTargetBranch(targetBranch);
|
|
2498
|
-
setRebaseStashChanges(true);
|
|
2499
|
-
setRebaseConflictStatus('checking');
|
|
2500
|
-
setIsRebaseOpen(true);
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
const handleRebase = async () => {
|
|
2504
|
-
if (!rebaseSourceBranch || !rebaseTargetBranch) return;
|
|
2505
|
-
setIsRebasing(true);
|
|
2506
|
-
try {
|
|
2507
|
-
await runGitAction({
|
|
2508
|
-
repoPath,
|
|
2509
|
-
action: 'checkout',
|
|
2510
|
-
data: { branch: rebaseSourceBranch }
|
|
2511
|
-
});
|
|
2512
|
-
|
|
2513
|
-
await runGitAction({
|
|
2514
|
-
repoPath,
|
|
2515
|
-
action: 'rebase',
|
|
2516
|
-
data: { ontoBranch: rebaseTargetBranch, stashChanges: rebaseStashChanges }
|
|
2517
|
-
});
|
|
2518
|
-
closeRebaseDialog();
|
|
2519
|
-
} catch (e) {
|
|
2520
|
-
console.error(e);
|
|
2521
|
-
} finally {
|
|
2522
|
-
setIsRebasing(false);
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
useEffect(() => {
|
|
2527
|
-
const sourceBranch = rebaseSourceBranch;
|
|
2528
|
-
const ontoBranch = rebaseTargetBranch;
|
|
2529
|
-
|
|
2530
|
-
if (!isRebaseOpen || !sourceBranch || !ontoBranch) {
|
|
2531
|
-
return;
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
let cancelled = false;
|
|
2535
|
-
setRebaseConflictStatus('checking');
|
|
2536
|
-
|
|
2537
|
-
const checkRebaseConflicts = async () => {
|
|
2538
|
-
try {
|
|
2539
|
-
const result = await runGitAction({
|
|
2540
|
-
repoPath,
|
|
2541
|
-
action: 'check-rebase-conflicts',
|
|
2542
|
-
data: {
|
|
2543
|
-
sourceBranch,
|
|
2544
|
-
ontoBranch,
|
|
2545
|
-
},
|
|
2546
|
-
});
|
|
2547
|
-
|
|
2548
|
-
if (!cancelled) {
|
|
2549
|
-
setRebaseConflictStatus(result.hasConflicts ? 'has-conflicts' : 'no-conflict');
|
|
2550
|
-
}
|
|
2551
|
-
} catch (e) {
|
|
2552
|
-
console.error(e);
|
|
2553
|
-
if (!cancelled) {
|
|
2554
|
-
// Be conservative when the check cannot be completed.
|
|
2555
|
-
setRebaseConflictStatus('has-conflicts');
|
|
2556
|
-
}
|
|
2557
|
-
}
|
|
2558
|
-
};
|
|
2559
|
-
|
|
2560
|
-
void checkRebaseConflicts();
|
|
2561
|
-
|
|
2562
|
-
return () => {
|
|
2563
|
-
cancelled = true;
|
|
2564
|
-
};
|
|
2565
|
-
}, [isRebaseOpen, rebaseSourceBranch, rebaseTargetBranch, repoPath, runGitAction]);
|
|
2566
|
-
|
|
2567
|
-
const confirmMerge = ({ sourceBranch, targetBranch }: BranchOperation) => {
|
|
2568
|
-
setMergeTargetBranch(targetBranch);
|
|
2569
|
-
setMergeSourceBranch(sourceBranch);
|
|
2570
|
-
setMergeRebaseBeforeMerge(false);
|
|
2571
|
-
setMergeSquash(false);
|
|
2572
|
-
setMergeFastForward(false);
|
|
2573
|
-
setMergeSquashMessage('');
|
|
2574
|
-
setMergeConflictStatus('checking');
|
|
2575
|
-
setIsMergeOpen(true);
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
const handleMergeSquashToggle = useCallback((enabled: boolean) => {
|
|
2579
|
-
setMergeSquash(enabled);
|
|
2580
|
-
|
|
2581
|
-
if (!enabled) {
|
|
2582
|
-
return;
|
|
2583
|
-
}
|
|
2584
|
-
|
|
2585
|
-
if (!mergeSourceBranch) {
|
|
2586
|
-
setMergeSquashMessage('');
|
|
2587
|
-
return;
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
void (async () => {
|
|
2591
|
-
try {
|
|
2592
|
-
const result = await runGitAction({
|
|
2593
|
-
repoPath,
|
|
2594
|
-
action: 'get-latest-commit-message',
|
|
2595
|
-
data: { branch: mergeSourceBranch },
|
|
2596
|
-
});
|
|
2597
|
-
setMergeSquashMessage(typeof result?.message === 'string' ? result.message : '');
|
|
2598
|
-
} catch (e) {
|
|
2599
|
-
console.error(e);
|
|
2600
|
-
setMergeSquashMessage('');
|
|
2601
|
-
}
|
|
2602
|
-
})();
|
|
2603
|
-
}, [mergeSourceBranch, repoPath, runGitAction]);
|
|
2604
|
-
|
|
2605
|
-
const handleMerge = async () => {
|
|
2606
|
-
if (!mergeTargetBranch || !mergeSourceBranch) return;
|
|
2607
|
-
setIsMerging(true);
|
|
2608
|
-
try {
|
|
2609
|
-
await runGitAction({
|
|
2610
|
-
repoPath,
|
|
2611
|
-
action: 'checkout',
|
|
2612
|
-
data: { branch: mergeTargetBranch }
|
|
2613
|
-
});
|
|
2614
|
-
|
|
2615
|
-
await runGitAction({
|
|
2616
|
-
repoPath,
|
|
2617
|
-
action: 'merge',
|
|
2618
|
-
data: {
|
|
2619
|
-
targetBranch: mergeSourceBranch,
|
|
2620
|
-
rebaseBeforeMerge: mergeRebaseBeforeMerge,
|
|
2621
|
-
squash: mergeSquash,
|
|
2622
|
-
fastForward: mergeFastForward,
|
|
2623
|
-
squashMessage: mergeSquash ? mergeSquashMessage : undefined,
|
|
2624
|
-
}
|
|
2625
|
-
});
|
|
2626
|
-
closeMergeDialog();
|
|
2627
|
-
} catch (e) {
|
|
2628
|
-
console.error(e);
|
|
2629
|
-
} finally {
|
|
2630
|
-
setIsMerging(false);
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
useEffect(() => {
|
|
2635
|
-
const sourceBranch = mergeSourceBranch;
|
|
2636
|
-
const targetBranch = mergeTargetBranch;
|
|
2637
|
-
|
|
2638
|
-
if (!isMergeOpen || !sourceBranch || !targetBranch) {
|
|
2639
|
-
return;
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
let cancelled = false;
|
|
2643
|
-
setMergeConflictStatus('checking');
|
|
2644
|
-
|
|
2645
|
-
const checkMergeConflicts = async () => {
|
|
2646
|
-
try {
|
|
2647
|
-
const result = await runGitAction({
|
|
2648
|
-
repoPath,
|
|
2649
|
-
action: 'check-merge-conflicts',
|
|
2650
|
-
data: {
|
|
2651
|
-
sourceBranch,
|
|
2652
|
-
targetBranch,
|
|
2653
|
-
},
|
|
2654
|
-
});
|
|
2655
|
-
|
|
2656
|
-
if (!cancelled) {
|
|
2657
|
-
setMergeConflictStatus(result.hasConflicts ? 'has-conflicts' : 'no-conflict');
|
|
2658
|
-
}
|
|
2659
|
-
} catch (e) {
|
|
2660
|
-
console.error(e);
|
|
2661
|
-
if (!cancelled) {
|
|
2662
|
-
// Be conservative when the check cannot be completed.
|
|
2663
|
-
setMergeConflictStatus('has-conflicts');
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
};
|
|
2667
|
-
|
|
2668
|
-
void checkMergeConflicts();
|
|
2669
|
-
|
|
2670
|
-
return () => {
|
|
2671
|
-
cancelled = true;
|
|
2672
|
-
};
|
|
2673
|
-
}, [isMergeOpen, mergeSourceBranch, mergeTargetBranch, repoPath, runGitAction]);
|
|
2674
|
-
|
|
2675
|
-
const confirmPushToRemote = async (branch: string) => {
|
|
2676
|
-
setPushBranch(branch);
|
|
2677
|
-
setPushError(null);
|
|
2678
|
-
setPushRemotes([]);
|
|
2679
|
-
setPushRemoteBranches([]);
|
|
2680
|
-
setPushSelectedRemote('');
|
|
2681
|
-
setPushSelectedRemoteBranch('');
|
|
2682
|
-
setPushTrackingBranch(null);
|
|
2683
|
-
setPushRebaseFirst(false);
|
|
2684
|
-
setPushForcePush(false);
|
|
2685
|
-
setPushLocalOnlyTags(true);
|
|
2686
|
-
setPushSquash(false);
|
|
2687
|
-
setPushSquashMessage('');
|
|
2688
|
-
setIsPushOpen(true);
|
|
2689
|
-
|
|
2690
|
-
// Load remotes
|
|
2691
|
-
setPushLoadingRemotes(true);
|
|
2692
|
-
try {
|
|
2693
|
-
const result = await runGitAction({
|
|
2694
|
-
repoPath,
|
|
2695
|
-
action: 'get-remotes',
|
|
2696
|
-
data: {}
|
|
2697
|
-
});
|
|
2698
|
-
|
|
2699
|
-
if (!result.remotes || result.remotes.length === 0) {
|
|
2700
|
-
setPushError('No remote repository configured. Please add a remote first.');
|
|
2701
|
-
setPushLoadingRemotes(false);
|
|
2702
|
-
return;
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
setPushRemotes(result.remotes);
|
|
2706
|
-
|
|
2707
|
-
// Get tracking branch info
|
|
2708
|
-
const trackingResult = await runGitAction({
|
|
2709
|
-
repoPath,
|
|
2710
|
-
action: 'get-tracking-branch',
|
|
2711
|
-
data: { branch }
|
|
2712
|
-
});
|
|
2713
|
-
|
|
2714
|
-
setPushTrackingBranch(trackingResult.tracking);
|
|
2715
|
-
|
|
2716
|
-
// Set default remote - use tracking remote if available and exists in remotes list, otherwise first remote
|
|
2717
|
-
const trackingRemote = trackingResult.tracking?.remote;
|
|
2718
|
-
const defaultRemote = (trackingRemote && result.remotes.includes(trackingRemote))
|
|
2719
|
-
? trackingRemote
|
|
2720
|
-
: result.remotes[0];
|
|
2721
|
-
setPushSelectedRemote(defaultRemote);
|
|
2722
|
-
|
|
2723
|
-
// Load branches for the default remote
|
|
2724
|
-
setPushLoadingBranches(true);
|
|
2725
|
-
const branchesResult = await runGitAction({
|
|
2726
|
-
repoPath,
|
|
2727
|
-
action: 'get-remote-branches',
|
|
2728
|
-
data: { remote: defaultRemote }
|
|
2729
|
-
});
|
|
2730
|
-
|
|
2731
|
-
setPushRemoteBranches(branchesResult.branches || []);
|
|
2732
|
-
|
|
2733
|
-
// Set default remote branch - use tracking branch if on same remote, otherwise use branch name
|
|
2734
|
-
if (trackingResult.tracking?.remote === defaultRemote && trackingResult.tracking?.branch) {
|
|
2735
|
-
setPushSelectedRemoteBranch(trackingResult.tracking.branch);
|
|
2736
|
-
} else {
|
|
2737
|
-
// Default to same name as local branch, or first branch if local branch name doesn't exist
|
|
2738
|
-
const localBranchName = branch;
|
|
2739
|
-
if (branchesResult.branches?.includes(localBranchName)) {
|
|
2740
|
-
setPushSelectedRemoteBranch(localBranchName);
|
|
2741
|
-
} else {
|
|
2742
|
-
// Will create new branch with local branch name
|
|
2743
|
-
setPushSelectedRemoteBranch(localBranchName);
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
} catch (e) {
|
|
2747
|
-
console.error(e);
|
|
2748
|
-
setPushError((e as Error).message || 'Failed to load remote information');
|
|
2749
|
-
} finally {
|
|
2750
|
-
setPushLoadingRemotes(false);
|
|
2751
|
-
setPushLoadingBranches(false);
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
|
|
2755
|
-
const handlePushRemoteChange = async (remote: string) => {
|
|
2756
|
-
setPushSelectedRemote(remote);
|
|
2757
|
-
setPushLoadingBranches(true);
|
|
2758
|
-
setPushRemoteBranches([]);
|
|
2759
|
-
|
|
2760
|
-
try {
|
|
2761
|
-
const branchesResult = await runGitAction({
|
|
2762
|
-
repoPath,
|
|
2763
|
-
action: 'get-remote-branches',
|
|
2764
|
-
data: { remote }
|
|
2765
|
-
});
|
|
2766
|
-
|
|
2767
|
-
setPushRemoteBranches(branchesResult.branches || []);
|
|
2768
|
-
|
|
2769
|
-
// Set default branch - tracking branch if on this remote, otherwise local branch name
|
|
2770
|
-
if (pushTrackingBranch?.remote === remote && pushTrackingBranch?.branch) {
|
|
2771
|
-
setPushSelectedRemoteBranch(pushTrackingBranch.branch);
|
|
2772
|
-
} else {
|
|
2773
|
-
setPushSelectedRemoteBranch(pushBranch || '');
|
|
2774
|
-
}
|
|
2775
|
-
} catch (e) {
|
|
2776
|
-
console.error(e);
|
|
2777
|
-
setPushError((e as Error).message || 'Failed to load remote branches');
|
|
2778
|
-
} finally {
|
|
2779
|
-
setPushLoadingBranches(false);
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
|
-
const handlePushSquashToggle = useCallback((enabled: boolean) => {
|
|
2784
|
-
setPushSquash(enabled);
|
|
2785
|
-
|
|
2786
|
-
if (!enabled) {
|
|
2787
|
-
return;
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
if (!pushBranch) {
|
|
2791
|
-
setPushSquashMessage('');
|
|
2792
|
-
return;
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
void (async () => {
|
|
2796
|
-
try {
|
|
2797
|
-
const result = await runGitAction({
|
|
2798
|
-
repoPath,
|
|
2799
|
-
action: 'get-latest-commit-message',
|
|
2800
|
-
data: { branch: pushBranch },
|
|
2801
|
-
});
|
|
2802
|
-
setPushSquashMessage(typeof result?.message === 'string' ? result.message : '');
|
|
2803
|
-
} catch (e) {
|
|
2804
|
-
console.error(e);
|
|
2805
|
-
setPushSquashMessage('');
|
|
2806
|
-
}
|
|
2807
|
-
})();
|
|
2808
|
-
}, [pushBranch, repoPath, runGitAction]);
|
|
2809
|
-
|
|
2810
|
-
const handlePushToRemote = async () => {
|
|
2811
|
-
if (!pushBranch || !pushSelectedRemote || !pushSelectedRemoteBranch) return;
|
|
2812
|
-
|
|
2813
|
-
setIsPushing(true);
|
|
2814
|
-
setPushError(null);
|
|
2815
|
-
|
|
2816
|
-
try {
|
|
2817
|
-
// Determine if we need to set upstream
|
|
2818
|
-
const isNewBranch = !pushRemoteBranches.includes(pushSelectedRemoteBranch);
|
|
2819
|
-
const needsSetUpstream = isNewBranch ||
|
|
2820
|
-
pushTrackingBranch?.remote !== pushSelectedRemote ||
|
|
2821
|
-
pushTrackingBranch?.branch !== pushSelectedRemoteBranch;
|
|
2822
|
-
|
|
2823
|
-
await runGitAction({
|
|
2824
|
-
repoPath,
|
|
2825
|
-
action: 'push-to-remote',
|
|
2826
|
-
data: {
|
|
2827
|
-
localBranch: pushBranch,
|
|
2828
|
-
remote: pushSelectedRemote,
|
|
2829
|
-
remoteBranch: pushSelectedRemoteBranch,
|
|
2830
|
-
rebaseFirst: pushForcePush ? false : pushRebaseFirst,
|
|
2831
|
-
forcePush: pushForcePush,
|
|
2832
|
-
pushLocalOnlyTags,
|
|
2833
|
-
setUpstream: needsSetUpstream,
|
|
2834
|
-
squash: pushSquash,
|
|
2835
|
-
squashMessage: pushSquashMessage,
|
|
2836
|
-
}
|
|
2837
|
-
});
|
|
2838
|
-
|
|
2839
|
-
// Fetch from the remote we just pushed to
|
|
2840
|
-
await runGitAction({
|
|
2841
|
-
repoPath,
|
|
2842
|
-
action: 'fetch',
|
|
2843
|
-
data: { remote: pushSelectedRemote }
|
|
2844
|
-
});
|
|
2845
|
-
|
|
2846
|
-
setIsPushOpen(false);
|
|
2847
|
-
setPushBranch(null);
|
|
2848
|
-
} catch (e) {
|
|
2849
|
-
console.error(e);
|
|
2850
|
-
setPushError((e as Error).message || 'Failed to push to remote');
|
|
2851
|
-
} finally {
|
|
2852
|
-
setIsPushing(false);
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
const confirmPullFromRemote = async (branch: string) => {
|
|
2857
|
-
setPullBranch(branch);
|
|
2858
|
-
setPullError(null);
|
|
2859
|
-
setPullRemotes([]);
|
|
2860
|
-
setPullRemoteBranches([]);
|
|
2861
|
-
setPullSelectedRemote('');
|
|
2862
|
-
setPullSelectedRemoteBranch('');
|
|
2863
|
-
setPullTrackingBranch(null);
|
|
2864
|
-
setPullRebase(true);
|
|
2865
|
-
setIsPullOpen(true);
|
|
2866
|
-
|
|
2867
|
-
// Load remotes
|
|
2868
|
-
setPullLoadingRemotes(true);
|
|
2869
|
-
try {
|
|
2870
|
-
const result = await runGitAction({
|
|
2871
|
-
repoPath,
|
|
2872
|
-
action: 'get-remotes',
|
|
2873
|
-
data: {}
|
|
2874
|
-
});
|
|
2875
|
-
|
|
2876
|
-
if (!result.remotes || result.remotes.length === 0) {
|
|
2877
|
-
setPullError('No remote repository configured. Please add a remote first.');
|
|
2878
|
-
setPullLoadingRemotes(false);
|
|
2879
|
-
return;
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
setPullRemotes(result.remotes);
|
|
2883
|
-
|
|
2884
|
-
// Get tracking branch info
|
|
2885
|
-
const trackingResult = await runGitAction({
|
|
2886
|
-
repoPath,
|
|
2887
|
-
action: 'get-tracking-branch',
|
|
2888
|
-
data: { branch }
|
|
2889
|
-
});
|
|
2890
|
-
|
|
2891
|
-
setPullTrackingBranch(trackingResult.tracking);
|
|
2892
|
-
|
|
2893
|
-
// Set default remote - use tracking remote if available and exists in remotes list, otherwise first remote
|
|
2894
|
-
const trackingRemote = trackingResult.tracking?.remote;
|
|
2895
|
-
const defaultRemote = (trackingRemote && result.remotes.includes(trackingRemote))
|
|
2896
|
-
? trackingRemote
|
|
2897
|
-
: result.remotes[0];
|
|
2898
|
-
setPullSelectedRemote(defaultRemote);
|
|
2899
|
-
|
|
2900
|
-
// Load branches for the default remote
|
|
2901
|
-
setPullLoadingBranches(true);
|
|
2902
|
-
const branchesResult = await runGitAction({
|
|
2903
|
-
repoPath,
|
|
2904
|
-
action: 'get-remote-branches',
|
|
2905
|
-
data: { remote: defaultRemote }
|
|
2906
|
-
});
|
|
2907
|
-
|
|
2908
|
-
setPullRemoteBranches(branchesResult.branches || []);
|
|
2909
|
-
|
|
2910
|
-
// Set default remote branch - use tracking branch if on same remote
|
|
2911
|
-
if (trackingResult.tracking?.remote === defaultRemote && trackingResult.tracking?.branch) {
|
|
2912
|
-
setPullSelectedRemoteBranch(trackingResult.tracking.branch);
|
|
2913
|
-
} else {
|
|
2914
|
-
// No tracking branch on this remote - leave empty to show error
|
|
2915
|
-
setPullSelectedRemoteBranch('');
|
|
2916
|
-
}
|
|
2917
|
-
} catch (e) {
|
|
2918
|
-
console.error(e);
|
|
2919
|
-
setPullError((e as Error).message || 'Failed to load remote information');
|
|
2920
|
-
} finally {
|
|
2921
|
-
setPullLoadingRemotes(false);
|
|
2922
|
-
setPullLoadingBranches(false);
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
const handlePullRemoteChange = async (remote: string) => {
|
|
2927
|
-
setPullSelectedRemote(remote);
|
|
2928
|
-
setPullLoadingBranches(true);
|
|
2929
|
-
setPullRemoteBranches([]);
|
|
2930
|
-
setPullSelectedRemoteBranch('');
|
|
2931
|
-
|
|
2932
|
-
try {
|
|
2933
|
-
const branchesResult = await runGitAction({
|
|
2934
|
-
repoPath,
|
|
2935
|
-
action: 'get-remote-branches',
|
|
2936
|
-
data: { remote }
|
|
2937
|
-
});
|
|
2938
|
-
|
|
2939
|
-
setPullRemoteBranches(branchesResult.branches || []);
|
|
2940
|
-
|
|
2941
|
-
// Set default branch - tracking branch if on this remote
|
|
2942
|
-
if (pullTrackingBranch?.remote === remote && pullTrackingBranch?.branch) {
|
|
2943
|
-
setPullSelectedRemoteBranch(pullTrackingBranch.branch);
|
|
2944
|
-
} else {
|
|
2945
|
-
// No tracking branch on this remote - leave empty
|
|
2946
|
-
setPullSelectedRemoteBranch('');
|
|
2947
|
-
}
|
|
2948
|
-
} catch (e) {
|
|
2949
|
-
console.error(e);
|
|
2950
|
-
setPullError((e as Error).message || 'Failed to load remote branches');
|
|
2951
|
-
} finally {
|
|
2952
|
-
setPullLoadingBranches(false);
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
|
|
2956
|
-
const handlePullFromRemote = async () => {
|
|
2957
|
-
if (!pullBranch || !pullSelectedRemote || !pullSelectedRemoteBranch) return;
|
|
2958
|
-
|
|
2959
|
-
setIsPulling(true);
|
|
2960
|
-
setPullError(null);
|
|
2961
|
-
|
|
2962
|
-
try {
|
|
2963
|
-
await runGitAction({
|
|
2964
|
-
repoPath,
|
|
2965
|
-
action: 'pull-from-remote',
|
|
2966
|
-
data: {
|
|
2967
|
-
localBranch: pullBranch,
|
|
2968
|
-
remote: pullSelectedRemote,
|
|
2969
|
-
remoteBranch: pullSelectedRemoteBranch,
|
|
2970
|
-
rebase: pullRebase,
|
|
2971
|
-
}
|
|
2972
|
-
});
|
|
2973
|
-
|
|
2974
|
-
setIsPullOpen(false);
|
|
2975
|
-
setPullBranch(null);
|
|
2976
|
-
} catch (e) {
|
|
2977
|
-
console.error(e);
|
|
2978
|
-
setPullError((e as Error).message || 'Failed to pull from remote');
|
|
2979
|
-
} finally {
|
|
2980
|
-
setIsPulling(false);
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
|
-
const confirmCheckoutToLocal = (remoteBranch: string) => {
|
|
2985
|
-
setCheckoutRemoteBranch(remoteBranch);
|
|
2986
|
-
// Extract the branch name from remotes/origin/branch-name
|
|
2987
|
-
const parts = remoteBranch.split('/');
|
|
2988
|
-
// Skip 'remotes' and remote name (e.g., 'origin'), take the rest as branch name
|
|
2989
|
-
const branchName = parts.slice(2).join('/');
|
|
2990
|
-
setCheckoutLocalBranchName(branchName);
|
|
2991
|
-
setIsCheckoutToLocalOpen(true);
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
const handleCheckoutToLocal = async () => {
|
|
2995
|
-
if (!checkoutRemoteBranch || !checkoutLocalBranchName) return;
|
|
2996
|
-
setIsCheckingOutToLocal(true);
|
|
2997
|
-
try {
|
|
2998
|
-
await runGitAction({
|
|
2999
|
-
repoPath,
|
|
3000
|
-
action: 'checkout-to-local',
|
|
3001
|
-
data: { remoteBranch: checkoutRemoteBranch, localBranch: checkoutLocalBranchName }
|
|
3002
|
-
});
|
|
3003
|
-
setIsCheckoutToLocalOpen(false);
|
|
3004
|
-
setCheckoutRemoteBranch(null);
|
|
3005
|
-
setCheckoutLocalBranchName('');
|
|
3006
|
-
} catch (e) {
|
|
3007
|
-
console.error(e);
|
|
3008
|
-
} finally {
|
|
3009
|
-
setIsCheckingOutToLocal(false);
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
const handleFetchFromAllRemotes = async () => {
|
|
3014
|
-
setIsFetchingAllRemotes(true);
|
|
3015
|
-
try {
|
|
3016
|
-
await runGitAction({
|
|
3017
|
-
repoPath,
|
|
3018
|
-
action: 'fetch',
|
|
3019
|
-
data: { allRemotes: true }
|
|
3020
|
-
});
|
|
3021
|
-
} catch (e) {
|
|
3022
|
-
console.error(e);
|
|
3023
|
-
} finally {
|
|
3024
|
-
setIsFetchingAllRemotes(false);
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
|
|
3028
|
-
const handleFetchFromRemote = async (remote: string) => {
|
|
3029
|
-
try {
|
|
3030
|
-
await runGitAction({
|
|
3031
|
-
repoPath,
|
|
3032
|
-
action: 'fetch',
|
|
3033
|
-
data: { remote }
|
|
3034
|
-
});
|
|
3035
|
-
} catch (e) {
|
|
3036
|
-
console.error(e);
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
|
|
3040
|
-
const handleCheckout = async (branchName: string) => {
|
|
3041
|
-
|
|
3042
|
-
try {
|
|
3043
|
-
await runGitAction({
|
|
3044
|
-
repoPath,
|
|
3045
|
-
action: 'checkout',
|
|
3046
|
-
data: { branch: branchName }
|
|
3047
|
-
});
|
|
3048
|
-
} catch (e) {
|
|
3049
|
-
console.error(e);
|
|
3050
|
-
}
|
|
3051
|
-
};
|
|
3052
|
-
|
|
3053
|
-
const handleResetToCommit = async (commitHash: string) => {
|
|
3054
|
-
setResetCommitHash(commitHash);
|
|
3055
|
-
setIsResetOpen(true);
|
|
3056
|
-
};
|
|
3057
|
-
|
|
3058
|
-
const handleConfirmReset = async () => {
|
|
3059
|
-
if (!resetCommitHash) return;
|
|
3060
|
-
setIsResetting(true);
|
|
3061
|
-
try {
|
|
3062
|
-
await runGitAction({
|
|
3063
|
-
repoPath,
|
|
3064
|
-
action: 'reset',
|
|
3065
|
-
data: { commitHash: resetCommitHash, mode: 'hard' }
|
|
3066
|
-
});
|
|
3067
|
-
setIsResetOpen(false);
|
|
3068
|
-
setResetCommitHash(null);
|
|
3069
|
-
} catch (e) {
|
|
3070
|
-
console.error(e);
|
|
3071
|
-
} finally {
|
|
3072
|
-
setIsResetting(false);
|
|
3073
|
-
}
|
|
3074
|
-
};
|
|
3075
|
-
|
|
3076
|
-
const confirmRevertCommit = (commitHash: string, commitMessage: string) => {
|
|
3077
|
-
setCommitToRevert({ hash: commitHash, message: commitMessage });
|
|
3078
|
-
setIsRevertOpen(true);
|
|
3079
|
-
};
|
|
3080
|
-
|
|
3081
|
-
const handleConfirmRevert = async () => {
|
|
3082
|
-
if (!commitToRevert) return;
|
|
3083
|
-
setIsReverting(true);
|
|
3084
|
-
try {
|
|
3085
|
-
await runGitAction({
|
|
3086
|
-
repoPath,
|
|
3087
|
-
action: 'revert',
|
|
3088
|
-
data: { commitHash: commitToRevert.hash }
|
|
3089
|
-
});
|
|
3090
|
-
setIsRevertOpen(false);
|
|
3091
|
-
setCommitToRevert(null);
|
|
3092
|
-
} catch (e) {
|
|
3093
|
-
console.error(e);
|
|
3094
|
-
} finally {
|
|
3095
|
-
setIsReverting(false);
|
|
3096
|
-
}
|
|
3097
|
-
};
|
|
3098
|
-
|
|
3099
|
-
const confirmRewordCommit = (hash: string, subject: string, body: string, branch: string) => {
|
|
3100
|
-
setCommitToReword({ hash, subject, body, branch });
|
|
3101
|
-
setNewMessageSubject(subject);
|
|
3102
|
-
setNewMessageBody(body);
|
|
3103
|
-
setIsRewordOpen(true);
|
|
3104
|
-
};
|
|
3105
|
-
|
|
3106
|
-
const handleReword = async () => {
|
|
3107
|
-
if (!commitToReword || !newMessageSubject.trim()) return;
|
|
3108
|
-
setIsRewording(true);
|
|
3109
|
-
try {
|
|
3110
|
-
await runGitAction({
|
|
3111
|
-
repoPath,
|
|
3112
|
-
action: 'reword',
|
|
3113
|
-
data: {
|
|
3114
|
-
commitHash: commitToReword.hash,
|
|
3115
|
-
message: buildCommitMessage(newMessageSubject, newMessageBody),
|
|
3116
|
-
branch: commitToReword.branch,
|
|
3117
|
-
}
|
|
3118
|
-
});
|
|
3119
|
-
closeRewordDialog();
|
|
3120
|
-
} catch (e) {
|
|
3121
|
-
console.error(e);
|
|
3122
|
-
} finally {
|
|
3123
|
-
setIsRewording(false);
|
|
3124
|
-
}
|
|
3125
|
-
};
|
|
3126
|
-
|
|
3127
|
-
const confirmCherryPickCommit = (commitHash: string, commitMessage: string) => {
|
|
3128
|
-
setCommitsToCherryPick([{ hash: commitHash, message: commitMessage }]);
|
|
3129
|
-
setIsCherryPickOpen(true);
|
|
3130
|
-
};
|
|
3131
|
-
|
|
3132
|
-
const confirmCherryPickSelectedCommits = () => {
|
|
3133
|
-
if (selectedCommitsForCherryPick.length < 2) return;
|
|
3134
|
-
setCommitsToCherryPick(selectedCommitsForCherryPick.map((commit) => ({
|
|
3135
|
-
hash: commit.hash,
|
|
3136
|
-
message: commit.message,
|
|
3137
|
-
})));
|
|
3138
|
-
setIsCherryPickOpen(true);
|
|
3139
|
-
};
|
|
3140
|
-
|
|
3141
|
-
const isCherryPickAlreadyInProgressError = (error: unknown) => {
|
|
3142
|
-
if (!(error instanceof Error)) return false;
|
|
3143
|
-
const message = error.message.toLowerCase();
|
|
3144
|
-
return message.includes('cherry-pick') && message.includes('already in progress');
|
|
3145
|
-
};
|
|
3146
|
-
|
|
3147
|
-
const isCherryPickConflictError = (error: unknown) => {
|
|
3148
|
-
if (!(error instanceof Error)) return false;
|
|
3149
|
-
const message = error.message.toLowerCase();
|
|
3150
|
-
return (
|
|
3151
|
-
message.includes('could not apply') ||
|
|
3152
|
-
message.includes('conflict') ||
|
|
3153
|
-
message.includes('cherry-pick --continue')
|
|
3154
|
-
);
|
|
3155
|
-
};
|
|
3156
|
-
|
|
3157
|
-
const runCherryPickByHashes = useCallback(async (commitHashes: string[]) => {
|
|
3158
|
-
if (commitHashes.length === 1) {
|
|
3159
|
-
await runGitAction({
|
|
3160
|
-
repoPath,
|
|
3161
|
-
action: 'cherry-pick',
|
|
3162
|
-
data: { commitHash: commitHashes[0] }
|
|
3163
|
-
});
|
|
3164
|
-
return;
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
await runGitAction({
|
|
3168
|
-
repoPath,
|
|
3169
|
-
action: 'cherry-pick-multiple',
|
|
3170
|
-
data: { commitHashes }
|
|
3171
|
-
});
|
|
3172
|
-
}, [repoPath, runGitAction]);
|
|
3173
|
-
|
|
3174
|
-
const abortCherryPickAndResetUi = useCallback(async () => {
|
|
3175
|
-
try {
|
|
3176
|
-
await runGitAction({
|
|
3177
|
-
repoPath,
|
|
3178
|
-
action: 'cherry-pick-abort',
|
|
3179
|
-
});
|
|
3180
|
-
} catch (abortError) {
|
|
3181
|
-
console.error(abortError);
|
|
3182
|
-
} finally {
|
|
3183
|
-
setIsCherryPickOpen(false);
|
|
3184
|
-
setIsAbortCherryPickOpen(false);
|
|
3185
|
-
setCommitsToCherryPick([]);
|
|
3186
|
-
}
|
|
3187
|
-
}, [repoPath, runGitAction]);
|
|
3188
|
-
|
|
3189
|
-
const handleCherryPickCommit = async () => {
|
|
3190
|
-
if (commitsToCherryPick.length === 0) return;
|
|
3191
|
-
setIsCherryPicking(true);
|
|
3192
|
-
try {
|
|
3193
|
-
const commitHashes = commitsToCherryPick.map((commit) => commit.hash);
|
|
3194
|
-
await runCherryPickByHashes(commitHashes);
|
|
3195
|
-
setIsCherryPickOpen(false);
|
|
3196
|
-
setCommitsToCherryPick([]);
|
|
3197
|
-
} catch (e) {
|
|
3198
|
-
if (isCherryPickAlreadyInProgressError(e)) {
|
|
3199
|
-
setIsCherryPickOpen(false);
|
|
3200
|
-
setIsAbortCherryPickOpen(true);
|
|
3201
|
-
} else if (isCherryPickConflictError(e)) {
|
|
3202
|
-
await abortCherryPickAndResetUi();
|
|
3203
|
-
}
|
|
3204
|
-
console.error(e);
|
|
3205
|
-
} finally {
|
|
3206
|
-
setIsCherryPicking(false);
|
|
3207
|
-
}
|
|
3208
|
-
};
|
|
3209
|
-
|
|
3210
|
-
const handleAbortCherryPick = async () => {
|
|
3211
|
-
if (commitsToCherryPick.length === 0) {
|
|
3212
|
-
setIsAbortCherryPickOpen(false);
|
|
3213
|
-
return;
|
|
3214
|
-
}
|
|
3215
|
-
|
|
3216
|
-
setIsAbortingCherryPick(true);
|
|
3217
|
-
try {
|
|
3218
|
-
await runGitAction({
|
|
3219
|
-
repoPath,
|
|
3220
|
-
action: 'cherry-pick-abort',
|
|
3221
|
-
});
|
|
3222
|
-
|
|
3223
|
-
const commitHashes = commitsToCherryPick.map((commit) => commit.hash);
|
|
3224
|
-
await runCherryPickByHashes(commitHashes);
|
|
3225
|
-
|
|
3226
|
-
setIsAbortCherryPickOpen(false);
|
|
3227
|
-
setCommitsToCherryPick([]);
|
|
3228
|
-
} catch (e) {
|
|
3229
|
-
if (isCherryPickAlreadyInProgressError(e)) {
|
|
3230
|
-
setIsAbortCherryPickOpen(true);
|
|
3231
|
-
} else if (isCherryPickConflictError(e)) {
|
|
3232
|
-
await abortCherryPickAndResetUi();
|
|
3233
|
-
}
|
|
3234
|
-
console.error(e);
|
|
3235
|
-
} finally {
|
|
3236
|
-
setIsAbortingCherryPick(false);
|
|
3237
|
-
}
|
|
3238
|
-
};
|
|
3239
|
-
|
|
3240
|
-
const handleCreateBranch = async () => {
|
|
3241
|
-
if (!newBranchName) return;
|
|
3242
|
-
setIsCreating(true);
|
|
3243
|
-
try {
|
|
3244
|
-
await runGitAction({
|
|
3245
|
-
repoPath,
|
|
3246
|
-
action: 'branch',
|
|
3247
|
-
data: { branch: newBranchName, fromRef: createBranchFromRef || undefined }
|
|
3248
|
-
});
|
|
3249
|
-
setIsCreateBranchOpen(false);
|
|
3250
|
-
setNewBranchName('');
|
|
3251
|
-
setCreateBranchFromRef(null);
|
|
3252
|
-
} catch (e) {
|
|
3253
|
-
console.error(e);
|
|
3254
|
-
// alert or toast error
|
|
3255
|
-
} finally {
|
|
3256
|
-
setIsCreating(false);
|
|
3257
|
-
}
|
|
3258
|
-
};
|
|
3259
|
-
|
|
3260
|
-
const confirmCreateBranch = (sourceBranch?: string) => {
|
|
3261
|
-
setCreateBranchFromRef(sourceBranch || null);
|
|
3262
|
-
setIsCreateBranchOpen(true);
|
|
3263
|
-
};
|
|
3264
|
-
|
|
3265
|
-
const confirmCreateTag = (commitHash: string) => {
|
|
3266
|
-
setCreateTagCommitHash(commitHash);
|
|
3267
|
-
setNewTagName('');
|
|
3268
|
-
setPushTagToRemote(false);
|
|
3269
|
-
setIsCreateTagOpen(true);
|
|
3270
|
-
};
|
|
3271
|
-
|
|
3272
|
-
const handleCreateTag = async () => {
|
|
3273
|
-
if (!createTagCommitHash || !newTagName.trim()) return;
|
|
3274
|
-
setIsCreatingTag(true);
|
|
3275
|
-
try {
|
|
3276
|
-
await runGitAction({
|
|
3277
|
-
repoPath,
|
|
3278
|
-
action: 'create-tag',
|
|
3279
|
-
data: {
|
|
3280
|
-
tagName: newTagName.trim(),
|
|
3281
|
-
commitHash: createTagCommitHash,
|
|
3282
|
-
pushToRemote: pushTagToRemote,
|
|
3283
|
-
}
|
|
3284
|
-
});
|
|
3285
|
-
setIsCreateTagOpen(false);
|
|
3286
|
-
setCreateTagCommitHash(null);
|
|
3287
|
-
setNewTagName('');
|
|
3288
|
-
setPushTagToRemote(false);
|
|
3289
|
-
} catch (e) {
|
|
3290
|
-
console.error(e);
|
|
3291
|
-
} finally {
|
|
3292
|
-
setIsCreatingTag(false);
|
|
3293
|
-
}
|
|
3294
|
-
};
|
|
3295
|
-
|
|
3296
|
-
const currentBranch = branchData?.current;
|
|
3297
|
-
const trackingInfoByBranch = branchData?.trackingInfo;
|
|
3298
|
-
const remoteNames = useMemo(() => {
|
|
3299
|
-
const names = Object.keys(branchData?.remoteUrls ?? {});
|
|
3300
|
-
return names.sort((a, b) => {
|
|
3301
|
-
if (a === 'origin') return -1;
|
|
3302
|
-
if (b === 'origin') return 1;
|
|
3303
|
-
return a.localeCompare(b);
|
|
3304
|
-
});
|
|
3305
|
-
}, [branchData?.remoteUrls]);
|
|
3306
|
-
const remoteNameForTagDelete = remoteNames[0] || null;
|
|
3307
|
-
const selectedTrackingUpstreams = useMemo(() => {
|
|
3308
|
-
const upstreams: string[] = [];
|
|
3309
|
-
for (const branchRef of branchesToDelete) {
|
|
3310
|
-
if (branchRef.startsWith('remotes/')) continue;
|
|
3311
|
-
const upstream = trackingInfoByBranch?.[branchRef]?.upstream;
|
|
3312
|
-
if (upstream) upstreams.push(upstream);
|
|
3313
|
-
}
|
|
3314
|
-
return Array.from(new Set(upstreams));
|
|
3315
|
-
}, [branchesToDelete, trackingInfoByBranch]);
|
|
3316
|
-
const trackingInfoForRename = useMemo(() => {
|
|
3317
|
-
if (!branchToRename || remoteBranchToRename) return null;
|
|
3318
|
-
return trackingInfoByBranch?.[branchToRename] ?? null;
|
|
3319
|
-
}, [branchToRename, remoteBranchToRename, trackingInfoByBranch]);
|
|
3320
|
-
const localChangesCount = statusData?.files?.length;
|
|
3321
|
-
const currentBranchName = currentBranch || (isBranchesLoading ? 'Loading branches...' : 'Detached HEAD');
|
|
3322
|
-
const currentBranchLabel = currentBranch && typeof localChangesCount === 'number' && localChangesCount > 0
|
|
3323
|
-
? `${currentBranch} (${localChangesCount})`
|
|
3324
|
-
: currentBranchName;
|
|
3325
|
-
const currentTrackingBranch = useMemo(() => {
|
|
3326
|
-
if (!currentBranch) return null;
|
|
3327
|
-
const tracking = trackingInfoByBranch?.[currentBranch];
|
|
3328
|
-
if (!tracking?.upstream) return null;
|
|
3329
|
-
const slashIndex = tracking.upstream.indexOf('/');
|
|
3330
|
-
if (slashIndex <= 0) return null;
|
|
3331
|
-
|
|
3332
|
-
return {
|
|
3333
|
-
upstream: tracking.upstream,
|
|
3334
|
-
remote: tracking.upstream.slice(0, slashIndex),
|
|
3335
|
-
branch: tracking.upstream.slice(slashIndex + 1),
|
|
3336
|
-
};
|
|
3337
|
-
}, [currentBranch, trackingInfoByBranch]);
|
|
3338
|
-
const pullActionDisabledReason = useMemo(() => {
|
|
3339
|
-
if (isBranchesLoading) return 'Loading branches...';
|
|
3340
|
-
if (!currentBranch) return 'Not on a local branch';
|
|
3341
|
-
if (!currentTrackingBranch) return `Branch "${currentBranch}" has no tracking remote branch`;
|
|
3342
|
-
return null;
|
|
3343
|
-
}, [currentBranch, currentTrackingBranch, isBranchesLoading]);
|
|
3344
|
-
const pushActionDisabledReason = useMemo(() => {
|
|
3345
|
-
if (isBranchesLoading) return 'Loading branches...';
|
|
3346
|
-
if (!currentBranch) return 'Not on a local branch';
|
|
3347
|
-
return null;
|
|
3348
|
-
}, [currentBranch, isBranchesLoading]);
|
|
3349
|
-
|
|
3350
|
-
const confirmPullCurrentBranch = () => {
|
|
3351
|
-
if (!currentBranch || pullActionDisabledReason) return;
|
|
3352
|
-
void confirmPullFromRemote(currentBranch);
|
|
3353
|
-
};
|
|
3354
|
-
|
|
3355
|
-
const confirmPushCurrentBranch = () => {
|
|
3356
|
-
if (!currentBranch || pushActionDisabledReason) return;
|
|
3357
|
-
void confirmPushToRemote(currentBranch);
|
|
3358
|
-
};
|
|
3359
|
-
|
|
3360
|
-
const getBranchContextMenuItems = (options: BranchMenuOptions): ContextMenuItem[] => {
|
|
3361
|
-
const menuItems = buildBranchContextMenuItems(options, {
|
|
3362
|
-
onCheckout: handleCheckout,
|
|
3363
|
-
onCheckoutToLocal: confirmCheckoutToLocal,
|
|
3364
|
-
onCreateBranch: confirmCreateBranch,
|
|
3365
|
-
onDeleteBranch: confirmDeleteBranch,
|
|
3366
|
-
onDeleteBranches: confirmDeleteBranches,
|
|
3367
|
-
onRenameBranch: confirmRenameBranch,
|
|
3368
|
-
onRenameRemoteBranch: confirmRenameRemoteBranch,
|
|
3369
|
-
onRebase: confirmRebase,
|
|
3370
|
-
onMerge: confirmMerge,
|
|
3371
|
-
onPushToRemote: confirmPushToRemote,
|
|
3372
|
-
onPullFromRemote: confirmPullFromRemote,
|
|
3373
|
-
});
|
|
3374
|
-
|
|
3375
|
-
if (customBranchScripts.length > 0) {
|
|
3376
|
-
menuItems.push({
|
|
3377
|
-
label: 'Custom scripts',
|
|
3378
|
-
children: customBranchScripts.map((script) => ({
|
|
3379
|
-
label: script.name,
|
|
3380
|
-
onClick: () => {
|
|
3381
|
-
void handleRunCustomScript(script, options.branchRef);
|
|
3382
|
-
},
|
|
3383
|
-
})),
|
|
3384
|
-
});
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
return menuItems;
|
|
3388
|
-
};
|
|
3389
|
-
|
|
3390
|
-
const localBranchSet = useMemo(() => {
|
|
3391
|
-
return new Set(branchData?.branches ?? []);
|
|
3392
|
-
}, [branchData?.branches]);
|
|
3393
|
-
|
|
3394
|
-
const remoteBranchMap = useMemo(() => {
|
|
3395
|
-
const map = new Map<string, string>();
|
|
3396
|
-
if (!branchData?.remotes) return map;
|
|
3397
|
-
|
|
3398
|
-
for (const [remoteName, branches] of Object.entries(branchData.remotes)) {
|
|
3399
|
-
for (const branch of branches) {
|
|
3400
|
-
map.set(`${remoteName}/${branch}`, `remotes/${remoteName}/${branch}`);
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
|
-
return map;
|
|
3405
|
-
}, [branchData?.remotes]);
|
|
3406
|
-
|
|
3407
|
-
const getBranchTagContextMenuItems = (displayRef: string): ContextMenuItem[] | null => {
|
|
3408
|
-
if (displayRef.startsWith('tag:')) {
|
|
3409
|
-
const tagName = displayRef.replace(/^tag:\s*/, '').trim();
|
|
3410
|
-
if (!tagName) return null;
|
|
3411
|
-
return [
|
|
3412
|
-
{
|
|
3413
|
-
label: 'Delete Tag',
|
|
3414
|
-
onClick: () => confirmDeleteTag(tagName),
|
|
3415
|
-
danger: true,
|
|
3416
|
-
},
|
|
3417
|
-
];
|
|
3418
|
-
}
|
|
3419
|
-
|
|
3420
|
-
if (localBranchSet.has(displayRef)) {
|
|
3421
|
-
return getBranchContextMenuItems({
|
|
3422
|
-
branchRef: displayRef,
|
|
3423
|
-
branchLeafName: displayRef.split('/').pop() || displayRef,
|
|
3424
|
-
currentBranch,
|
|
3425
|
-
isRemote: false,
|
|
3426
|
-
});
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
const remoteBranchRef = remoteBranchMap.get(displayRef);
|
|
3430
|
-
if (!remoteBranchRef) return null;
|
|
3431
|
-
|
|
3432
|
-
return getBranchContextMenuItems({
|
|
3433
|
-
branchRef: remoteBranchRef,
|
|
3434
|
-
branchLeafName: remoteBranchRef.split('/').pop() || displayRef,
|
|
3435
|
-
currentBranch,
|
|
3436
|
-
isRemote: true,
|
|
3437
|
-
});
|
|
3438
|
-
};
|
|
3439
|
-
const localGroupBranchRefs = useMemo(() => {
|
|
3440
|
-
if (!localBranchTree) return [];
|
|
3441
|
-
return collectAllBranchRefs(localBranchTree).filter((branchRef) => branchRef !== currentBranch);
|
|
3442
|
-
}, [localBranchTree, currentBranch]);
|
|
3443
|
-
|
|
3444
|
-
const branchTreePopoverContent = (
|
|
3445
|
-
<div className="w-[22rem] max-w-[calc(100vw-2rem)] flex flex-col border border-base-300 bg-base-100 rounded-box shadow-xl overflow-hidden">
|
|
3446
|
-
<div className="px-4 border-b border-base-300 flex items-center justify-between bg-base-100 h-[57px] shrink-0">
|
|
3447
|
-
<h2 className="font-bold text-lg">Branches</h2>
|
|
3448
|
-
<div className="flex items-center gap-1">
|
|
3449
|
-
{hasVisibilityFilters && (
|
|
3450
|
-
<div className="tooltip tooltip-left z-20" data-tip="Clear filters">
|
|
3451
|
-
<button
|
|
3452
|
-
className="btn btn-ghost btn-xs btn-square"
|
|
3453
|
-
onClick={handleClearAllFilters}
|
|
3454
|
-
>
|
|
3455
|
-
<i className="iconoir-filter text-[16px]" aria-hidden="true" />
|
|
3456
|
-
</button>
|
|
3457
|
-
</div>
|
|
3458
|
-
)}
|
|
3459
|
-
<div className="tooltip tooltip-left z-20" data-tip="Create Branch">
|
|
3460
|
-
<button className="btn btn-ghost btn-xs btn-square" onClick={() => confirmCreateBranch()}>
|
|
3461
|
-
<i className="iconoir-plus-circle text-[16px]" aria-hidden="true" />
|
|
3462
|
-
</button>
|
|
3463
|
-
</div>
|
|
3464
|
-
</div>
|
|
3465
|
-
</div>
|
|
3466
|
-
<div className="max-h-[70vh] overflow-auto">
|
|
3467
|
-
<div className="p-2 space-y-0.5">
|
|
3468
|
-
{localBranchTree && (
|
|
3469
|
-
<>
|
|
3470
|
-
<ContextMenu
|
|
3471
|
-
items={[
|
|
3472
|
-
{
|
|
3473
|
-
label: 'Delete',
|
|
3474
|
-
onClick: () => confirmDeleteBranches(localGroupBranchRefs),
|
|
3475
|
-
danger: true,
|
|
3476
|
-
disabled: localGroupBranchRefs.length === 0,
|
|
3477
|
-
},
|
|
3478
|
-
]}
|
|
3479
|
-
>
|
|
3480
|
-
<GroupHeader
|
|
3481
|
-
name="Branches"
|
|
3482
|
-
groupPath="__local__"
|
|
3483
|
-
icon={<i className="iconoir-git-branch text-[14px]" aria-hidden="true" />}
|
|
3484
|
-
isExpanded={localGroupExpanded}
|
|
3485
|
-
onToggle={handleToggleLocalGroup}
|
|
3486
|
-
visibilityMap={visibilityMap}
|
|
3487
|
-
onToggleVisibility={handleToggleVisibility}
|
|
3488
|
-
/>
|
|
3489
|
-
</ContextMenu>
|
|
3490
|
-
{localGroupExpanded && (
|
|
3491
|
-
<BranchTreeItem
|
|
3492
|
-
node={localBranchTree}
|
|
3493
|
-
currentBranch={branchData?.current}
|
|
3494
|
-
expandedFolders={expandedFolders}
|
|
3495
|
-
onToggleFolder={toggleFolder}
|
|
3496
|
-
onCheckout={handleCheckout}
|
|
3497
|
-
onCheckoutToLocal={confirmCheckoutToLocal}
|
|
3498
|
-
onCreateBranch={() => confirmCreateBranch()}
|
|
3499
|
-
onDeleteBranch={confirmDeleteBranch}
|
|
3500
|
-
onRenameBranch={confirmRenameBranch}
|
|
3501
|
-
onRenameRemoteBranch={confirmRenameRemoteBranch}
|
|
3502
|
-
onRebase={confirmRebase}
|
|
3503
|
-
onMerge={confirmMerge}
|
|
3504
|
-
onPushToRemote={confirmPushToRemote}
|
|
3505
|
-
onPullFromRemote={confirmPullFromRemote}
|
|
3506
|
-
getBranchContextMenuItems={getBranchContextMenuItems}
|
|
3507
|
-
onBranchClick={handleBranchClick}
|
|
3508
|
-
onBranchContextMenu={handleBranchContextMenu}
|
|
3509
|
-
onDeleteBranchGroup={confirmDeleteBranches}
|
|
3510
|
-
selectedBranches={selectedBranchSet}
|
|
3511
|
-
visibilityMap={visibilityMap}
|
|
3512
|
-
onToggleVisibility={handleToggleVisibility}
|
|
3513
|
-
depth={1}
|
|
3514
|
-
groupPath="__local__"
|
|
3515
|
-
trackingInfo={branchData?.trackingInfo}
|
|
3516
|
-
/>
|
|
3517
|
-
)}
|
|
3518
|
-
</>
|
|
3519
|
-
)}
|
|
3520
|
-
|
|
3521
|
-
<>
|
|
3522
|
-
<ContextMenu
|
|
3523
|
-
items={[
|
|
3524
|
-
{ label: 'Fetch from all remotes', onClick: handleFetchFromAllRemotes },
|
|
3525
|
-
{ label: 'Add remote', onClick: confirmAddRemote },
|
|
3526
|
-
]}
|
|
3527
|
-
>
|
|
3528
|
-
<GroupHeader
|
|
3529
|
-
name="Remotes"
|
|
3530
|
-
groupPath="__remotes__"
|
|
3531
|
-
icon={<i className="iconoir-globe text-[14px]" aria-hidden="true" />}
|
|
3532
|
-
isExpanded={remotesGroupExpanded}
|
|
3533
|
-
onToggle={handleToggleRemotesGroup}
|
|
3534
|
-
visibilityMap={visibilityMap}
|
|
3535
|
-
onToggleVisibility={handleToggleVisibility}
|
|
3536
|
-
/>
|
|
3537
|
-
</ContextMenu>
|
|
3538
|
-
{remotesGroupExpanded && isBranchesLoading && !remoteBranchTrees && (
|
|
3539
|
-
<div className="flex items-center gap-2 px-2 py-2 text-sm opacity-70" style={{ paddingLeft: '20px' }}>
|
|
3540
|
-
<span className="loading loading-spinner loading-xs"></span>
|
|
3541
|
-
<span>Loading remotes...</span>
|
|
3542
|
-
</div>
|
|
3543
|
-
)}
|
|
3544
|
-
{remotesGroupExpanded && remoteBranchTrees && Array.from(remoteBranchTrees.entries()).map(([remoteName, tree]) => {
|
|
3545
|
-
const remoteGroupPath = `__remotes__/${remoteName}`;
|
|
3546
|
-
const isRemoteExpanded = expandedFolders.has(remoteGroupPath);
|
|
3547
|
-
|
|
3548
|
-
return (
|
|
3549
|
-
<div key={remoteName}>
|
|
3550
|
-
<ContextMenu
|
|
3551
|
-
items={[
|
|
3552
|
-
{ label: `Fetch from ${remoteName}`, onClick: () => handleFetchFromRemote(remoteName) },
|
|
3553
|
-
{ label: 'Rename', onClick: () => confirmRenameRemote(remoteName) },
|
|
3554
|
-
{
|
|
3555
|
-
label: 'Delete',
|
|
3556
|
-
onClick: () => confirmDeleteRemote(remoteName),
|
|
3557
|
-
danger: true,
|
|
3558
|
-
},
|
|
3559
|
-
]}
|
|
3560
|
-
>
|
|
3561
|
-
<GroupHeader
|
|
3562
|
-
name={remoteName}
|
|
3563
|
-
groupPath={remoteGroupPath}
|
|
3564
|
-
icon={<i className="iconoir-globe text-[14px] opacity-50" aria-hidden="true" />}
|
|
3565
|
-
isExpanded={isRemoteExpanded}
|
|
3566
|
-
onToggle={() => toggleFolder(remoteGroupPath)}
|
|
3567
|
-
visibilityMap={visibilityMap}
|
|
3568
|
-
onToggleVisibility={handleToggleVisibility}
|
|
3569
|
-
depth={1}
|
|
3570
|
-
/>
|
|
3571
|
-
</ContextMenu>
|
|
3572
|
-
{isRemoteExpanded && (
|
|
3573
|
-
<BranchTreeItem
|
|
3574
|
-
node={tree}
|
|
3575
|
-
currentBranch={branchData?.current}
|
|
3576
|
-
expandedFolders={expandedFolders}
|
|
3577
|
-
onToggleFolder={toggleFolder}
|
|
3578
|
-
onCheckout={handleCheckout}
|
|
3579
|
-
onCheckoutToLocal={confirmCheckoutToLocal}
|
|
3580
|
-
onCreateBranch={() => confirmCreateBranch()}
|
|
3581
|
-
onDeleteBranch={confirmDeleteBranch}
|
|
3582
|
-
onRenameBranch={confirmRenameBranch}
|
|
3583
|
-
onRenameRemoteBranch={confirmRenameRemoteBranch}
|
|
3584
|
-
onRebase={confirmRebase}
|
|
3585
|
-
onMerge={confirmMerge}
|
|
3586
|
-
onPushToRemote={confirmPushToRemote}
|
|
3587
|
-
onPullFromRemote={confirmPullFromRemote}
|
|
3588
|
-
getBranchContextMenuItems={getBranchContextMenuItems}
|
|
3589
|
-
onBranchClick={handleBranchClick}
|
|
3590
|
-
onBranchContextMenu={handleBranchContextMenu}
|
|
3591
|
-
onDeleteBranchGroup={confirmDeleteBranches}
|
|
3592
|
-
selectedBranches={selectedBranchSet}
|
|
3593
|
-
visibilityMap={visibilityMap}
|
|
3594
|
-
onToggleVisibility={handleToggleVisibility}
|
|
3595
|
-
depth={2}
|
|
3596
|
-
groupPath={remoteGroupPath}
|
|
3597
|
-
isRemote={true}
|
|
3598
|
-
trackingInfo={branchData?.trackingInfo}
|
|
3599
|
-
/>
|
|
3600
|
-
)}
|
|
3601
|
-
</div>
|
|
3602
|
-
);
|
|
3603
|
-
})}
|
|
3604
|
-
</>
|
|
3605
|
-
</div>
|
|
3606
|
-
</div>
|
|
3607
|
-
</div>
|
|
3608
|
-
);
|
|
3609
|
-
|
|
3610
|
-
if (isLoading && limit === 100) {
|
|
3611
|
-
return <div className="flex items-center justify-center p-8 h-full"><span className="loading loading-spinner text-base-content/50"></span></div>;
|
|
3612
|
-
}
|
|
3613
|
-
|
|
3614
|
-
if (isError) {
|
|
3615
|
-
return (
|
|
3616
|
-
<div className="flex items-center justify-center p-8 h-full flex-col gap-4">
|
|
3617
|
-
<p className="text-error font-medium">Error Loading History</p>
|
|
3618
|
-
<p className="text-sm opacity-70">{(error as Error)?.message || 'An unknown error occurred'}</p>
|
|
3619
|
-
<button onClick={() => refetch()} className="btn btn-outline btn-sm">
|
|
3620
|
-
<i className="iconoir-refresh-circle text-[16px] mr-1" aria-hidden="true" />
|
|
3621
|
-
Try Again
|
|
3622
|
-
</button>
|
|
3623
|
-
</div>
|
|
3624
|
-
);
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
if (!log) return <div className="flex items-center justify-center p-8 h-full opacity-70">No history data available</div>;
|
|
3628
|
-
|
|
3629
|
-
return (
|
|
3630
|
-
<div className="flex h-full overflow-hidden">
|
|
3631
|
-
{isResetOpen && (
|
|
3632
|
-
<dialog className="modal modal-open">
|
|
3633
|
-
<div className="modal-box">
|
|
3634
|
-
<h3 className="font-bold text-lg">Reset to Commit</h3>
|
|
3635
|
-
<p className="py-4 break-words">
|
|
3636
|
-
Are you sure you want to hard reset the current branch to commit <span className="font-mono bg-base-200 px-1 rounded">{resetCommitHash?.substring(0, 7)}</span>?
|
|
3637
|
-
<br/>
|
|
3638
|
-
<span className="text-error font-bold">Warning: This will discard all local changes and commits after this point. This action cannot be undone.</span>
|
|
3639
|
-
</p>
|
|
3640
|
-
<div className="modal-action">
|
|
3641
|
-
<button className="btn" onClick={() => setIsResetOpen(false)} disabled={isResetting}>Cancel</button>
|
|
3642
|
-
<button className="btn btn-error" onClick={handleConfirmReset} disabled={isResetting}>
|
|
3643
|
-
{isResetting && <span className="loading loading-spinner loading-xs"></span>}
|
|
3644
|
-
Reset
|
|
3645
|
-
</button>
|
|
3646
|
-
</div>
|
|
3647
|
-
</div>
|
|
3648
|
-
<form method="dialog" className="modal-backdrop">
|
|
3649
|
-
<button onClick={() => setIsResetOpen(false)}>close</button>
|
|
3650
|
-
</form>
|
|
3651
|
-
</dialog>
|
|
3652
|
-
)}
|
|
3653
|
-
|
|
3654
|
-
{isRevertOpen && (
|
|
3655
|
-
<dialog className="modal modal-open">
|
|
3656
|
-
<div className="modal-box">
|
|
3657
|
-
<h3 className="font-bold text-lg">Revert Commit</h3>
|
|
3658
|
-
<p className="py-4 break-words">
|
|
3659
|
-
Are you sure you want to revert commit <span className="font-mono bg-base-200 px-1 rounded">{commitToRevert?.hash.substring(0, 7)}</span> on <span className="font-bold break-all">{branchData?.current || 'current'}</span>?
|
|
3660
|
-
</p>
|
|
3661
|
-
{commitToRevert?.message && (
|
|
3662
|
-
<div className="rounded border border-base-300 bg-base-200/40 px-3 py-2 text-xs break-words">
|
|
3663
|
-
{commitToRevert.message}
|
|
3664
|
-
</div>
|
|
3665
|
-
)}
|
|
3666
|
-
<div className="modal-action">
|
|
3667
|
-
<button
|
|
3668
|
-
className="btn"
|
|
3669
|
-
onClick={() => {
|
|
3670
|
-
setIsRevertOpen(false);
|
|
3671
|
-
setCommitToRevert(null);
|
|
3672
|
-
}}
|
|
3673
|
-
disabled={isReverting}
|
|
3674
|
-
>
|
|
3675
|
-
Cancel
|
|
3676
|
-
</button>
|
|
3677
|
-
<button className="btn btn-primary" onClick={handleConfirmRevert} disabled={isReverting}>
|
|
3678
|
-
{isReverting && <span className="loading loading-spinner loading-xs"></span>}
|
|
3679
|
-
Revert
|
|
3680
|
-
</button>
|
|
3681
|
-
</div>
|
|
3682
|
-
</div>
|
|
3683
|
-
<form method="dialog" className="modal-backdrop">
|
|
3684
|
-
<button
|
|
3685
|
-
onClick={() => {
|
|
3686
|
-
setIsRevertOpen(false);
|
|
3687
|
-
setCommitToRevert(null);
|
|
3688
|
-
}}
|
|
3689
|
-
>
|
|
3690
|
-
close
|
|
3691
|
-
</button>
|
|
3692
|
-
</form>
|
|
3693
|
-
</dialog>
|
|
3694
|
-
)}
|
|
3695
|
-
|
|
3696
|
-
{isRewordOpen && (
|
|
3697
|
-
<dialog className="modal modal-open">
|
|
3698
|
-
<div className="modal-box">
|
|
3699
|
-
<h3 className="font-bold text-lg">Reword Commit</h3>
|
|
3700
|
-
<p className="py-4 break-words">
|
|
3701
|
-
Reword commit <span className="font-mono bg-base-200 px-1 rounded">{commitToReword?.hash.substring(0, 7)}</span> on branch <span className="font-bold">{commitToReword?.branch}</span>.
|
|
3702
|
-
</p>
|
|
3703
|
-
<input
|
|
3704
|
-
type="text"
|
|
3705
|
-
className="input input-bordered w-full font-mono text-sm mb-3"
|
|
3706
|
-
value={newMessageSubject}
|
|
3707
|
-
onChange={e => setNewMessageSubject(e.target.value)}
|
|
3708
|
-
autoFocus
|
|
3709
|
-
onKeyDown={e => {
|
|
3710
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && newMessageSubject.trim() && !isRewording) {
|
|
3711
|
-
e.preventDefault();
|
|
3712
|
-
handleReword();
|
|
3713
|
-
}
|
|
3714
|
-
}}
|
|
3715
|
-
placeholder="Commit subject"
|
|
3716
|
-
disabled={isRewording}
|
|
3717
|
-
/>
|
|
3718
|
-
<textarea
|
|
3719
|
-
className="textarea textarea-bordered w-full h-32 font-mono text-sm"
|
|
3720
|
-
value={newMessageBody}
|
|
3721
|
-
onChange={e => setNewMessageBody(e.target.value)}
|
|
3722
|
-
onKeyDown={e => {
|
|
3723
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && newMessageSubject.trim() && !isRewording) {
|
|
3724
|
-
e.preventDefault();
|
|
3725
|
-
handleReword();
|
|
3726
|
-
}
|
|
3727
|
-
}}
|
|
3728
|
-
placeholder="Commit message body (optional)"
|
|
3729
|
-
disabled={isRewording}
|
|
3730
|
-
/>
|
|
3731
|
-
{commitToReword?.branch !== branchData?.current && (
|
|
3732
|
-
<div className="alert alert-warning text-xs mt-2 py-2">
|
|
3733
|
-
<span>This will briefly checkout <b>{commitToReword?.branch}</b> to amend the commit.</span>
|
|
3734
|
-
</div>
|
|
3735
|
-
)}
|
|
3736
|
-
<div className="modal-action">
|
|
3737
|
-
<button className="btn" onClick={closeRewordDialog} disabled={isRewording}>Cancel</button>
|
|
3738
|
-
<button className="btn btn-primary" onClick={handleReword} disabled={!newMessageSubject.trim() || isRewording}>
|
|
3739
|
-
{isRewording && <span className="loading loading-spinner loading-xs"></span>}
|
|
3740
|
-
Reword
|
|
3741
|
-
</button>
|
|
3742
|
-
</div>
|
|
3743
|
-
</div>
|
|
3744
|
-
<form method="dialog" className="modal-backdrop">
|
|
3745
|
-
<button onClick={closeRewordDialog}>close</button>
|
|
3746
|
-
</form>
|
|
3747
|
-
</dialog>
|
|
3748
|
-
)}
|
|
3749
|
-
|
|
3750
|
-
{isDeleteOpen && (
|
|
3751
|
-
<dialog className="modal modal-open">
|
|
3752
|
-
<div className="modal-box">
|
|
3753
|
-
<h3 className="font-bold text-lg">{branchesToDelete.length > 1 ? 'Delete Branches' : 'Delete Branch'}</h3>
|
|
3754
|
-
{branchesToDelete.length > 1 ? (
|
|
3755
|
-
<div className="py-4 space-y-3">
|
|
3756
|
-
<p className="break-words">
|
|
3757
|
-
Are you sure you want to delete <span className="font-bold">{branchesToDelete.length} selected branches</span>?
|
|
3758
|
-
This action cannot be undone.
|
|
3759
|
-
</p>
|
|
3760
|
-
<div className="max-h-44 overflow-auto rounded border border-base-300 bg-base-200/40 p-2 space-y-1">
|
|
3761
|
-
{branchesToDelete.map((branch) => (
|
|
3762
|
-
<div key={branch} className="text-xs min-w-0 break-all">
|
|
3763
|
-
{branch.startsWith('remotes/') ? branch.slice('remotes/'.length) : branch}
|
|
3764
|
-
</div>
|
|
3765
|
-
))}
|
|
3766
|
-
</div>
|
|
3767
|
-
</div>
|
|
3768
|
-
) : (
|
|
3769
|
-
<p className="py-4 break-words">
|
|
3770
|
-
Are you sure you want to delete the branch <span className="font-bold break-all">{branchesToDelete[0]?.startsWith('remotes/') ? branchesToDelete[0].slice('remotes/'.length) : branchesToDelete[0]}</span>?
|
|
3771
|
-
This action cannot be undone.
|
|
3772
|
-
</p>
|
|
3773
|
-
)}
|
|
3774
|
-
{selectedTrackingUpstreams.length > 0 && (
|
|
3775
|
-
<div className="form-control">
|
|
3776
|
-
<label className="label cursor-pointer justify-start items-start gap-2 min-w-0">
|
|
3777
|
-
<input type="checkbox" className="checkbox checkbox-sm" checked={deleteRemoteBranch} onChange={(e) => setDeleteRemoteBranch(e.target.checked)} disabled={isDeleting} />
|
|
3778
|
-
<span className="label-text break-words whitespace-normal">
|
|
3779
|
-
{selectedTrackingUpstreams.length === 1 ? (
|
|
3780
|
-
<>
|
|
3781
|
-
Delete tracking remote branch <span className="font-mono opacity-70 break-all">{selectedTrackingUpstreams[0]}</span>
|
|
3782
|
-
</>
|
|
3783
|
-
) : (
|
|
3784
|
-
<>Delete {selectedTrackingUpstreams.length} tracking remote branches</>
|
|
3785
|
-
)}
|
|
3786
|
-
</span>
|
|
3787
|
-
</label>
|
|
3788
|
-
</div>
|
|
3789
|
-
)}
|
|
3790
|
-
<div className="modal-action">
|
|
3791
|
-
<button className="btn" onClick={() => { setIsDeleteOpen(false); setBranchesToDelete([]); setDeleteRemoteBranch(false); }} disabled={isDeleting}>Cancel</button>
|
|
3792
|
-
<button className="btn btn-error" onClick={handleDeleteBranch} disabled={isDeleting}>
|
|
3793
|
-
{isDeleting && <span className="loading loading-spinner loading-xs"></span>}
|
|
3794
|
-
Delete
|
|
3795
|
-
</button>
|
|
3796
|
-
</div>
|
|
3797
|
-
</div>
|
|
3798
|
-
<form method="dialog" className="modal-backdrop">
|
|
3799
|
-
<button onClick={() => { setIsDeleteOpen(false); setBranchesToDelete([]); setDeleteRemoteBranch(false); }}>close</button>
|
|
3800
|
-
</form>
|
|
3801
|
-
</dialog>
|
|
3802
|
-
)}
|
|
3803
|
-
|
|
3804
|
-
{isDeleteTagOpen && (
|
|
3805
|
-
<dialog className="modal modal-open">
|
|
3806
|
-
<div className="modal-box">
|
|
3807
|
-
<h3 className="font-bold text-lg">Delete Tag</h3>
|
|
3808
|
-
<p className="py-4 break-words">
|
|
3809
|
-
Are you sure you want to delete the tag <span className="font-bold break-all">{tagToDelete}</span>?
|
|
3810
|
-
This action cannot be undone.
|
|
3811
|
-
</p>
|
|
3812
|
-
<div className="form-control">
|
|
3813
|
-
<label className="label cursor-pointer justify-start items-start gap-2 min-w-0">
|
|
3814
|
-
<input
|
|
3815
|
-
type="checkbox"
|
|
3816
|
-
className="checkbox checkbox-sm"
|
|
3817
|
-
checked={deleteRemoteTag}
|
|
3818
|
-
onChange={(e) => setDeleteRemoteTag(e.target.checked)}
|
|
3819
|
-
disabled={isDeletingTag || !remoteNameForTagDelete}
|
|
3820
|
-
/>
|
|
3821
|
-
<span className="label-text break-words whitespace-normal">
|
|
3822
|
-
{remoteNameForTagDelete ? (
|
|
3823
|
-
<>
|
|
3824
|
-
Also delete from remote <span className="font-mono opacity-70">{remoteNameForTagDelete}</span>
|
|
3825
|
-
</>
|
|
3826
|
-
) : (
|
|
3827
|
-
'No remote configured'
|
|
3828
|
-
)}
|
|
3829
|
-
</span>
|
|
3830
|
-
</label>
|
|
3831
|
-
</div>
|
|
3832
|
-
<div className="modal-action">
|
|
3833
|
-
<button className="btn" onClick={closeDeleteTagDialog} disabled={isDeletingTag}>Cancel</button>
|
|
3834
|
-
<button className="btn btn-error" onClick={handleDeleteTag} disabled={isDeletingTag || !tagToDelete}>
|
|
3835
|
-
{isDeletingTag && <span className="loading loading-spinner loading-xs"></span>}
|
|
3836
|
-
Delete
|
|
3837
|
-
</button>
|
|
3838
|
-
</div>
|
|
3839
|
-
</div>
|
|
3840
|
-
<form method="dialog" className="modal-backdrop">
|
|
3841
|
-
<button onClick={closeDeleteTagDialog}>close</button>
|
|
3842
|
-
</form>
|
|
3843
|
-
</dialog>
|
|
3844
|
-
)}
|
|
3845
|
-
|
|
3846
|
-
{isCherryPickOpen && (
|
|
3847
|
-
<dialog className="modal modal-open">
|
|
3848
|
-
<div className="modal-box">
|
|
3849
|
-
<h3 className="font-bold text-lg">Cherry Pick</h3>
|
|
3850
|
-
<p className="text-sm opacity-70 mt-1">
|
|
3851
|
-
{commitsToCherryPick.length > 1
|
|
3852
|
-
? 'Apply selected commits from oldest to newest'
|
|
3853
|
-
: 'Apply changes from the selected commit'}
|
|
3854
|
-
</p>
|
|
3855
|
-
{commitsToCherryPick.length === 1 ? (
|
|
3856
|
-
<p className="py-4 break-words">
|
|
3857
|
-
Are you sure to apply <span className="font-bold font-mono break-all">{commitsToCherryPick[0]?.hash}</span> <span className="font-bold break-words">{commitsToCherryPick[0]?.message}</span> to <span className="font-bold break-all">{branchData?.current || 'current'}</span> branch?
|
|
3858
|
-
</p>
|
|
3859
|
-
) : (
|
|
3860
|
-
<div className="py-4 space-y-3">
|
|
3861
|
-
<p className="break-words">
|
|
3862
|
-
Are you sure to apply <span className="font-bold">{commitsToCherryPick.length} selected commits</span> to <span className="font-bold break-all">{branchData?.current || 'current'}</span> branch?
|
|
3863
|
-
</p>
|
|
3864
|
-
<div className="max-h-44 overflow-auto rounded border border-base-300 bg-base-200/40 p-2 space-y-1">
|
|
3865
|
-
{commitsToCherryPick.map((commit) => (
|
|
3866
|
-
<div key={commit.hash} className="text-xs min-w-0">
|
|
3867
|
-
<span className="font-mono opacity-70">{commit.hash.slice(0, 7)}</span>{' '}
|
|
3868
|
-
<span className="break-words">{commit.message}</span>
|
|
3869
|
-
</div>
|
|
3870
|
-
))}
|
|
3871
|
-
</div>
|
|
3872
|
-
</div>
|
|
3873
|
-
)}
|
|
3874
|
-
<div className="modal-action">
|
|
3875
|
-
<button
|
|
3876
|
-
className="btn"
|
|
3877
|
-
onClick={() => {
|
|
3878
|
-
setIsCherryPickOpen(false);
|
|
3879
|
-
setCommitsToCherryPick([]);
|
|
3880
|
-
}}
|
|
3881
|
-
disabled={isCherryPicking}
|
|
3882
|
-
>
|
|
3883
|
-
Cancel
|
|
3884
|
-
</button>
|
|
3885
|
-
<button className="btn btn-primary" onClick={handleCherryPickCommit} disabled={isCherryPicking}>
|
|
3886
|
-
{isCherryPicking && <span className="loading loading-spinner loading-xs"></span>}
|
|
3887
|
-
Confirm
|
|
3888
|
-
</button>
|
|
3889
|
-
</div>
|
|
3890
|
-
</div>
|
|
3891
|
-
<form method="dialog" className="modal-backdrop">
|
|
3892
|
-
<button
|
|
3893
|
-
onClick={() => {
|
|
3894
|
-
setIsCherryPickOpen(false);
|
|
3895
|
-
setCommitsToCherryPick([]);
|
|
3896
|
-
}}
|
|
3897
|
-
>
|
|
3898
|
-
close
|
|
3899
|
-
</button>
|
|
3900
|
-
</form>
|
|
3901
|
-
</dialog>
|
|
3902
|
-
)}
|
|
3903
|
-
|
|
3904
|
-
{isAbortCherryPickOpen && (
|
|
3905
|
-
<dialog className="modal modal-open">
|
|
3906
|
-
<div className="modal-box">
|
|
3907
|
-
<h3 className="font-bold text-lg">Cherry Pick In Progress</h3>
|
|
3908
|
-
<p className="text-sm opacity-70 mt-1">Another cherry-pick operation is currently in progress.</p>
|
|
3909
|
-
<p className="py-4 break-words">
|
|
3910
|
-
Abort the in-progress cherry-pick and continue with {commitsToCherryPick.length > 1 ? 'the selected commits' : 'this commit'}?
|
|
3911
|
-
</p>
|
|
3912
|
-
<div className="modal-action">
|
|
3913
|
-
<button
|
|
3914
|
-
className="btn"
|
|
3915
|
-
onClick={() => {
|
|
3916
|
-
setIsAbortCherryPickOpen(false);
|
|
3917
|
-
setCommitsToCherryPick([]);
|
|
3918
|
-
}}
|
|
3919
|
-
disabled={isAbortingCherryPick}
|
|
3920
|
-
>
|
|
3921
|
-
Cancel
|
|
3922
|
-
</button>
|
|
3923
|
-
<button
|
|
3924
|
-
className="btn btn-warning"
|
|
3925
|
-
onClick={() => void handleAbortCherryPick()}
|
|
3926
|
-
disabled={isAbortingCherryPick}
|
|
3927
|
-
>
|
|
3928
|
-
{isAbortingCherryPick && <span className="loading loading-spinner loading-xs"></span>}
|
|
3929
|
-
Abort and Continue
|
|
3930
|
-
</button>
|
|
3931
|
-
</div>
|
|
3932
|
-
</div>
|
|
3933
|
-
<form method="dialog" className="modal-backdrop">
|
|
3934
|
-
<button
|
|
3935
|
-
onClick={() => {
|
|
3936
|
-
setIsAbortCherryPickOpen(false);
|
|
3937
|
-
setCommitsToCherryPick([]);
|
|
3938
|
-
}}
|
|
3939
|
-
>
|
|
3940
|
-
close
|
|
3941
|
-
</button>
|
|
3942
|
-
</form>
|
|
3943
|
-
</dialog>
|
|
3944
|
-
)}
|
|
3945
|
-
|
|
3946
|
-
{isRenameOpen && (
|
|
3947
|
-
<dialog className="modal modal-open">
|
|
3948
|
-
<div className="modal-box">
|
|
3949
|
-
<h3 className="font-bold text-lg">{remoteBranchToRename ? 'Rename Remote Branch' : 'Rename Branch'}</h3>
|
|
3950
|
-
<p className="py-4 break-words">
|
|
3951
|
-
Enter a new name for the branch <span className="font-bold break-all">{branchToRename}</span>. Press <kbd className="kbd kbd-sm">Cmd</kbd>+<kbd className="kbd kbd-sm">Enter</kbd> to confirm.
|
|
3952
|
-
</p>
|
|
3953
|
-
<input
|
|
3954
|
-
type="text"
|
|
3955
|
-
className="input input-bordered w-full"
|
|
3956
|
-
value={newBranchNameForRename}
|
|
3957
|
-
onChange={e => setNewBranchNameForRename(sanitizeBranchName(e.target.value))}
|
|
3958
|
-
placeholder="New branch name"
|
|
3959
|
-
disabled={isRenaming}
|
|
3960
|
-
autoFocus
|
|
3961
|
-
onKeyDown={e => {
|
|
3962
|
-
const shortcutPressed = e.key === 'Enter' && (e.metaKey || e.ctrlKey);
|
|
3963
|
-
const sameName = remoteBranchToRename
|
|
3964
|
-
? newBranchNameForRename === remoteBranchToRename.branch
|
|
3965
|
-
: newBranchNameForRename === branchToRename;
|
|
3966
|
-
if (shortcutPressed && newBranchNameForRename && !sameName && !isRenaming) {
|
|
3967
|
-
handleRenameBranch();
|
|
3968
|
-
}
|
|
3969
|
-
}}
|
|
3970
|
-
/>
|
|
3971
|
-
{!remoteBranchToRename && trackingInfoForRename?.upstream && (
|
|
3972
|
-
<div className="form-control mt-2">
|
|
3973
|
-
<label className="label cursor-pointer justify-start items-start gap-2 min-w-0">
|
|
3974
|
-
<input
|
|
3975
|
-
type="checkbox"
|
|
3976
|
-
className="checkbox checkbox-sm"
|
|
3977
|
-
checked={renameTrackingRemoteBranch}
|
|
3978
|
-
onChange={(e) => setRenameTrackingRemoteBranch(e.target.checked)}
|
|
3979
|
-
disabled={isRenaming}
|
|
3980
|
-
/>
|
|
3981
|
-
<span className="label-text break-words whitespace-normal">
|
|
3982
|
-
Also rename tracking remote branch <span className="font-mono opacity-70 break-all">{trackingInfoForRename.upstream}</span>
|
|
3983
|
-
</span>
|
|
3984
|
-
</label>
|
|
3985
|
-
</div>
|
|
3986
|
-
)}
|
|
3987
|
-
<div className="modal-action">
|
|
3988
|
-
<button
|
|
3989
|
-
className="btn"
|
|
3990
|
-
onClick={closeRenameBranchDialog}
|
|
3991
|
-
disabled={isRenaming}
|
|
3992
|
-
>
|
|
3993
|
-
Cancel
|
|
3994
|
-
</button>
|
|
3995
|
-
<button
|
|
3996
|
-
className="btn btn-primary"
|
|
3997
|
-
onClick={handleRenameBranch}
|
|
3998
|
-
disabled={
|
|
3999
|
-
!newBranchNameForRename ||
|
|
4000
|
-
(remoteBranchToRename
|
|
4001
|
-
? newBranchNameForRename === remoteBranchToRename.branch
|
|
4002
|
-
: newBranchNameForRename === branchToRename) ||
|
|
4003
|
-
isRenaming
|
|
4004
|
-
}
|
|
4005
|
-
>
|
|
4006
|
-
{isRenaming && <span className="loading loading-spinner loading-xs"></span>}
|
|
4007
|
-
Rename
|
|
4008
|
-
</button>
|
|
4009
|
-
</div>
|
|
4010
|
-
</div>
|
|
4011
|
-
<form method="dialog" className="modal-backdrop">
|
|
4012
|
-
<button onClick={closeRenameBranchDialog}>
|
|
4013
|
-
close
|
|
4014
|
-
</button>
|
|
4015
|
-
</form>
|
|
4016
|
-
</dialog>
|
|
4017
|
-
)}
|
|
4018
|
-
|
|
4019
|
-
{isRenameRemoteOpen && (
|
|
4020
|
-
<dialog className="modal modal-open">
|
|
4021
|
-
<div className="modal-box">
|
|
4022
|
-
<h3 className="font-bold text-lg">Rename Remote</h3>
|
|
4023
|
-
<p className="py-4 break-words">
|
|
4024
|
-
Enter a new name for remote <span className="font-bold break-all">{remoteToRename}</span>. Press <kbd className="kbd kbd-sm">Cmd</kbd>+<kbd className="kbd kbd-sm">Enter</kbd> to confirm.
|
|
4025
|
-
</p>
|
|
4026
|
-
<input
|
|
4027
|
-
type="text"
|
|
4028
|
-
className="input input-bordered w-full"
|
|
4029
|
-
value={newRemoteNameForRename}
|
|
4030
|
-
onChange={(e) => setNewRemoteNameForRename(e.target.value)}
|
|
4031
|
-
placeholder="New remote name"
|
|
4032
|
-
disabled={isRenamingRemote}
|
|
4033
|
-
autoFocus
|
|
4034
|
-
onKeyDown={(e) => {
|
|
4035
|
-
const shortcutPressed = e.key === 'Enter' && (e.metaKey || e.ctrlKey);
|
|
4036
|
-
const trimmedNewName = newRemoteNameForRename.trim();
|
|
4037
|
-
const sameName = trimmedNewName === (remoteToRename ?? '').trim();
|
|
4038
|
-
if (shortcutPressed && trimmedNewName && !sameName && !isRenamingRemote) {
|
|
4039
|
-
handleRenameRemote();
|
|
4040
|
-
}
|
|
4041
|
-
}}
|
|
4042
|
-
/>
|
|
4043
|
-
<div className="modal-action">
|
|
4044
|
-
<button
|
|
4045
|
-
className="btn"
|
|
4046
|
-
onClick={closeRenameRemoteDialog}
|
|
4047
|
-
disabled={isRenamingRemote}
|
|
4048
|
-
>
|
|
4049
|
-
Cancel
|
|
4050
|
-
</button>
|
|
4051
|
-
<button
|
|
4052
|
-
className="btn btn-primary"
|
|
4053
|
-
onClick={handleRenameRemote}
|
|
4054
|
-
disabled={
|
|
4055
|
-
!newRemoteNameForRename.trim() ||
|
|
4056
|
-
newRemoteNameForRename.trim() === (remoteToRename ?? '').trim() ||
|
|
4057
|
-
isRenamingRemote
|
|
4058
|
-
}
|
|
4059
|
-
>
|
|
4060
|
-
{isRenamingRemote && <span className="loading loading-spinner loading-xs"></span>}
|
|
4061
|
-
Rename
|
|
4062
|
-
</button>
|
|
4063
|
-
</div>
|
|
4064
|
-
</div>
|
|
4065
|
-
<form method="dialog" className="modal-backdrop">
|
|
4066
|
-
<button onClick={closeRenameRemoteDialog}>
|
|
4067
|
-
close
|
|
4068
|
-
</button>
|
|
4069
|
-
</form>
|
|
4070
|
-
</dialog>
|
|
4071
|
-
)}
|
|
4072
|
-
|
|
4073
|
-
{isDeleteRemoteOpen && (
|
|
4074
|
-
<dialog className="modal modal-open">
|
|
4075
|
-
<div className="modal-box">
|
|
4076
|
-
<h3 className="font-bold text-lg">Delete Remote</h3>
|
|
4077
|
-
<p className="py-4 break-words">
|
|
4078
|
-
Are you sure you want to delete remote <span className="font-bold break-all">{remoteToDelete}</span>?
|
|
4079
|
-
This removes all tracking branches for that remote.
|
|
4080
|
-
</p>
|
|
4081
|
-
<div className="modal-action">
|
|
4082
|
-
<button
|
|
4083
|
-
className="btn"
|
|
4084
|
-
onClick={closeDeleteRemoteDialog}
|
|
4085
|
-
disabled={isDeletingRemote}
|
|
4086
|
-
>
|
|
4087
|
-
Cancel
|
|
4088
|
-
</button>
|
|
4089
|
-
<button
|
|
4090
|
-
className="btn btn-error"
|
|
4091
|
-
onClick={handleDeleteRemote}
|
|
4092
|
-
disabled={!remoteToDelete || isDeletingRemote}
|
|
4093
|
-
>
|
|
4094
|
-
{isDeletingRemote && <span className="loading loading-spinner loading-xs"></span>}
|
|
4095
|
-
Delete
|
|
4096
|
-
</button>
|
|
4097
|
-
</div>
|
|
4098
|
-
</div>
|
|
4099
|
-
<form method="dialog" className="modal-backdrop">
|
|
4100
|
-
<button onClick={closeDeleteRemoteDialog}>
|
|
4101
|
-
close
|
|
4102
|
-
</button>
|
|
4103
|
-
</form>
|
|
4104
|
-
</dialog>
|
|
4105
|
-
)}
|
|
4106
|
-
|
|
4107
|
-
{isAddRemoteOpen && (
|
|
4108
|
-
<dialog className="modal modal-open">
|
|
4109
|
-
<div className="modal-box">
|
|
4110
|
-
<h3 className="font-bold text-lg">Add Remote</h3>
|
|
4111
|
-
<p className="py-4 break-words">
|
|
4112
|
-
Add a new remote by providing a name and URL. Press <kbd className="kbd kbd-sm">Cmd</kbd>+<kbd className="kbd kbd-sm">Enter</kbd> to confirm.
|
|
4113
|
-
</p>
|
|
4114
|
-
<div className="space-y-3">
|
|
4115
|
-
<div>
|
|
4116
|
-
<label className="label pt-0">
|
|
4117
|
-
<span className="label-text">Remote name</span>
|
|
4118
|
-
</label>
|
|
4119
|
-
<input
|
|
4120
|
-
type="text"
|
|
4121
|
-
className="input input-bordered w-full"
|
|
4122
|
-
value={newRemoteName}
|
|
4123
|
-
onChange={(e) => setNewRemoteName(e.target.value)}
|
|
4124
|
-
placeholder="origin"
|
|
4125
|
-
disabled={isAddingRemote}
|
|
4126
|
-
autoFocus
|
|
4127
|
-
/>
|
|
4128
|
-
</div>
|
|
4129
|
-
<div>
|
|
4130
|
-
<label className="label pt-0">
|
|
4131
|
-
<span className="label-text">Remote URL</span>
|
|
4132
|
-
</label>
|
|
4133
|
-
<input
|
|
4134
|
-
type="text"
|
|
4135
|
-
className="input input-bordered w-full"
|
|
4136
|
-
value={newRemoteUrl}
|
|
4137
|
-
onChange={(e) => setNewRemoteUrl(e.target.value)}
|
|
4138
|
-
placeholder="https://github.com/owner/repo.git"
|
|
4139
|
-
disabled={isAddingRemote}
|
|
4140
|
-
onKeyDown={(e) => {
|
|
4141
|
-
const shortcutPressed = e.key === 'Enter' && (e.metaKey || e.ctrlKey);
|
|
4142
|
-
if (shortcutPressed && newRemoteName.trim() && newRemoteUrl.trim() && !isAddingRemote) {
|
|
4143
|
-
handleAddRemote();
|
|
4144
|
-
}
|
|
4145
|
-
}}
|
|
4146
|
-
/>
|
|
4147
|
-
</div>
|
|
4148
|
-
</div>
|
|
4149
|
-
<div className="modal-action">
|
|
4150
|
-
<button
|
|
4151
|
-
className="btn"
|
|
4152
|
-
onClick={closeAddRemoteDialog}
|
|
4153
|
-
disabled={isAddingRemote}
|
|
4154
|
-
>
|
|
4155
|
-
Cancel
|
|
4156
|
-
</button>
|
|
4157
|
-
<button
|
|
4158
|
-
className="btn btn-primary"
|
|
4159
|
-
onClick={handleAddRemote}
|
|
4160
|
-
disabled={!newRemoteName.trim() || !newRemoteUrl.trim() || isAddingRemote}
|
|
4161
|
-
>
|
|
4162
|
-
{isAddingRemote && <span className="loading loading-spinner loading-xs"></span>}
|
|
4163
|
-
Add
|
|
4164
|
-
</button>
|
|
4165
|
-
</div>
|
|
4166
|
-
</div>
|
|
4167
|
-
<form method="dialog" className="modal-backdrop">
|
|
4168
|
-
<button onClick={closeAddRemoteDialog}>
|
|
4169
|
-
close
|
|
4170
|
-
</button>
|
|
4171
|
-
</form>
|
|
4172
|
-
</dialog>
|
|
4173
|
-
)}
|
|
4174
|
-
|
|
4175
|
-
{isRebaseOpen && (
|
|
4176
|
-
<dialog className="modal modal-open">
|
|
4177
|
-
<div className="modal-box">
|
|
4178
|
-
<h3 className="font-bold text-lg">Rebase</h3>
|
|
4179
|
-
<p className="py-4 break-words">
|
|
4180
|
-
Copy commits from one branch to another.<br/>
|
|
4181
|
-
Are you sure to rebase <span className="font-bold break-all">{rebaseSourceBranch}</span> onto <span className="font-bold break-all">{rebaseTargetBranch}</span>?
|
|
4182
|
-
</p>
|
|
4183
|
-
<div className="form-control">
|
|
4184
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4185
|
-
<input type="checkbox" className="checkbox checkbox-sm" checked={rebaseStashChanges} onChange={(e) => setRebaseStashChanges(e.target.checked)} disabled={isRebasing} />
|
|
4186
|
-
<span className="label-text">Stash and reapply local changes</span>
|
|
4187
|
-
</label>
|
|
4188
|
-
</div>
|
|
4189
|
-
{!rebaseStashChanges && (
|
|
4190
|
-
<p className="text-xs text-warning mt-2 ml-6">
|
|
4191
|
-
Warning: All local changes will be discarded.
|
|
4192
|
-
</p>
|
|
4193
|
-
)}
|
|
4194
|
-
{rebaseConflictStatus === 'checking' ? (
|
|
4195
|
-
<div className="alert alert-info text-sm mt-4 py-2">
|
|
4196
|
-
<span className="loading loading-spinner loading-xs"></span>
|
|
4197
|
-
<span>Checking conflicts for rebasing <span className="font-bold break-all">{rebaseSourceBranch}</span> onto <span className="font-bold break-all">{rebaseTargetBranch}</span>...</span>
|
|
4198
|
-
</div>
|
|
4199
|
-
) : rebaseConflictStatus === 'no-conflict' ? (
|
|
4200
|
-
<div className="alert alert-success text-sm mt-4 py-2">
|
|
4201
|
-
<i className="iconoir-check-circle-solid text-[18px]" aria-hidden="true" />
|
|
4202
|
-
<span>No conflict: rebasing <span className="font-bold break-all">{rebaseSourceBranch}</span> onto <span className="font-bold break-all">{rebaseTargetBranch}</span> will not cause conflicts.</span>
|
|
4203
|
-
</div>
|
|
4204
|
-
) : (
|
|
4205
|
-
<div className="alert alert-warning text-sm mt-4 py-2">
|
|
4206
|
-
<i className="iconoir-warning-circle-solid text-[18px]" aria-hidden="true" />
|
|
4207
|
-
<span>Conflicts detected: rebasing <span className="font-bold break-all">{rebaseSourceBranch}</span> onto <span className="font-bold break-all">{rebaseTargetBranch}</span> will cause conflicts.</span>
|
|
4208
|
-
</div>
|
|
4209
|
-
)}
|
|
4210
|
-
<div className="modal-action">
|
|
4211
|
-
<button className="btn" onClick={closeRebaseDialog} disabled={isRebasing}>Cancel</button>
|
|
4212
|
-
<button className="btn btn-primary" onClick={handleRebase} disabled={isRebasing}>
|
|
4213
|
-
{isRebasing && <span className="loading loading-spinner loading-xs"></span>}
|
|
4214
|
-
Confirm
|
|
4215
|
-
</button>
|
|
4216
|
-
</div>
|
|
4217
|
-
</div>
|
|
4218
|
-
<form method="dialog" className="modal-backdrop">
|
|
4219
|
-
<button onClick={closeRebaseDialog}>close</button>
|
|
4220
|
-
</form>
|
|
4221
|
-
</dialog>
|
|
4222
|
-
)}
|
|
4223
|
-
|
|
4224
|
-
{isMergeOpen && (
|
|
4225
|
-
<dialog className="modal modal-open">
|
|
4226
|
-
<div className="modal-box">
|
|
4227
|
-
<h3 className="font-bold text-lg">Merge</h3>
|
|
4228
|
-
<p className="py-4 break-words">
|
|
4229
|
-
Merge branch into another one.<br/>
|
|
4230
|
-
Are you sure to merge <span className="font-bold break-all">{mergeSourceBranch}</span> into <span className="font-bold break-all">{mergeTargetBranch}</span>?
|
|
4231
|
-
</p>
|
|
4232
|
-
<div className="form-control">
|
|
4233
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4234
|
-
<input type="checkbox" className="checkbox checkbox-sm" checked={mergeRebaseBeforeMerge} onChange={(e) => setMergeRebaseBeforeMerge(e.target.checked)} disabled={isMerging} />
|
|
4235
|
-
<span className="label-text">Rebase before merge</span>
|
|
4236
|
-
</label>
|
|
4237
|
-
</div>
|
|
4238
|
-
<div className="form-control">
|
|
4239
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4240
|
-
<input type="checkbox" className="checkbox checkbox-sm" checked={mergeSquash} onChange={(e) => handleMergeSquashToggle(e.target.checked)} disabled={isMerging} />
|
|
4241
|
-
<span className="label-text">Squash before merge</span>
|
|
4242
|
-
</label>
|
|
4243
|
-
</div>
|
|
4244
|
-
{mergeSquash && (
|
|
4245
|
-
<textarea
|
|
4246
|
-
className="textarea textarea-bordered w-full mt-2"
|
|
4247
|
-
placeholder="Commit message for squash merge"
|
|
4248
|
-
value={mergeSquashMessage}
|
|
4249
|
-
onChange={(e) => setMergeSquashMessage(e.target.value)}
|
|
4250
|
-
disabled={isMerging}
|
|
4251
|
-
autoFocus
|
|
4252
|
-
/>
|
|
4253
|
-
)}
|
|
4254
|
-
<div className="form-control">
|
|
4255
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4256
|
-
<input type="checkbox" className="checkbox checkbox-sm" checked={mergeFastForward} onChange={(e) => setMergeFastForward(e.target.checked)} disabled={isMerging} />
|
|
4257
|
-
<span className="label-text">Fast forward merge</span>
|
|
4258
|
-
</label>
|
|
4259
|
-
</div>
|
|
4260
|
-
{mergeConflictStatus === 'checking' ? (
|
|
4261
|
-
<div className="alert alert-info text-sm mt-4 py-2">
|
|
4262
|
-
<span className="loading loading-spinner loading-xs"></span>
|
|
4263
|
-
<span>Checking conflicts for merging <span className="font-bold break-all">{mergeSourceBranch}</span> into <span className="font-bold break-all">{mergeTargetBranch}</span>...</span>
|
|
4264
|
-
</div>
|
|
4265
|
-
) : mergeConflictStatus === 'no-conflict' ? (
|
|
4266
|
-
<div className="alert alert-success text-sm mt-4 py-2">
|
|
4267
|
-
<i className="iconoir-check-circle-solid text-[18px]" aria-hidden="true" />
|
|
4268
|
-
<span>No conflict: merging <span className="font-bold break-all">{mergeSourceBranch}</span> into <span className="font-bold break-all">{mergeTargetBranch}</span> will not cause conflicts.</span>
|
|
4269
|
-
</div>
|
|
4270
|
-
) : (
|
|
4271
|
-
<div className="alert alert-warning text-sm mt-4 py-2">
|
|
4272
|
-
<i className="iconoir-warning-circle-solid text-[18px]" aria-hidden="true" />
|
|
4273
|
-
<span>Conflicts detected: merging <span className="font-bold break-all">{mergeSourceBranch}</span> into <span className="font-bold break-all">{mergeTargetBranch}</span> will cause conflicts.</span>
|
|
4274
|
-
</div>
|
|
4275
|
-
)}
|
|
4276
|
-
<div className="modal-action">
|
|
4277
|
-
<button className="btn" onClick={closeMergeDialog} disabled={isMerging}>Cancel</button>
|
|
4278
|
-
<button className="btn btn-primary" onClick={handleMerge} disabled={isMerging}>
|
|
4279
|
-
{isMerging && <span className="loading loading-spinner loading-xs"></span>}
|
|
4280
|
-
Confirm
|
|
4281
|
-
</button>
|
|
4282
|
-
</div>
|
|
4283
|
-
</div>
|
|
4284
|
-
<form method="dialog" className="modal-backdrop">
|
|
4285
|
-
<button onClick={closeMergeDialog}>close</button>
|
|
4286
|
-
</form>
|
|
4287
|
-
</dialog>
|
|
4288
|
-
)}
|
|
4289
|
-
|
|
4290
|
-
{isPushOpen && (
|
|
4291
|
-
<dialog className="modal modal-open">
|
|
4292
|
-
<div className="modal-box">
|
|
4293
|
-
<h3 className="font-bold text-lg">Push to Remote</h3>
|
|
4294
|
-
<p className="py-4 break-words">Push <span className="font-bold break-all">{pushBranch}</span> to a remote repository.</p>
|
|
4295
|
-
|
|
4296
|
-
{pushError && pushRemotes.length === 0 ? (
|
|
4297
|
-
<div className="alert alert-error">
|
|
4298
|
-
<span className="text-xl">⚠️</span>
|
|
4299
|
-
<span>{pushError}</span>
|
|
4300
|
-
</div>
|
|
4301
|
-
) : pushLoadingRemotes ? (
|
|
4302
|
-
<div className="flex justify-center py-8">
|
|
4303
|
-
<span className="loading loading-spinner loading-lg"></span>
|
|
4304
|
-
</div>
|
|
4305
|
-
) : (
|
|
4306
|
-
<div className="flex flex-col gap-4">
|
|
4307
|
-
<div className="form-control w-full flex flex-row items-center justify-between gap-4">
|
|
4308
|
-
<label className="label flex-shrink-0"><span className="label-text">Remote Repository</span></label>
|
|
4309
|
-
<select className="select select-bordered w-64" value={pushSelectedRemote} onChange={(e) => handlePushRemoteChange(e.target.value)} disabled={isPushing}>
|
|
4310
|
-
{pushRemotes.map((remote) => <option key={remote} value={remote}>{remote}</option>)}
|
|
4311
|
-
</select>
|
|
4312
|
-
</div>
|
|
4313
|
-
|
|
4314
|
-
<div className="form-control w-full flex flex-row items-center justify-between gap-4">
|
|
4315
|
-
<label className="label flex-shrink-0"><span className="label-text">Remote Branch</span></label>
|
|
4316
|
-
<div className="flex flex-col items-end gap-1 w-64">
|
|
4317
|
-
{pushLoadingBranches ? (
|
|
4318
|
-
<div className="flex items-center gap-2 p-3 border rounded-lg bg-base-200 opacity-70 w-full">
|
|
4319
|
-
<span className="loading loading-spinner loading-xs"></span> Loading branches...
|
|
4320
|
-
</div>
|
|
4321
|
-
) : (
|
|
4322
|
-
<select className="select select-bordered w-full" value={pushSelectedRemoteBranch} onChange={(e) => setPushSelectedRemoteBranch(e.target.value)} disabled={isPushing}>
|
|
4323
|
-
{pushBranch && !pushRemoteBranches.includes(pushBranch) && <option value={pushBranch}>{pushBranch} (new)</option>}
|
|
4324
|
-
{pushRemoteBranches.map((branch) => <option key={branch} value={branch}>{branch}{pushTrackingBranch?.remote === pushSelectedRemote && pushTrackingBranch?.branch === branch ? ' (tracking)' : ''}</option>)}
|
|
4325
|
-
</select>
|
|
4326
|
-
)}
|
|
4327
|
-
{pushSelectedRemoteBranch && !pushRemoteBranches.includes(pushSelectedRemoteBranch) && (
|
|
4328
|
-
<div className="label"><span className="label-text-alt text-warning">New branch will be created</span></div>
|
|
4329
|
-
)}
|
|
4330
|
-
</div>
|
|
4331
|
-
</div>
|
|
4332
|
-
|
|
4333
|
-
<div className="form-control">
|
|
4334
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4335
|
-
<input type="checkbox" className="checkbox checkbox-sm" checked={pushRebaseFirst} onChange={(e) => setPushRebaseFirst(e.target.checked)} disabled={isPushing || pushForcePush} />
|
|
4336
|
-
<span className="label-text">Rebase onto remote branch before pushing</span>
|
|
4337
|
-
</label>
|
|
4338
|
-
</div>
|
|
4339
|
-
|
|
4340
|
-
<div className="form-control">
|
|
4341
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4342
|
-
<input type="checkbox" className="checkbox checkbox-sm checkbox-error" checked={pushForcePush} onChange={(e) => setPushForcePush(e.target.checked)} disabled={isPushing} />
|
|
4343
|
-
<span className="label-text text-error">Force push</span>
|
|
4344
|
-
</label>
|
|
4345
|
-
</div>
|
|
4346
|
-
|
|
4347
|
-
<div className="form-control">
|
|
4348
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4349
|
-
<input type="checkbox" className="checkbox checkbox-sm" checked={pushLocalOnlyTags} onChange={(e) => setPushLocalOnlyTags(e.target.checked)} disabled={isPushing} />
|
|
4350
|
-
<span className="label-text">Push all tags</span>
|
|
4351
|
-
</label>
|
|
4352
|
-
</div>
|
|
4353
|
-
|
|
4354
|
-
<div className="form-control">
|
|
4355
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4356
|
-
<input type="checkbox" className="checkbox checkbox-sm" checked={pushSquash} onChange={(e) => handlePushSquashToggle(e.target.checked)} disabled={isPushing} />
|
|
4357
|
-
<span className="label-text">Squash local commits before push</span>
|
|
4358
|
-
</label>
|
|
4359
|
-
</div>
|
|
4360
|
-
{pushSquash && (
|
|
4361
|
-
<textarea className="textarea textarea-bordered w-full" placeholder="Commit message for squash" value={pushSquashMessage} onChange={(e) => setPushSquashMessage(e.target.value)} disabled={isPushing} autoFocus />
|
|
4362
|
-
)}
|
|
4363
|
-
|
|
4364
|
-
{pushError && (
|
|
4365
|
-
<div className="alert alert-error text-sm">
|
|
4366
|
-
<span>{pushError}</span>
|
|
4367
|
-
</div>
|
|
4368
|
-
)}
|
|
4369
|
-
</div>
|
|
4370
|
-
)}
|
|
4371
|
-
|
|
4372
|
-
<div className="modal-action">
|
|
4373
|
-
<button className="btn" onClick={() => setIsPushOpen(false)} disabled={isPushing}>Cancel</button>
|
|
4374
|
-
{pushRemotes.length > 0 && (
|
|
4375
|
-
<button className="btn btn-primary" onClick={handlePushToRemote} disabled={isPushing || !pushSelectedRemote || !pushSelectedRemoteBranch}>
|
|
4376
|
-
{isPushing && <span className="loading loading-spinner loading-xs"></span>} Push
|
|
4377
|
-
</button>
|
|
4378
|
-
)}
|
|
4379
|
-
</div>
|
|
4380
|
-
</div>
|
|
4381
|
-
<form method="dialog" className="modal-backdrop">
|
|
4382
|
-
<button onClick={() => setIsPushOpen(false)}>close</button>
|
|
4383
|
-
</form>
|
|
4384
|
-
</dialog>
|
|
4385
|
-
)}
|
|
4386
|
-
|
|
4387
|
-
{isPullOpen && (
|
|
4388
|
-
<dialog className="modal modal-open">
|
|
4389
|
-
<div className="modal-box">
|
|
4390
|
-
<h3 className="font-bold text-lg">Pull from Remote</h3>
|
|
4391
|
-
<p className="py-4 break-words">Pull changes from a remote branch into <span className="font-bold break-all">{pullBranch}</span>.</p>
|
|
4392
|
-
|
|
4393
|
-
{pullError && pullRemotes.length === 0 ? (
|
|
4394
|
-
<div className="alert alert-error"><span>{pullError}</span></div>
|
|
4395
|
-
) : pullLoadingRemotes ? (
|
|
4396
|
-
<div className="flex justify-center py-8"><span className="loading loading-spinner loading-lg"></span></div>
|
|
4397
|
-
) : (
|
|
4398
|
-
<div className="flex flex-col gap-4">
|
|
4399
|
-
<div className="form-control w-full flex flex-row items-center justify-between gap-4">
|
|
4400
|
-
<label className="label flex-shrink-0"><span className="label-text">Remote Repository</span></label>
|
|
4401
|
-
<select className="select select-bordered w-64" value={pullSelectedRemote} onChange={(e) => handlePullRemoteChange(e.target.value)} disabled={isPulling}>
|
|
4402
|
-
{pullRemotes.map((remote) => <option key={remote} value={remote}>{remote}</option>)}
|
|
4403
|
-
</select>
|
|
4404
|
-
</div>
|
|
4405
|
-
|
|
4406
|
-
<div className="form-control w-full flex flex-row items-center justify-between gap-4">
|
|
4407
|
-
<label className="label flex-shrink-0"><span className="label-text">Remote Branch</span></label>
|
|
4408
|
-
{pullLoadingBranches ? (
|
|
4409
|
-
<div className="flex items-center gap-2 p-3 border rounded-lg bg-base-200 opacity-70 w-64">
|
|
4410
|
-
<span className="loading loading-spinner loading-xs"></span> Loading branches...
|
|
4411
|
-
</div>
|
|
4412
|
-
) : (
|
|
4413
|
-
<select className="select select-bordered w-64" value={pullSelectedRemoteBranch} onChange={(e) => setPullSelectedRemoteBranch(e.target.value)} disabled={isPulling}>
|
|
4414
|
-
{pullRemoteBranches.map((branch) => <option key={branch} value={branch}>{branch}{pullTrackingBranch?.remote === pullSelectedRemote && pullTrackingBranch?.branch === branch ? ' (tracking)' : ''}</option>)}
|
|
4415
|
-
</select>
|
|
4416
|
-
)}
|
|
4417
|
-
</div>
|
|
4418
|
-
|
|
4419
|
-
<div className="form-control">
|
|
4420
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4421
|
-
<input type="checkbox" className="checkbox checkbox-sm" checked={pullRebase} onChange={(e) => setPullRebase(e.target.checked)} disabled={isPulling} />
|
|
4422
|
-
<span className="label-text">Rebase onto remote branch</span>
|
|
4423
|
-
</label>
|
|
4424
|
-
</div>
|
|
4425
|
-
|
|
4426
|
-
{pullError && <div className="alert alert-error text-sm"><span>{pullError}</span></div>}
|
|
4427
|
-
</div>
|
|
4428
|
-
)}
|
|
4429
|
-
|
|
4430
|
-
<div className="modal-action">
|
|
4431
|
-
<button className="btn" onClick={() => setIsPullOpen(false)} disabled={isPulling}>Cancel</button>
|
|
4432
|
-
{pullRemotes.length > 0 && (
|
|
4433
|
-
<button className="btn btn-primary" onClick={handlePullFromRemote} disabled={isPulling || !pullSelectedRemote || !pullSelectedRemoteBranch}>
|
|
4434
|
-
{isPulling && <span className="loading loading-spinner loading-xs"></span>} Pull
|
|
4435
|
-
</button>
|
|
4436
|
-
)}
|
|
4437
|
-
</div>
|
|
4438
|
-
</div>
|
|
4439
|
-
<form method="dialog" className="modal-backdrop">
|
|
4440
|
-
<button onClick={() => setIsPullOpen(false)}>close</button>
|
|
4441
|
-
</form>
|
|
4442
|
-
</dialog>
|
|
4443
|
-
)}
|
|
4444
|
-
|
|
4445
|
-
{isCreateTagOpen && (
|
|
4446
|
-
<dialog className="modal modal-open">
|
|
4447
|
-
<div className="modal-box">
|
|
4448
|
-
<h3 className="font-bold text-lg">Create Tag</h3>
|
|
4449
|
-
<p className="py-4 break-words">
|
|
4450
|
-
Create a new tag at commit <span className="font-mono bg-base-200 px-1 rounded">{createTagCommitHash?.substring(0, 7)}</span>.
|
|
4451
|
-
</p>
|
|
4452
|
-
<input
|
|
4453
|
-
type="text"
|
|
4454
|
-
className="input input-bordered w-full font-mono"
|
|
4455
|
-
value={newTagName}
|
|
4456
|
-
onChange={(e) => setNewTagName(e.target.value)}
|
|
4457
|
-
placeholder="Tag name"
|
|
4458
|
-
disabled={isCreatingTag}
|
|
4459
|
-
autoFocus
|
|
4460
|
-
onKeyDown={(e) => {
|
|
4461
|
-
if (e.key === 'Enter' && newTagName.trim() && !isCreatingTag) {
|
|
4462
|
-
handleCreateTag();
|
|
4463
|
-
}
|
|
4464
|
-
}}
|
|
4465
|
-
/>
|
|
4466
|
-
<div className="form-control mt-3">
|
|
4467
|
-
<label className="label cursor-pointer justify-start gap-2">
|
|
4468
|
-
<input
|
|
4469
|
-
type="checkbox"
|
|
4470
|
-
className="checkbox checkbox-sm"
|
|
4471
|
-
checked={pushTagToRemote}
|
|
4472
|
-
onChange={(e) => setPushTagToRemote(e.target.checked)}
|
|
4473
|
-
disabled={isCreatingTag}
|
|
4474
|
-
/>
|
|
4475
|
-
<span className="label-text">Push tag to remote after creation</span>
|
|
4476
|
-
</label>
|
|
4477
|
-
</div>
|
|
4478
|
-
<div className="modal-action">
|
|
4479
|
-
<button
|
|
4480
|
-
className="btn"
|
|
4481
|
-
onClick={() => {
|
|
4482
|
-
setIsCreateTagOpen(false);
|
|
4483
|
-
setCreateTagCommitHash(null);
|
|
4484
|
-
setNewTagName('');
|
|
4485
|
-
setPushTagToRemote(false);
|
|
4486
|
-
}}
|
|
4487
|
-
disabled={isCreatingTag}
|
|
4488
|
-
>
|
|
4489
|
-
Cancel
|
|
4490
|
-
</button>
|
|
4491
|
-
<button className="btn btn-primary" onClick={handleCreateTag} disabled={!newTagName.trim() || isCreatingTag}>
|
|
4492
|
-
{isCreatingTag && <span className="loading loading-spinner loading-xs"></span>}
|
|
4493
|
-
Create
|
|
4494
|
-
</button>
|
|
4495
|
-
</div>
|
|
4496
|
-
</div>
|
|
4497
|
-
<form method="dialog" className="modal-backdrop">
|
|
4498
|
-
<button
|
|
4499
|
-
onClick={() => {
|
|
4500
|
-
setIsCreateTagOpen(false);
|
|
4501
|
-
setCreateTagCommitHash(null);
|
|
4502
|
-
setNewTagName('');
|
|
4503
|
-
setPushTagToRemote(false);
|
|
4504
|
-
}}
|
|
4505
|
-
>
|
|
4506
|
-
close
|
|
4507
|
-
</button>
|
|
4508
|
-
</form>
|
|
4509
|
-
</dialog>
|
|
4510
|
-
)}
|
|
4511
|
-
|
|
4512
|
-
{iscreateBranchOpen && (
|
|
4513
|
-
<dialog className="modal modal-open">
|
|
4514
|
-
<div className="modal-box">
|
|
4515
|
-
<h3 className="font-bold text-lg">Create New Branch</h3>
|
|
4516
|
-
<p className="py-4 break-words">
|
|
4517
|
-
Create a new branch from{' '}
|
|
4518
|
-
<span className="font-bold break-all">
|
|
4519
|
-
{createBranchFromRef ? createBranchFromRef.replace(/^remotes\//, '') : 'current HEAD'}
|
|
4520
|
-
</span>.
|
|
4521
|
-
</p>
|
|
4522
|
-
<input
|
|
4523
|
-
type="text"
|
|
4524
|
-
className="input input-bordered w-full"
|
|
4525
|
-
value={newBranchName}
|
|
4526
|
-
onChange={e => setNewBranchName(sanitizeBranchName(e.target.value))}
|
|
4527
|
-
placeholder="Branch name"
|
|
4528
|
-
disabled={isCreating}
|
|
4529
|
-
autoFocus
|
|
4530
|
-
onKeyDown={e => {
|
|
4531
|
-
if (e.key === 'Enter' && newBranchName && !isCreating) {
|
|
4532
|
-
handleCreateBranch();
|
|
4533
|
-
}
|
|
4534
|
-
}}
|
|
4535
|
-
/>
|
|
4536
|
-
<div className="modal-action">
|
|
4537
|
-
<button
|
|
4538
|
-
className="btn"
|
|
4539
|
-
onClick={() => {
|
|
4540
|
-
setIsCreateBranchOpen(false);
|
|
4541
|
-
setCreateBranchFromRef(null);
|
|
4542
|
-
}}
|
|
4543
|
-
disabled={isCreating}
|
|
4544
|
-
>
|
|
4545
|
-
Cancel
|
|
4546
|
-
</button>
|
|
4547
|
-
<button className="btn btn-primary" onClick={handleCreateBranch} disabled={!newBranchName || isCreating}>
|
|
4548
|
-
{isCreating && <span className="loading loading-spinner loading-xs"></span>} Create & Checkout
|
|
4549
|
-
</button>
|
|
4550
|
-
</div>
|
|
4551
|
-
</div>
|
|
4552
|
-
<form method="dialog" className="modal-backdrop">
|
|
4553
|
-
<button
|
|
4554
|
-
onClick={() => {
|
|
4555
|
-
setIsCreateBranchOpen(false);
|
|
4556
|
-
setCreateBranchFromRef(null);
|
|
4557
|
-
}}
|
|
4558
|
-
>
|
|
4559
|
-
close
|
|
4560
|
-
</button>
|
|
4561
|
-
</form>
|
|
4562
|
-
</dialog>
|
|
4563
|
-
)}
|
|
4564
|
-
|
|
4565
|
-
{isCheckoutToLocalOpen && (
|
|
4566
|
-
<dialog className="modal modal-open">
|
|
4567
|
-
<div className="modal-box">
|
|
4568
|
-
<h3 className="font-bold text-lg">Checkout to Local Branch</h3>
|
|
4569
|
-
<p className="py-4 break-words">Create a local branch from <span className="font-bold break-all">{checkoutRemoteBranch?.replace(/^remotes\//, '')}</span> and set up tracking.</p>
|
|
4570
|
-
<div className="form-control w-full">
|
|
4571
|
-
<label className="label"><span className="label-text">Local Branch Name</span></label>
|
|
4572
|
-
<input
|
|
4573
|
-
type="text"
|
|
4574
|
-
className="input input-bordered w-full"
|
|
4575
|
-
value={checkoutLocalBranchName}
|
|
4576
|
-
onChange={e => setCheckoutLocalBranchName(sanitizeBranchName(e.target.value))}
|
|
4577
|
-
placeholder="Local branch name"
|
|
4578
|
-
disabled={isCheckingOutToLocal}
|
|
4579
|
-
autoFocus
|
|
4580
|
-
onKeyDown={e => {
|
|
4581
|
-
if (e.key === 'Enter' && checkoutLocalBranchName && !isCheckingOutToLocal) {
|
|
4582
|
-
handleCheckoutToLocal();
|
|
4583
|
-
}
|
|
4584
|
-
}}
|
|
4585
|
-
/>
|
|
4586
|
-
</div>
|
|
4587
|
-
<div className="modal-action">
|
|
4588
|
-
<button className="btn" onClick={() => setIsCheckoutToLocalOpen(false)} disabled={isCheckingOutToLocal}>Cancel</button>
|
|
4589
|
-
<button className="btn btn-primary" onClick={handleCheckoutToLocal} disabled={!checkoutLocalBranchName || isCheckingOutToLocal}>
|
|
4590
|
-
{isCheckingOutToLocal && <span className="loading loading-spinner loading-xs"></span>} Checkout
|
|
4591
|
-
</button>
|
|
4592
|
-
</div>
|
|
4593
|
-
</div>
|
|
4594
|
-
<form method="dialog" className="modal-backdrop">
|
|
4595
|
-
<button onClick={() => setIsCheckoutToLocalOpen(false)}>close</button>
|
|
4596
|
-
</form>
|
|
4597
|
-
</dialog>
|
|
4598
|
-
)}
|
|
4599
|
-
|
|
4600
|
-
{scriptExecution.isOpen && (
|
|
4601
|
-
<dialog className="modal modal-open">
|
|
4602
|
-
<div className="modal-box max-w-4xl">
|
|
4603
|
-
<div className="flex items-start justify-between gap-4">
|
|
4604
|
-
<div className="min-w-0">
|
|
4605
|
-
<h3 className="font-bold text-lg truncate">Custom Script: {scriptExecution.scriptName}</h3>
|
|
4606
|
-
<p className="text-xs opacity-70 mt-1 break-all">
|
|
4607
|
-
Branch: {scriptExecution.branchRef}
|
|
4608
|
-
</p>
|
|
4609
|
-
</div>
|
|
4610
|
-
<span
|
|
4611
|
-
className={cn(
|
|
4612
|
-
"badge badge-sm shrink-0",
|
|
4613
|
-
scriptExecution.status === 'running' || scriptExecution.status === 'starting'
|
|
4614
|
-
? 'badge-info'
|
|
4615
|
-
: scriptExecution.status === 'completed'
|
|
4616
|
-
? 'badge-success'
|
|
4617
|
-
: scriptExecution.status === 'failed'
|
|
4618
|
-
? 'badge-error'
|
|
4619
|
-
: scriptExecution.status === 'canceled'
|
|
4620
|
-
? 'badge-warning'
|
|
4621
|
-
: 'badge-ghost'
|
|
4622
|
-
)}
|
|
4623
|
-
>
|
|
4624
|
-
{scriptExecution.status}
|
|
4625
|
-
</span>
|
|
4626
|
-
</div>
|
|
4627
|
-
|
|
4628
|
-
<div className="mt-4 border border-base-300 rounded bg-base-200/40">
|
|
4629
|
-
<pre className="p-3 font-mono text-xs overflow-auto max-h-[50vh] whitespace-pre-wrap break-words">{scriptExecution.output || 'Waiting for output...'}</pre>
|
|
4630
|
-
</div>
|
|
4631
|
-
|
|
4632
|
-
{scriptExecution.error && (
|
|
4633
|
-
<div className="alert alert-error py-2 mt-3">
|
|
4634
|
-
<span>{scriptExecution.error}</span>
|
|
4635
|
-
</div>
|
|
4636
|
-
)}
|
|
4637
|
-
|
|
4638
|
-
<div className="modal-action">
|
|
4639
|
-
<button
|
|
4640
|
-
className="btn btn-warning"
|
|
4641
|
-
onClick={() => void handleCancelCustomScriptExecution()}
|
|
4642
|
-
disabled={!isScriptExecutionRunning || isCancelingScriptExecution}
|
|
4643
|
-
>
|
|
4644
|
-
{isCancelingScriptExecution && <span className="loading loading-spinner loading-xs"></span>}
|
|
4645
|
-
Cancel
|
|
4646
|
-
</button>
|
|
4647
|
-
<button
|
|
4648
|
-
className="btn btn-outline"
|
|
4649
|
-
onClick={() => void handleCopyCustomScriptOutput()}
|
|
4650
|
-
disabled={isCopyingScriptOutput}
|
|
4651
|
-
>
|
|
4652
|
-
{isCopyingScriptOutput && <span className="loading loading-spinner loading-xs"></span>}
|
|
4653
|
-
{didCopyScriptOutput ? 'Copied' : 'Copy'}
|
|
4654
|
-
</button>
|
|
4655
|
-
<button
|
|
4656
|
-
className="btn btn-primary"
|
|
4657
|
-
onClick={() => {
|
|
4658
|
-
setScriptExecution(DEFAULT_SCRIPT_EXECUTION);
|
|
4659
|
-
setDidCopyScriptOutput(false);
|
|
4660
|
-
setIsCancelingScriptExecution(false);
|
|
4661
|
-
}}
|
|
4662
|
-
disabled={!isScriptExecutionFinished}
|
|
4663
|
-
>
|
|
4664
|
-
Done
|
|
4665
|
-
</button>
|
|
4666
|
-
</div>
|
|
4667
|
-
</div>
|
|
4668
|
-
<form method="dialog" className="modal-backdrop">
|
|
4669
|
-
<button
|
|
4670
|
-
onClick={(e) => {
|
|
4671
|
-
if (!isScriptExecutionFinished) {
|
|
4672
|
-
e.preventDefault();
|
|
4673
|
-
return;
|
|
4674
|
-
}
|
|
4675
|
-
setScriptExecution(DEFAULT_SCRIPT_EXECUTION);
|
|
4676
|
-
setDidCopyScriptOutput(false);
|
|
4677
|
-
setIsCancelingScriptExecution(false);
|
|
4678
|
-
}}
|
|
4679
|
-
>
|
|
4680
|
-
close
|
|
4681
|
-
</button>
|
|
4682
|
-
</form>
|
|
4683
|
-
</dialog>
|
|
4684
|
-
)}
|
|
4685
|
-
|
|
4686
|
-
{/* Main Content */}
|
|
4687
|
-
<div className="flex-1 flex flex-col min-w-0 bg-base-100">
|
|
4688
|
-
<div className="h-[57px] flex items-center px-6 border-b border-base-300 shrink-0">
|
|
4689
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
4690
|
-
<h1 className="font-bold text-lg">History</h1>
|
|
4691
|
-
<div className="relative" ref={branchPopoverRef}>
|
|
4692
|
-
<button
|
|
4693
|
-
className="btn btn-sm gap-2 max-w-[24rem]"
|
|
4694
|
-
onClick={() => setIsBranchPopoverOpen(prev => !prev)}
|
|
4695
|
-
title={currentBranchLabel}
|
|
4696
|
-
>
|
|
4697
|
-
<span className="truncate">{currentBranchLabel}</span>
|
|
4698
|
-
<i className={cn("iconoir-nav-arrow-down text-[16px] shrink-0 transition-transform", isBranchPopoverOpen && "rotate-180")} aria-hidden="true" />
|
|
4699
|
-
</button>
|
|
4700
|
-
{isBranchPopoverOpen && (
|
|
4701
|
-
<div className="absolute left-0 top-full mt-2 z-50">
|
|
4702
|
-
{branchTreePopoverContent}
|
|
4703
|
-
</div>
|
|
4704
|
-
)}
|
|
4705
|
-
</div>
|
|
4706
|
-
<div className="flex items-center gap-2 shrink-0">
|
|
4707
|
-
<button
|
|
4708
|
-
className="btn btn-sm gap-2"
|
|
4709
|
-
onClick={() => void handleFetchFromAllRemotes()}
|
|
4710
|
-
disabled={isFetchingAllRemotes || isPullOpen || isPushOpen}
|
|
4711
|
-
title="Fetch latest changes from all remotes"
|
|
4712
|
-
>
|
|
4713
|
-
{isFetchingAllRemotes ? (
|
|
4714
|
-
<span className="loading loading-spinner loading-xs"></span>
|
|
4715
|
-
) : (
|
|
4716
|
-
<i className="iconoir-refresh text-[16px]" aria-hidden="true" />
|
|
4717
|
-
)}
|
|
4718
|
-
Fetch
|
|
4719
|
-
</button>
|
|
4720
|
-
<button
|
|
4721
|
-
className="btn btn-sm gap-2"
|
|
4722
|
-
onClick={confirmPullCurrentBranch}
|
|
4723
|
-
disabled={!!pullActionDisabledReason || isPullOpen || isPushOpen}
|
|
4724
|
-
title={pullActionDisabledReason || `Pull from ${currentTrackingBranch?.upstream}`}
|
|
4725
|
-
>
|
|
4726
|
-
{pullLoadingRemotes ? (
|
|
4727
|
-
<span className="loading loading-spinner loading-xs"></span>
|
|
4728
|
-
) : (
|
|
4729
|
-
<i className="iconoir-arrow-down text-[16px]" aria-hidden="true" />
|
|
4730
|
-
)}
|
|
4731
|
-
Pull
|
|
4732
|
-
</button>
|
|
4733
|
-
<button
|
|
4734
|
-
className="btn btn-sm gap-2"
|
|
4735
|
-
onClick={confirmPushCurrentBranch}
|
|
4736
|
-
disabled={!!pushActionDisabledReason || isPullOpen || isPushOpen}
|
|
4737
|
-
title={pushActionDisabledReason || (currentTrackingBranch ? `Push to ${currentTrackingBranch.upstream}` : 'Push current branch to remote')}
|
|
4738
|
-
>
|
|
4739
|
-
{pushLoadingRemotes ? (
|
|
4740
|
-
<span className="loading loading-spinner loading-xs"></span>
|
|
4741
|
-
) : (
|
|
4742
|
-
<i className="iconoir-arrow-up text-[16px]" aria-hidden="true" />
|
|
4743
|
-
)}
|
|
4744
|
-
Push
|
|
4745
|
-
</button>
|
|
4746
|
-
</div>
|
|
4747
|
-
</div>
|
|
4748
|
-
</div>
|
|
4749
|
-
|
|
4750
|
-
<div className="flex-1 overflow-hidden relative">
|
|
4751
|
-
{/* Show loading spinner while branches are loading if visibility filters are set */}
|
|
4752
|
-
{hasVisibilityFilters && isBranchesLoading ? (
|
|
4753
|
-
<div className="flex items-center justify-center h-full">
|
|
4754
|
-
<span className="loading loading-spinner loading-lg opacity-50"></span>
|
|
4755
|
-
</div>
|
|
4756
|
-
) : (
|
|
4757
|
-
<GitGraph
|
|
4758
|
-
ref={gitGraphRef}
|
|
4759
|
-
commits={filteredCommits}
|
|
4760
|
-
selectedHash={selectedHash || undefined}
|
|
4761
|
-
selectedHashes={selectedCommitHashSet}
|
|
4762
|
-
onSelectCommit={handleSelectCommit}
|
|
4763
|
-
onResetToCommit={handleResetToCommit}
|
|
4764
|
-
onRevertCommit={confirmRevertCommit}
|
|
4765
|
-
onCreateTag={confirmCreateTag}
|
|
4766
|
-
onCherryPickCommit={confirmCherryPickCommit}
|
|
4767
|
-
onCherryPickSelectedCommits={confirmCherryPickSelectedCommits}
|
|
4768
|
-
onRewordCommit={confirmRewordCommit}
|
|
4769
|
-
localBranches={branchData?.branches || []}
|
|
4770
|
-
trackingInfo={branchData?.trackingInfo}
|
|
4771
|
-
onEndReached={() => {
|
|
4772
|
-
if (!isFetching && log.all.length >= limit) {
|
|
4773
|
-
setLimit(l => l + 50);
|
|
4774
|
-
}
|
|
4775
|
-
}}
|
|
4776
|
-
isLoadingMore={isFetching && limit > 100}
|
|
4777
|
-
currentBranch={branchData?.current}
|
|
4778
|
-
hiddenBranches={hiddenBranches}
|
|
4779
|
-
getBranchTagContextMenuItems={getBranchTagContextMenuItems}
|
|
4780
|
-
/>
|
|
4781
|
-
)}
|
|
4782
|
-
</div>
|
|
4783
|
-
|
|
4784
|
-
{selectedHash && (
|
|
4785
|
-
<div
|
|
4786
|
-
className="flex flex-col overflow-hidden border-t border-base-300 bg-base-200/30"
|
|
4787
|
-
style={{ height: panelHeight }}
|
|
4788
|
-
>
|
|
4789
|
-
{/* Resize handle */}
|
|
4790
|
-
<div
|
|
4791
|
-
className={cn(
|
|
4792
|
-
"h-1.5 cursor-ns-resize flex items-center justify-center hover:bg-base-200 transition-colors group shrink-0",
|
|
4793
|
-
isResizing && "bg-base-200"
|
|
4794
|
-
)}
|
|
4795
|
-
onMouseDown={handleResizeStart}
|
|
4796
|
-
>
|
|
4797
|
-
<div className="w-8 h-1 rounded-full bg-base-300 group-hover:bg-base-content/20 transition-colors" />
|
|
4798
|
-
</div>
|
|
4799
|
-
|
|
4800
|
-
{/* Header with commit info */}
|
|
4801
|
-
<div className="flex flex-row items-center py-2 px-4 border-b border-base-300 bg-base-100 shrink-0 justify-between gap-4">
|
|
4802
|
-
<div className="flex items-center gap-4 flex-1 min-w-0">
|
|
4803
|
-
<span className="text-sm font-bold truncate">
|
|
4804
|
-
{log.all.find(c => c.hash === selectedHash)?.message}
|
|
4805
|
-
</span>
|
|
4806
|
-
<span className="text-xs font-mono opacity-50 shrink-0">
|
|
4807
|
-
{selectedHash.substring(0, 7)}
|
|
4808
|
-
</span>
|
|
4809
|
-
</div>
|
|
4810
|
-
<div className="flex items-center gap-1 shrink-0">
|
|
4811
|
-
<button
|
|
4812
|
-
className="ml-2 btn btn-ghost btn-xs btn-square"
|
|
4813
|
-
onClick={() => selectSingleCommit(null)}
|
|
4814
|
-
title="Close"
|
|
4815
|
-
>
|
|
4816
|
-
✕
|
|
4817
|
-
</button>
|
|
4818
|
-
</div>
|
|
4819
|
-
</div>
|
|
4820
|
-
|
|
4821
|
-
{/* Combined commit message and changes content */}
|
|
4822
|
-
<div
|
|
4823
|
-
ref={commitDetailsContentRef}
|
|
4824
|
-
className="flex-1 overflow-hidden bg-base-100 grid"
|
|
4825
|
-
style={{
|
|
4826
|
-
gridTemplateRows: `${commitDetailsMessageRatio}fr 6px ${1 - commitDetailsMessageRatio}fr`,
|
|
4827
|
-
}}
|
|
4828
|
-
>
|
|
4829
|
-
<div className="border-b border-base-300 bg-base-100 min-h-0 flex flex-col">
|
|
4830
|
-
<div className="px-4 pt-3 pb-1 text-[10px] uppercase tracking-wider font-bold opacity-60">
|
|
4831
|
-
Message
|
|
4832
|
-
</div>
|
|
4833
|
-
<div className="px-4 pb-3 overflow-auto flex-1 min-h-0">
|
|
4834
|
-
<div className="text-xs opacity-70 whitespace-pre-wrap font-mono">
|
|
4835
|
-
{log.all.find(c => c.hash === selectedHash)?.body || 'No additional message'}
|
|
4836
|
-
</div>
|
|
4837
|
-
</div>
|
|
4838
|
-
</div>
|
|
4839
|
-
<div
|
|
4840
|
-
className={cn(
|
|
4841
|
-
'cursor-ns-resize flex items-center justify-center hover:bg-base-200/60 transition-colors',
|
|
4842
|
-
isCommitDetailsRatioResizing && 'bg-base-200/60'
|
|
4843
|
-
)}
|
|
4844
|
-
onMouseDown={handleCommitDetailsRatioResizeStart}
|
|
4845
|
-
>
|
|
4846
|
-
<div className="w-8 h-1 rounded-full bg-base-300" />
|
|
4847
|
-
</div>
|
|
4848
|
-
<div className="flex-1 overflow-hidden min-h-0 flex flex-col">
|
|
4849
|
-
<div className="px-4 pt-2 pb-1 text-[10px] uppercase tracking-wider font-bold opacity-60 border-b border-base-300 bg-base-100 shrink-0">
|
|
4850
|
-
Changes
|
|
4851
|
-
</div>
|
|
4852
|
-
<div className="flex-1 min-h-0">
|
|
4853
|
-
<CommitChangesView repoPath={repoPath} commitHash={selectedHash} />
|
|
4854
|
-
</div>
|
|
4855
|
-
</div>
|
|
4856
|
-
</div>
|
|
4857
|
-
</div>
|
|
4858
|
-
)}
|
|
4859
|
-
</div>
|
|
4860
|
-
</div>
|
|
4861
|
-
);
|
|
4862
|
-
}
|