Allow multiple merges (#510)
* Allow multiple merge for a single task attempt Merge more than once (vibe-kanban 618829fc) When creating a PR, new changes can be pushed after creation. We need merge to work the same way, when changes have been made after the first merge, a second one should work. Commit changes from coding agent for task attempt 548ff450-df77-47b2-a5ba-c88d0aa4a334 Merge more than once (vibe-kanban 618829fc) When creating a PR, new changes can be pushed after creation. We need merge to work the same way, when changes have been made after the first merge, a second one should work. Remove pinned todo list (vibe-kanban cc66cda2) Make a minimal change to remove the pinned todo list from the frontend Remove pinned todo list (vibe-kanban cc66cda2) Make a minimal change to remove the pinned todo list from the frontend * Create merges table; remove task_attempt.merge_commit Add merge model, replace ta.merge_commit with m.merge_commit Fix frontend * Move PR to merges table * Refactor GitHub repository info retrieval to return structured data * Fix frontend * Reset task branch after PR merge Add branch status handling to TaskDetailsProvider and related components fmt Add branch status handling to TaskDetailsProvider and related components fmt Test (vibe-kanban 1bf1a80f) add test.txt Show merged diff when no worktree present Refresh branch status after PR creation Test (vibe-kanban 1bf1a80f) add test.txt Test (vibe-kanban 1bf1a80f) add test.txt Show rebase when behind Refactor container service to check if the container is clean before showing merged diff; remove unused BranchStatus import Test (vibe-kanban a3c1b297) add test.txt Refactor branch status handling: rename BranchStatusResponse to BranchStatus and update related types and usages Test (vibe-kanban) (#540) * Remove test.txt * Test (vibe-kanban aade357e) add test.txt * test.txt removed. * Fix diff when merged and new commits have been made * Remvoe logging (vibe-kanban) (#541) * Test (vibe-kanban aade357e) add test.txt * Test (vibe-kanban aade357e) add test.txt * Perfect! I've successfully removed the "Fetching branch status" logging statement from the code. The logging has been removed from `crates/server/src/routes/task_attempts.rs:568-571`. * Clear previous errors on successful PR creation, push, merge, and rebase actions * Show branch in worktree dirty error message * Add success indicators for push and merge actions in CurrentAttempt * Refactor status display logic in CurrentAttempt for improved readability and maintainability * Add target_branch_name to merge models and queries for direct and PR merges * Enhance merge status display logic in CurrentAttempt for better clarity on direct merges * Remove unnecessary condition check in attempt data fetching interval * Clippy * Add index for task_attempt_id in merges table to improve query performance * Pass PR creation error * Disable buttons (vibe-kanban 240346bf) Instead of not showing the merge/pr buttons when theyre not available we should disable them. frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx
This commit is contained in:
31
frontend/package-lock.json
generated
31
frontend/package-lock.json
generated
@@ -46,7 +46,8 @@
|
||||
"react-window": "^1.8.11",
|
||||
"rfc6902": "^5.1.2",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
@@ -8035,6 +8036,34 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
EditorType,
|
||||
TaskAttempt,
|
||||
TaskWithAttemptStatus,
|
||||
BranchStatus,
|
||||
} from 'shared/types';
|
||||
import { attemptsApi, executionProcessesApi } from '@/lib/api.ts';
|
||||
import {
|
||||
@@ -52,6 +53,7 @@ const TaskDetailsProvider: FC<{
|
||||
processes: [],
|
||||
runningProcessDetails: {},
|
||||
});
|
||||
const [branchStatus, setBranchStatus] = useState<BranchStatus | null>(null);
|
||||
|
||||
const handleOpenInEditor = useCallback(
|
||||
async (editorType?: EditorType) => {
|
||||
@@ -111,6 +113,15 @@ const TaskDetailsProvider: FC<{
|
||||
return newData;
|
||||
});
|
||||
}
|
||||
|
||||
// Also fetch branch status as part of attempt data
|
||||
try {
|
||||
const branchResult = await attemptsApi.getBranchStatus(attemptId);
|
||||
setBranchStatus(branchResult);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch branch status:', err);
|
||||
setBranchStatus(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch attempt data:', err);
|
||||
}
|
||||
@@ -165,8 +176,6 @@ const TaskDetailsProvider: FC<{
|
||||
}, [attemptData.processes, selectedAttempt?.profile, profiles]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAttemptRunning || !task) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (selectedAttempt) {
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
@@ -176,6 +185,26 @@ const TaskDetailsProvider: FC<{
|
||||
return () => clearInterval(interval);
|
||||
}, [isAttemptRunning, task, selectedAttempt, fetchAttemptData]);
|
||||
|
||||
// Fetch branch status when selected attempt changes
|
||||
useEffect(() => {
|
||||
if (!selectedAttempt) {
|
||||
setBranchStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchBranchStatus = async () => {
|
||||
try {
|
||||
const result = await attemptsApi.getBranchStatus(selectedAttempt.id);
|
||||
setBranchStatus(result);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch branch status:', err);
|
||||
setBranchStatus(null);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBranchStatus();
|
||||
}, [selectedAttempt]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
task,
|
||||
@@ -218,8 +247,16 @@ const TaskDetailsProvider: FC<{
|
||||
fetchAttemptData,
|
||||
isAttemptRunning,
|
||||
defaultFollowUpVariant,
|
||||
branchStatus,
|
||||
setBranchStatus,
|
||||
}),
|
||||
[attemptData, fetchAttemptData, isAttemptRunning, defaultFollowUpVariant]
|
||||
[
|
||||
attemptData,
|
||||
fetchAttemptData,
|
||||
isAttemptRunning,
|
||||
defaultFollowUpVariant,
|
||||
branchStatus,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
EditorType,
|
||||
TaskAttempt,
|
||||
TaskWithAttemptStatus,
|
||||
BranchStatus,
|
||||
} from 'shared/types';
|
||||
import { AttemptData } from '@/lib/types.ts';
|
||||
|
||||
@@ -33,6 +34,8 @@ interface TaskAttemptDataContextValue {
|
||||
fetchAttemptData: (attemptId: string) => Promise<void> | void;
|
||||
isAttemptRunning: boolean;
|
||||
defaultFollowUpVariant: string | null;
|
||||
branchStatus: BranchStatus | null;
|
||||
setBranchStatus: Dispatch<SetStateAction<BranchStatus | null>>;
|
||||
}
|
||||
|
||||
export const TaskAttemptDataContext =
|
||||
|
||||
@@ -37,6 +37,7 @@ export function TaskFollowUpSection() {
|
||||
fetchAttemptData,
|
||||
isAttemptRunning,
|
||||
defaultFollowUpVariant,
|
||||
branchStatus,
|
||||
} = useContext(TaskAttemptDataContext);
|
||||
const { profiles } = useUserSystem();
|
||||
|
||||
@@ -66,12 +67,24 @@ export function TaskFollowUpSection() {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if PR is merged - if so, block follow-ups
|
||||
if (branchStatus?.merges) {
|
||||
const mergedPR = branchStatus.merges.find(
|
||||
(m) => m.type === 'pr' && m.pr_info.status === 'merged'
|
||||
);
|
||||
if (mergedPR) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [
|
||||
selectedAttempt,
|
||||
attemptData.processes,
|
||||
isAttemptRunning,
|
||||
isSendingFollowUp,
|
||||
branchStatus?.merges,
|
||||
]);
|
||||
const currentProfile = useMemo(() => {
|
||||
if (!selectedProfile || !profiles) return null;
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
TaskAttemptDataContext,
|
||||
TaskDetailsContext,
|
||||
TaskSelectedAttemptContext,
|
||||
} from '@/components/context/taskDetailsContext.ts';
|
||||
@@ -46,6 +47,7 @@ function CreatePrDialog({
|
||||
}: Props) {
|
||||
const { projectId, task } = useContext(TaskDetailsContext);
|
||||
const { selectedAttempt } = useContext(TaskSelectedAttemptContext);
|
||||
const { fetchAttemptData } = useContext(TaskAttemptDataContext);
|
||||
const [prTitle, setPrTitle] = useState('');
|
||||
const [prBody, setPrBody] = useState('');
|
||||
const [prBaseBranch, setPrBaseBranch] = useState(
|
||||
@@ -82,12 +84,14 @@ function CreatePrDialog({
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setError(null); // Clear any previous errors on success
|
||||
window.open(result.data, '_blank');
|
||||
setShowCreatePRDialog(false);
|
||||
// Reset form
|
||||
setPrTitle('');
|
||||
setPrBody('');
|
||||
setPrBaseBranch(selectedAttempt?.base_branch || 'main');
|
||||
// Refresh branch status to show the new PR
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
} else {
|
||||
if (result.error) {
|
||||
setShowCreatePRDialog(false);
|
||||
@@ -112,7 +116,7 @@ function CreatePrDialog({
|
||||
setError('Failed to create GitHub PR');
|
||||
}
|
||||
}
|
||||
|
||||
setShowCreatePRDialog(false);
|
||||
setCreatingPR(false);
|
||||
}, [
|
||||
projectId,
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
GitBranch as GitBranchIcon,
|
||||
GitPullRequest,
|
||||
History,
|
||||
Upload,
|
||||
Play,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
@@ -44,7 +43,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { ExecutionProcess } from 'shared/types';
|
||||
import type { BranchStatus, GitBranch, TaskAttempt } from 'shared/types';
|
||||
import type { GitBranch, TaskAttempt } from 'shared/types';
|
||||
import {
|
||||
TaskAttemptDataContext,
|
||||
TaskAttemptStoppingContext,
|
||||
@@ -103,9 +102,8 @@ function CurrentAttempt({
|
||||
useContext(TaskDetailsContext);
|
||||
const { config } = useConfig();
|
||||
const { isStopping, setIsStopping } = useContext(TaskAttemptStoppingContext);
|
||||
const { attemptData, fetchAttemptData, isAttemptRunning } = useContext(
|
||||
TaskAttemptDataContext
|
||||
);
|
||||
const { attemptData, fetchAttemptData, isAttemptRunning, branchStatus } =
|
||||
useContext(TaskAttemptDataContext);
|
||||
const { jumpToProcess } = useProcessSelection();
|
||||
|
||||
const [isStartingDevServer, setIsStartingDevServer] = useState(false);
|
||||
@@ -115,12 +113,12 @@ function CurrentAttempt({
|
||||
const [devServerDetails, setDevServerDetails] =
|
||||
useState<ExecutionProcess | null>(null);
|
||||
const [isHoveringDevServer, setIsHoveringDevServer] = useState(false);
|
||||
const [branchStatus, setBranchStatus] = useState<BranchStatus | null>(null);
|
||||
const [branchStatusLoading, setBranchStatusLoading] = useState(false);
|
||||
const [showRebaseDialog, setShowRebaseDialog] = useState(false);
|
||||
const [selectedRebaseBranch, setSelectedRebaseBranch] = useState<string>('');
|
||||
const [showStopConfirmation, setShowStopConfirmation] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [mergeSuccess, setMergeSuccess] = useState(false);
|
||||
const [pushSuccess, setPushSuccess] = useState(false);
|
||||
|
||||
const processedDevServerLogs = useMemo(() => {
|
||||
if (!devServerDetails) return 'No output yet...';
|
||||
@@ -263,7 +261,10 @@ function CurrentAttempt({
|
||||
try {
|
||||
setPushing(true);
|
||||
await attemptsApi.push(selectedAttempt.id);
|
||||
fetchBranchStatus();
|
||||
setError(null); // Clear any previous errors on success
|
||||
setPushSuccess(true);
|
||||
setTimeout(() => setPushSuccess(false), 2000);
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to push changes:', error);
|
||||
setError(error.message || 'Failed to push changes');
|
||||
@@ -272,38 +273,16 @@ function CurrentAttempt({
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBranchStatus = useCallback(async () => {
|
||||
if (!selectedAttempt?.id) return;
|
||||
|
||||
try {
|
||||
setBranchStatusLoading(true);
|
||||
const result = await attemptsApi.getBranchStatus(selectedAttempt.id);
|
||||
setBranchStatus((prev) => {
|
||||
if (JSON.stringify(prev) === JSON.stringify(result)) return prev;
|
||||
return result;
|
||||
});
|
||||
} catch (err) {
|
||||
setError('Failed to load branch status');
|
||||
} finally {
|
||||
setBranchStatusLoading(false);
|
||||
}
|
||||
}, [projectId, selectedAttempt?.id, selectedAttempt?.task_id, setError]);
|
||||
|
||||
// Fetch branch status when selected attempt changes
|
||||
useEffect(() => {
|
||||
if (selectedAttempt) {
|
||||
fetchBranchStatus();
|
||||
}
|
||||
}, [selectedAttempt, fetchBranchStatus]);
|
||||
|
||||
const performMerge = async () => {
|
||||
if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) return;
|
||||
|
||||
try {
|
||||
setMerging(true);
|
||||
await attemptsApi.merge(selectedAttempt.id);
|
||||
// Refetch branch status to show updated state
|
||||
fetchBranchStatus();
|
||||
setError(null); // Clear any previous errors on success
|
||||
setMergeSuccess(true);
|
||||
setTimeout(() => setMergeSuccess(false), 2000);
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
} catch (error) {
|
||||
console.error('Failed to merge changes:', error);
|
||||
// @ts-expect-error it is type ApiError
|
||||
@@ -319,8 +298,8 @@ function CurrentAttempt({
|
||||
try {
|
||||
setRebasing(true);
|
||||
await attemptsApi.rebase(selectedAttempt.id, { new_base_branch: null });
|
||||
// Refresh branch status after rebase
|
||||
fetchBranchStatus();
|
||||
setError(null); // Clear any previous errors on success
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to rebase branch');
|
||||
} finally {
|
||||
@@ -336,8 +315,8 @@ function CurrentAttempt({
|
||||
await attemptsApi.rebase(selectedAttempt.id, {
|
||||
new_base_branch: newBaseBranch,
|
||||
});
|
||||
// Refresh branch status after rebase
|
||||
fetchBranchStatus();
|
||||
setError(null); // Clear any previous errors on success
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
setShowRebaseDialog(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to rebase branch');
|
||||
@@ -360,9 +339,9 @@ function CurrentAttempt({
|
||||
const handlePRButtonClick = async () => {
|
||||
if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) return;
|
||||
|
||||
// If PR already exists, view it in a new tab
|
||||
if (selectedAttempt.pr_url) {
|
||||
window.open(selectedAttempt.pr_url, '_blank');
|
||||
// If PR already exists, push to it
|
||||
if (mergeInfo.hasOpenPR) {
|
||||
await handlePushClick();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -387,6 +366,42 @@ function CurrentAttempt({
|
||||
return getEditorDisplayName(config.editor.editor_type);
|
||||
}, [config?.editor?.editor_type]);
|
||||
|
||||
// Memoize merge status information to avoid repeated calculations
|
||||
const mergeInfo = useMemo(() => {
|
||||
if (!branchStatus?.merges)
|
||||
return {
|
||||
hasOpenPR: false,
|
||||
openPR: null,
|
||||
hasMergedPR: false,
|
||||
mergedPR: null,
|
||||
hasMerged: false,
|
||||
latestMerge: null,
|
||||
};
|
||||
|
||||
const openPR = branchStatus.merges.find(
|
||||
(m) => m.type === 'pr' && m.pr_info.status === 'open'
|
||||
);
|
||||
|
||||
const mergedPR = branchStatus.merges.find(
|
||||
(m) => m.type === 'pr' && m.pr_info.status === 'merged'
|
||||
);
|
||||
|
||||
const merges = branchStatus.merges.filter(
|
||||
(m) =>
|
||||
m.type === 'direct' ||
|
||||
(m.type === 'pr' && m.pr_info.status === 'merged')
|
||||
);
|
||||
|
||||
return {
|
||||
hasOpenPR: !!openPR,
|
||||
openPR,
|
||||
hasMergedPR: !!mergedPR,
|
||||
mergedPR,
|
||||
hasMerged: merges.length > 0,
|
||||
latestMerge: branchStatus.merges[0] || null, // Most recent merge
|
||||
};
|
||||
}, [branchStatus?.merges]);
|
||||
|
||||
const handleCopyWorktreePath = useCallback(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(selectedAttempt.container_ref || '');
|
||||
@@ -397,6 +412,71 @@ function CurrentAttempt({
|
||||
}
|
||||
}, [selectedAttempt.container_ref]);
|
||||
|
||||
// Get status information for display
|
||||
const getStatusInfo = useCallback(() => {
|
||||
if (mergeInfo.hasMergedPR && mergeInfo.mergedPR?.type === 'pr') {
|
||||
const prMerge = mergeInfo.mergedPR;
|
||||
return {
|
||||
dotColor: 'bg-green-500',
|
||||
textColor: 'text-green-700',
|
||||
text: `PR #${prMerge.pr_info.number} merged`,
|
||||
isClickable: true,
|
||||
onClick: () => window.open(prMerge.pr_info.url, '_blank'),
|
||||
};
|
||||
}
|
||||
if (
|
||||
mergeInfo.hasMerged &&
|
||||
mergeInfo.latestMerge?.type === 'direct' &&
|
||||
(branchStatus?.commits_ahead ?? 0) === 0
|
||||
) {
|
||||
return {
|
||||
dotColor: 'bg-green-500',
|
||||
textColor: 'text-green-700',
|
||||
text: `Merged`,
|
||||
isClickable: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (mergeInfo.hasOpenPR && mergeInfo.openPR?.type === 'pr') {
|
||||
const prMerge = mergeInfo.openPR;
|
||||
return {
|
||||
dotColor: 'bg-blue-500',
|
||||
textColor: 'text-blue-700',
|
||||
text: `PR #${prMerge.pr_info.number}`,
|
||||
isClickable: true,
|
||||
onClick: () => window.open(prMerge.pr_info.url, '_blank'),
|
||||
};
|
||||
}
|
||||
|
||||
if ((branchStatus?.commits_behind ?? 0) > 0) {
|
||||
return {
|
||||
dotColor: 'bg-orange-500',
|
||||
textColor: 'text-orange-700',
|
||||
text: `Rebase needed${branchStatus?.has_uncommitted_changes ? ' (dirty)' : ''}`,
|
||||
isClickable: false,
|
||||
};
|
||||
}
|
||||
|
||||
if ((branchStatus?.commits_ahead ?? 0) > 0) {
|
||||
return {
|
||||
dotColor: 'bg-yellow-500',
|
||||
textColor: 'text-yellow-700',
|
||||
text:
|
||||
branchStatus?.commits_ahead === 1
|
||||
? `1 commit ahead${branchStatus?.has_uncommitted_changes ? ' (dirty)' : ''}`
|
||||
: `${branchStatus?.commits_ahead} commits ahead${branchStatus?.has_uncommitted_changes ? ' (dirty)' : ''}`,
|
||||
isClickable: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
dotColor: 'bg-gray-500',
|
||||
textColor: 'text-gray-700',
|
||||
text: `Up to date${branchStatus?.has_uncommitted_changes ? ' (dirty)' : ''}`,
|
||||
isClickable: false,
|
||||
};
|
||||
}, [mergeInfo, branchStatus]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-6 items-start">
|
||||
@@ -429,9 +509,7 @@ function CurrentAttempt({
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={handleRebaseDialogOpen}
|
||||
disabled={
|
||||
rebasing || branchStatusLoading || isAttemptRunning
|
||||
}
|
||||
disabled={rebasing || isAttemptRunning}
|
||||
className="h-4 w-4 p-0 hover:bg-muted"
|
||||
>
|
||||
<Settings className="h-3 w-3" />
|
||||
@@ -456,24 +534,30 @@ function CurrentAttempt({
|
||||
Status
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{selectedAttempt.merge_commit ? (
|
||||
<div className="flex items-center gap-1.5 overflow-hidden">
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full" />
|
||||
<span className="text-sm font-medium text-green-700 truncate">
|
||||
Merged
|
||||
</span>
|
||||
<span className="text-xs font-mono text-muted-foreground truncate">
|
||||
({selectedAttempt.merge_commit.slice(0, 8)})
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 overflow-hidden">
|
||||
<div className="h-2 w-2 bg-yellow-500 rounded-full" />
|
||||
<span className="text-sm font-medium text-yellow-700">
|
||||
Not merged
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(() => {
|
||||
const statusInfo = getStatusInfo();
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className={`h-2 w-2 ${statusInfo.dotColor} rounded-full`}
|
||||
/>
|
||||
{statusInfo.isClickable ? (
|
||||
<button
|
||||
onClick={statusInfo.onClick}
|
||||
className={`text-sm font-medium ${statusInfo.textColor} hover:underline cursor-pointer`}
|
||||
>
|
||||
{statusInfo.text}
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className={`text-sm font-medium ${statusInfo.textColor}`}
|
||||
>
|
||||
{statusInfo.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,7 +578,7 @@ function CurrentAttempt({
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={`text-xs font-mono px-2 py-1 rounded cursor-pointer transition-all duration-300 flex items-center gap-2 ${
|
||||
className={`text-xs font-mono px-2 py-1 rounded break-all cursor-pointer transition-all duration-300 flex items-center gap-2 ${
|
||||
copied
|
||||
? 'bg-green-100 text-green-800 border border-green-300'
|
||||
: 'text-muted-foreground bg-muted hover:bg-muted/80'
|
||||
@@ -600,88 +684,73 @@ function CurrentAttempt({
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* Git Operations */}
|
||||
{selectedAttempt && branchStatus && (
|
||||
{selectedAttempt && branchStatus && !mergeInfo.hasMergedPR && (
|
||||
<>
|
||||
{(branchStatus.commits_behind ?? 0) > 0 &&
|
||||
!branchStatus.merged && (
|
||||
<Button
|
||||
onClick={handleRebaseClick}
|
||||
disabled={
|
||||
rebasing || branchStatusLoading || isAttemptRunning
|
||||
}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
className="border-orange-300 text-orange-700 hover:bg-orange-50 gap-1"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-3 w-3 ${rebasing ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
{rebasing ? 'Rebasing...' : `Rebase`}
|
||||
</Button>
|
||||
)}
|
||||
{
|
||||
// Normal merge and PR buttons for regular tasks
|
||||
!branchStatus.merged && (
|
||||
<>
|
||||
<Button
|
||||
onClick={handlePRButtonClick}
|
||||
disabled={
|
||||
creatingPR ||
|
||||
Boolean((branchStatus.commits_behind ?? 0) > 0) ||
|
||||
isAttemptRunning
|
||||
}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
className="border-blue-300 text-blue-700 hover:bg-blue-50 gap-1"
|
||||
>
|
||||
<GitPullRequest className="h-3 w-3" />
|
||||
{selectedAttempt.pr_url
|
||||
? 'View PR'
|
||||
: creatingPR
|
||||
? 'Creating...'
|
||||
: 'Create PR'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={
|
||||
selectedAttempt.pr_status === 'open'
|
||||
? handlePushClick
|
||||
: handleMergeClick
|
||||
}
|
||||
disabled={
|
||||
selectedAttempt.pr_status === 'open'
|
||||
? pushing ||
|
||||
isAttemptRunning ||
|
||||
(branchStatus.remote_up_to_date ?? true)
|
||||
: merging ||
|
||||
Boolean((branchStatus.commits_behind ?? 0) > 0) ||
|
||||
isAttemptRunning
|
||||
}
|
||||
size="xs"
|
||||
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 gap-1"
|
||||
>
|
||||
{selectedAttempt.pr_status === 'open' ? (
|
||||
<>
|
||||
<Upload className="h-3 w-3" />
|
||||
{pushing
|
||||
? 'Pushing...'
|
||||
: branchStatus.remote_commits_behind === null
|
||||
? 'Disconnected'
|
||||
: branchStatus.remote_commits_behind === 0
|
||||
? 'Push to remote'
|
||||
: branchStatus.remote_commits_behind === 1
|
||||
? 'Push 1 commit'
|
||||
: `Push ${branchStatus.remote_commits_behind} commits`}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GitBranchIcon className="h-3 w-3" />
|
||||
{merging ? 'Merging...' : 'Merge'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{(branchStatus.commits_behind ?? 0) > 0 && (
|
||||
<Button
|
||||
onClick={handleRebaseClick}
|
||||
disabled={rebasing || isAttemptRunning}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
className="border-orange-300 text-orange-700 hover:bg-orange-50 gap-1"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-3 w-3 ${rebasing ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
{rebasing ? 'Rebasing...' : `Rebase`}
|
||||
</Button>
|
||||
)}
|
||||
<>
|
||||
<Button
|
||||
onClick={handlePRButtonClick}
|
||||
disabled={
|
||||
creatingPR ||
|
||||
pushing ||
|
||||
Boolean((branchStatus.commits_behind ?? 0) > 0) ||
|
||||
isAttemptRunning ||
|
||||
(mergeInfo.hasOpenPR &&
|
||||
branchStatus.remote_commits_ahead === 0) ||
|
||||
((branchStatus.commits_ahead ?? 0) === 0 &&
|
||||
!pushSuccess &&
|
||||
!mergeSuccess)
|
||||
}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
className="border-blue-300 text-blue-700 hover:bg-blue-50 gap-1 min-w-[120px]"
|
||||
>
|
||||
<GitPullRequest className="h-3 w-3" />
|
||||
{mergeInfo.hasOpenPR
|
||||
? pushSuccess
|
||||
? 'Pushed!'
|
||||
: pushing
|
||||
? 'Pushing...'
|
||||
: branchStatus.remote_commits_ahead === 0
|
||||
? 'Push to PR'
|
||||
: branchStatus.remote_commits_ahead === 1
|
||||
? 'Push 1 commit'
|
||||
: `Push ${branchStatus.remote_commits_ahead || 0} commits`
|
||||
: creatingPR
|
||||
? 'Creating...'
|
||||
: 'Create PR'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleMergeClick}
|
||||
disabled={
|
||||
mergeInfo.hasOpenPR ||
|
||||
merging ||
|
||||
Boolean((branchStatus.commits_behind ?? 0) > 0) ||
|
||||
isAttemptRunning ||
|
||||
((branchStatus.commits_ahead ?? 0) === 0 &&
|
||||
!pushSuccess &&
|
||||
!mergeSuccess)
|
||||
}
|
||||
size="xs"
|
||||
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 gap-1 min-w-[120px]"
|
||||
>
|
||||
<GitBranchIcon className="h-3 w-3" />
|
||||
{mergeSuccess ? 'Merged!' : merging ? 'Merging...' : 'Merge'}
|
||||
</Button>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user