import { useGit } from '@yaakapp-internal/git'; import type { WorkspaceMeta } from '@yaakapp-internal/models'; import classNames from 'classnames'; import type { HTMLAttributes } from 'react'; import { forwardRef } from 'react'; import { openWorkspaceSettings } from '../commands/openWorkspaceSettings'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useKeyValue } from '../hooks/useKeyValue'; import { useWorkspaceMeta } from '../hooks/useWorkspaceMeta'; import { sync } from '../init/sync'; import { showConfirm, showConfirmDelete } from '../lib/confirm'; import { showDialog } from '../lib/dialog'; import { showPrompt } from '../lib/prompt'; import { showErrorToast, showToast } from '../lib/toast'; import { Banner } from './core/Banner'; import type { DropdownItem } from './core/Dropdown'; import { Dropdown } from './core/Dropdown'; import { Icon } from './core/Icon'; import { InlineCode } from './core/InlineCode'; import { BranchSelectionDialog } from './git/BranchSelectionDialog'; import { HistoryDialog } from './git/HistoryDialog'; import { GitCommitDialog } from './GitCommitDialog'; export function GitDropdown() { const workspaceMeta = useWorkspaceMeta(); if (workspaceMeta == null) return null; if (workspaceMeta.settingSyncDir == null) { return ; } return ; } function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { const workspace = useActiveWorkspace(); const [ { status, log }, { branch, deleteBranch, fetchAll, mergeBranch, push, pull, checkout, init }, ] = useGit(syncDir); const localBranches = status.data?.localBranches ?? []; const remoteBranches = status.data?.remoteBranches ?? []; const remoteOnlyBranches = remoteBranches.filter( (b) => !localBranches.includes(b.replace(/^origin\//, '')), ); const currentBranch = status.data?.headRefShorthand ?? 'UNKNOWN'; if (workspace == null) { return null; } const noRepo = status.error?.includes('not found'); if (noRepo) { return ( init.mutate({ dir: syncDir })} /> ); } const tryCheckout = (branch: string, force: boolean) => { checkout.mutate( { branch, force }, { async onError(err) { if (!force) { // Checkout failed so ask user if they want to force it const forceCheckout = await showConfirm({ id: 'git-force-checkout', title: 'Conflicts Detected', description: 'Your branch has conflicts. Either make a commit or force checkout to discard changes.', confirmText: 'Force Checkout', color: 'warning', }); if (forceCheckout) { tryCheckout(branch, true); } } else { // Checkout failed showErrorToast('git-checkout-error', String(err)); } }, async onSuccess(branchName) { showToast({ id: 'git-checkout-success', message: ( <> Switched branch {branchName} ), color: 'success', }); await sync({ force: true }); }, }, ); }; const items: DropdownItem[] = [ { label: 'View History', hidden: (log.data ?? []).length === 0, leftSlot: , onSelect: async () => { showDialog({ id: 'git-history', size: 'md', title: 'Commit History', render: () => , }); }, }, { label: 'New Branch', leftSlot: , async onSelect() { const name = await showPrompt({ id: 'git-branch-name', title: 'Create Branch', label: 'Branch Name', }); if (name) { await branch.mutateAsync( { branch: name }, { onError: (err) => { showErrorToast('git-branch-error', String(err)); }, }, ); tryCheckout(name, false); } }, }, { label: 'Merge Branch', leftSlot: , hidden: localBranches.length <= 1, async onSelect() { showDialog({ id: 'git-merge', title: 'Merge Branch', size: 'sm', description: ( <> Select a branch to merge into {currentBranch} ), render: ({ hide }) => ( b !== currentBranch)} onCancel={hide} onSelect={async (branch) => { await mergeBranch.mutateAsync( { branch, force: false }, { onSettled: hide, onSuccess() { showToast({ id: 'git-merged-branch', message: ( <> Merged {branch} into{' '} {currentBranch} ), }); sync({ force: true }); }, onError(err) { showErrorToast('git-merged-branch-error', String(err)); }, }, ); }} /> ), }); }, }, { label: 'Delete Branch', leftSlot: , hidden: localBranches.length <= 1, color: 'danger', async onSelect() { if (currentBranch == null) return; const confirmed = await showConfirmDelete({ id: 'git-delete-branch', title: 'Delete Branch', description: ( <> Permanently delete {currentBranch}? ), }); if (confirmed) { await deleteBranch.mutateAsync( { branch: currentBranch }, { onError(err) { showErrorToast('git-delete-branch-error', String(err)); }, async onSuccess() { await sync({ force: true }); }, }, ); } }, }, { type: 'separator' }, { label: 'Push', hidden: (status.data?.origins ?? []).length === 0, leftSlot: , waitForOnSelect: true, async onSelect() { push.mutate(undefined, { onSuccess(message) { if (message === 'nothing_to_push') { showToast({ id: 'push-success', message: 'Nothing to push', color: 'info' }); } else { showToast({ id: 'push-success', message: 'Push successful', color: 'success' }); } }, onError(err) { showErrorToast('git-pull-error', String(err)); }, }); }, }, { label: 'Pull', hidden: (status.data?.origins ?? []).length === 0, leftSlot: , waitForOnSelect: true, async onSelect() { const result = await pull.mutateAsync(undefined, { onError(err) { showErrorToast('git-pull-error', String(err)); }, }); if (result.receivedObjects > 0) { showToast({ id: 'git-pull-success', message: `Pulled ${result.receivedObjects} objects`, color: 'success', }); await sync({ force: true }); } else { showToast({ id: 'git-pull-success', message: 'Already up to date', color: 'info' }); } }, }, { label: 'Commit', leftSlot: , onSelect() { showDialog({ id: 'commit', title: 'Commit Changes', size: 'full', className: '!max-h-[min(80vh,40rem)] !max-w-[min(50rem,90vw)]', render: ({ hide }) => ( ), }); }, }, { type: 'separator', label: 'Branches', hidden: localBranches.length < 1 }, ...localBranches.map((branch) => { const isCurrent = currentBranch === branch; return { label: branch, leftSlot: , onSelect: isCurrent ? undefined : () => tryCheckout(branch, false), }; }), ...remoteOnlyBranches.map((branch) => { const isCurrent = currentBranch === branch; return { label: branch, leftSlot: , onSelect: isCurrent ? undefined : () => tryCheckout(branch, false), }; }), ]; return ( {currentBranch} ); } const GitMenuButton = forwardRef>( function GitMenuButton({ className, ...props }: HTMLAttributes, ref) { return (