Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/components/ForceDeleteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ export const ForceDeleteModal: React.FC<ForceDeleteModalProps> = ({
<WarningBox>
<WarningTitle>This action cannot be undone</WarningTitle>
<WarningText>
Force deleting will permanently remove the workspace and may discard uncommitted work or
lose data.
Force deleting will permanently remove the workspace and{" "}
{error.includes("unpushed commits:")
? "discard the unpushed commits shown above"
: "may discard uncommitted work or lose data"}
. This action cannot be undone.
</WarningText>
</WarningBox>

Expand Down
27 changes: 25 additions & 2 deletions src/runtime/SSHRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,11 @@ export class SSHRuntime implements Runtime {
cd ${shescape.quote(deletedPath)} || exit 1
git diff --quiet --exit-code && git diff --quiet --cached --exit-code || exit 1
if git remote | grep -q .; then
git log --branches --not --remotes --oneline | head -1 | grep -q . && exit 2
unpushed=$(git log --branches --not --remotes --oneline)
if [ -n "$unpushed" ]; then
echo "$unpushed" | head -10 >&2
exit 2
fi
fi
exit 0
`;
Expand Down Expand Up @@ -1012,9 +1016,28 @@ export class SSHRuntime implements Runtime {
}

if (checkExitCode === 2) {
// Read stderr which contains the unpushed commits output
const stderrReader = checkStream.stderr.getReader();
const decoder = new TextDecoder();
let stderr = "";
try {
while (true) {
const { done, value } = await stderrReader.read();
if (done) break;
stderr += decoder.decode(value, { stream: true });
}
} finally {
stderrReader.releaseLock();
}

const commitList = stderr.trim();
const errorMsg = commitList
? `Workspace contains unpushed commits:\n\n${commitList}`
: `Workspace contains unpushed commits. Use force flag to delete anyway.`;

return {
success: false,
error: `Workspace contains unpushed commits. Use force flag to delete anyway.`,
error: errorMsg,
};
}

Expand Down
62 changes: 62 additions & 0 deletions tests/ipcMain/removeWorkspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,5 +614,67 @@ describeIntegration("Workspace deletion integration tests", () => {
},
TEST_TIMEOUT_SSH_MS
);

test.concurrent(
"should include commit list in error for unpushed refs",
async () => {
const env = await createTestEnvironment();
const tempGitRepo = await createTempGitRepo();

try {
const branchName = generateBranchName("delete-unpushed-details");
const runtimeConfig = getRuntimeConfig(branchName);
const { workspaceId } = await createWorkspaceWithInit(
env,
tempGitRepo,
branchName,
runtimeConfig,
true, // waitForInit
true // isSSH
);

// Configure git for committing (SSH environment needs this)
await executeBash(env, workspaceId, 'git config user.email "test@example.com"');
await executeBash(env, workspaceId, 'git config user.name "Test User"');

// Add a fake remote (needed for unpushed check to work)
await executeBash(
env,
workspaceId,
"git remote add origin https://github.com/fake/repo.git"
);

// Create multiple commits with descriptive messages
await executeBash(env, workspaceId, 'echo "1" > file1.txt');
await executeBash(env, workspaceId, "git add file1.txt");
await executeBash(env, workspaceId, 'git commit -m "First commit"');

await executeBash(env, workspaceId, 'echo "2" > file2.txt');
await executeBash(env, workspaceId, "git add file2.txt");
await executeBash(env, workspaceId, 'git commit -m "Second commit"');

// Attempt to delete
const deleteResult = await env.mockIpcRenderer.invoke(
IPC_CHANNELS.WORKSPACE_REMOVE,
workspaceId
);

// Should fail with error containing commit details
expect(deleteResult.success).toBe(false);
expect(deleteResult.error).toContain("unpushed commits:");
expect(deleteResult.error).toContain("First commit");
expect(deleteResult.error).toContain("Second commit");

// Cleanup: force delete for cleanup
await env.mockIpcRenderer.invoke(IPC_CHANNELS.WORKSPACE_REMOVE, workspaceId, {
force: true,
});
} finally {
await cleanupTestEnvironment(env);
await cleanupTempGitRepo(tempGitRepo);
}
},
TEST_TIMEOUT_SSH_MS
);
});
});