Merge task: Warn the user if there are uncommitted changes into main

This commit is contained in:
Louis Knight-Webb
2025-06-24 17:43:09 +01:00
3 changed files with 91 additions and 15 deletions

View File

@@ -126,6 +126,7 @@ pub struct BranchStatus {
pub commits_ahead: usize, pub commits_ahead: usize,
pub up_to_date: bool, pub up_to_date: bool,
pub merged: bool, pub merged: bool,
pub has_uncommitted_changes: bool,
} }
impl TaskAttempt { impl TaskAttempt {
@@ -1229,6 +1230,17 @@ impl TaskAttempt {
let worktree_head = worktree_repo.head()?.peel_to_commit()?; let worktree_head = worktree_repo.head()?.peel_to_commit()?;
let worktree_oid = worktree_head.id(); let worktree_oid = worktree_head.id();
// Check for uncommitted changes in the worktree
let has_uncommitted_changes = {
let statuses = worktree_repo.statuses(None)?;
statuses.iter().any(|entry| {
let status = entry.status();
// Check for any unstaged or staged changes
status.is_wt_modified() || status.is_wt_new() || status.is_wt_deleted() ||
status.is_index_modified() || status.is_index_new() || status.is_index_deleted()
})
};
if main_oid == worktree_oid { if main_oid == worktree_oid {
// Branches are at the same commit // Branches are at the same commit
return Ok(BranchStatus { return Ok(BranchStatus {
@@ -1237,6 +1249,7 @@ impl TaskAttempt {
commits_ahead: 0, commits_ahead: 0,
up_to_date: true, up_to_date: true,
merged: attempt.merge_commit.is_some(), merged: attempt.merge_commit.is_some(),
has_uncommitted_changes,
}); });
} }
@@ -1260,6 +1273,7 @@ impl TaskAttempt {
commits_ahead, commits_ahead,
up_to_date: commits_behind == 0 && commits_ahead == 0, up_to_date: commits_behind == 0 && commits_ahead == 0,
merged: attempt.merge_commit.is_some(), merged: attempt.merge_commit.is_some(),
has_uncommitted_changes,
}) })
} }

View File

@@ -58,6 +58,7 @@ export function TaskAttemptComparePage() {
const [showAllUnchanged, setShowAllUnchanged] = useState(false); const [showAllUnchanged, setShowAllUnchanged] = useState(false);
const [deletingFiles, setDeletingFiles] = useState<Set<string>>(new Set()); const [deletingFiles, setDeletingFiles] = useState<Set<string>>(new Set());
const [fileToDelete, setFileToDelete] = useState<string | null>(null); const [fileToDelete, setFileToDelete] = useState<string | null>(null);
const [showUncommittedWarning, setShowUncommittedWarning] = useState(false);
useEffect(() => { useEffect(() => {
if (projectId && taskId && attemptId) { if (projectId && taskId && attemptId) {
@@ -125,6 +126,18 @@ export function TaskAttemptComparePage() {
const handleMergeClick = async () => { const handleMergeClick = async () => {
if (!projectId || !taskId || !attemptId) return; if (!projectId || !taskId || !attemptId) return;
// Check for uncommitted changes and show warning dialog
if (branchStatus?.has_uncommitted_changes) {
setShowUncommittedWarning(true);
return;
}
await performMerge();
};
const performMerge = async () => {
if (!projectId || !taskId || !attemptId) return;
try { try {
setMerging(true); setMerging(true);
const response = await makeRequest( const response = await makeRequest(
@@ -154,6 +167,15 @@ export function TaskAttemptComparePage() {
} }
}; };
const handleConfirmMergeWithUncommitted = async () => {
setShowUncommittedWarning(false);
await performMerge();
};
const handleCancelMergeWithUncommitted = () => {
setShowUncommittedWarning(false);
};
const handleRebaseClick = async () => { const handleRebaseClick = async () => {
if (!projectId || !taskId || !attemptId) return; if (!projectId || !taskId || !attemptId) return;
@@ -465,20 +487,28 @@ export function TaskAttemptComparePage() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* Branch Status */} {/* Branch Status */}
{!branchStatusLoading && branchStatus && ( {!branchStatusLoading && branchStatus && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-4 text-sm">
<GitBranch className="h-4 w-4" /> <div className="flex items-center gap-2">
{branchStatus.up_to_date ? ( <GitBranch className="h-4 w-4" />
<span className="text-green-600">Up to date</span> {branchStatus.up_to_date ? (
) : branchStatus.is_behind === true ? ( <span className="text-green-600">Up to date</span>
<span className="text-orange-600"> ) : branchStatus.is_behind === true ? (
{branchStatus.commits_behind} commit <span className="text-orange-600">
{branchStatus.commits_behind !== 1 ? "s" : ""} behind main {branchStatus.commits_behind} commit
</span> {branchStatus.commits_behind !== 1 ? "s" : ""} behind main
) : ( </span>
<span className="text-blue-600"> ) : (
{branchStatus.commits_ahead} commit <span className="text-blue-600">
{branchStatus.commits_ahead !== 1 ? "s" : ""} ahead of main {branchStatus.commits_ahead} commit
</span> {branchStatus.commits_ahead !== 1 ? "s" : ""} ahead of main
</span>
)}
</div>
{branchStatus.has_uncommitted_changes && (
<div className="flex items-center gap-1 text-yellow-600">
<FileText className="h-4 w-4" />
<span>Uncommitted changes</span>
</div>
)} )}
</div> </div>
)} )}
@@ -718,6 +748,38 @@ export function TaskAttemptComparePage() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Uncommitted Changes Warning Dialog */}
<Dialog open={showUncommittedWarning} onOpenChange={() => handleCancelMergeWithUncommitted()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Uncommitted Changes Detected</DialogTitle>
<DialogDescription>
There are uncommitted changes in the worktree that will be included in the merge.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
<p className="text-sm text-yellow-800">
<strong>Warning:</strong> The worktree contains uncommitted changes (modified, added, or deleted files)
that have not been committed to git. These changes will be permanently merged into the main branch.
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCancelMergeWithUncommitted}>
Cancel
</Button>
<Button
onClick={handleConfirmMergeWithUncommitted}
disabled={merging}
className="bg-yellow-600 hover:bg-yellow-700"
>
{merging ? "Merging..." : "Merge Anyway"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
); );
} }

View File

@@ -70,7 +70,7 @@ export type FileDiff = { path: string, chunks: Array<DiffChunk>, };
export type WorktreeDiff = { files: Array<FileDiff>, }; export type WorktreeDiff = { files: Array<FileDiff>, };
export type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, }; export type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, has_uncommitted_changes: boolean, };
export type ExecutionProcess = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, stdout: string | null, stderr: string | null, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, }; export type ExecutionProcess = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, stdout: string | null, stderr: string | null, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, };