Fix protected branch push rejections not being detected (#345)

This commit is contained in:
Gregory Schier
2026-01-05 13:28:41 -08:00
committed by GitHub
parent 5bd8685175
commit ab5c7f638b
11 changed files with 139 additions and 72 deletions

View File

@@ -1,27 +1,27 @@
import type { import type {
FindHttpResponsesRequest, FindHttpResponsesRequest,
FindHttpResponsesResponse, FindHttpResponsesResponse,
GetCookieValueRequest, GetCookieValueRequest,
GetCookieValueResponse, GetCookieValueResponse,
GetHttpRequestByIdRequest, GetHttpRequestByIdRequest,
GetHttpRequestByIdResponse, GetHttpRequestByIdResponse,
ListCookieNamesResponse, ListCookieNamesResponse,
ListFoldersRequest, ListFoldersRequest,
ListFoldersResponse, ListFoldersResponse,
ListHttpRequestsRequest, ListHttpRequestsRequest,
ListHttpRequestsResponse, ListHttpRequestsResponse,
OpenWindowRequest, OpenWindowRequest,
PromptTextRequest, PromptTextRequest,
PromptTextResponse, PromptTextResponse,
RenderGrpcRequestRequest, RenderGrpcRequestRequest,
RenderGrpcRequestResponse, RenderGrpcRequestResponse,
RenderHttpRequestRequest, RenderHttpRequestRequest,
RenderHttpRequestResponse, RenderHttpRequestResponse,
SendHttpRequestRequest, SendHttpRequestRequest,
SendHttpRequestResponse, SendHttpRequestResponse,
ShowToastRequest, ShowToastRequest,
TemplateRenderRequest, TemplateRenderRequest,
WorkspaceInfo, WorkspaceInfo,
} from '../bindings/gen_events.ts'; } from '../bindings/gen_events.ts';
import type { HttpRequest } from '../bindings/gen_models.ts'; import type { HttpRequest } from '../bindings/gen_models.ts';
import type { JsonValue } from '../bindings/serde_json/JsonValue'; import type { JsonValue } from '../bindings/serde_json/JsonValue';

View File

@@ -1,4 +1,7 @@
import type { CallWebsocketRequestActionArgs, WebsocketRequestAction } from '../bindings/gen_events'; import type {
CallWebsocketRequestActionArgs,
WebsocketRequestAction,
} from '../bindings/gen_events';
import type { Context } from './Context'; import type { Context } from './Context';
export type WebsocketRequestActionPlugin = WebsocketRequestAction & { export type WebsocketRequestActionPlugin = WebsocketRequestAction & {

View File

@@ -2,19 +2,12 @@
"compilerOptions": { "compilerOptions": {
"module": "node16", "module": "node16",
"target": "es6", "target": "es6",
"lib": [ "lib": ["es2021", "dom"],
"es2021",
"dom"
],
"declaration": true, "declaration": true,
"declarationDir": "./lib", "declarationDir": "./lib",
"outDir": "./lib", "outDir": "./lib",
"strict": true, "strict": true,
"types": [ "types": ["node"]
"node"
]
}, },
"files": [ "files": ["src/index.ts"]
"src/index.ts"
]
} }

View File

@@ -13,13 +13,8 @@
"outDir": "build", "outDir": "build",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"*": [ "*": ["node_modules/*", "src/types/*"]
"node_modules/*",
"src/types/*"
]
} }
}, },
"include": [ "include": ["src"]
"src"
]
} }

View File

@@ -33,26 +33,48 @@ pub(crate) fn git_push(dir: &Path) -> Result<PushResult> {
let stdout = String::from_utf8_lossy(&out.stdout); let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr); let stderr = String::from_utf8_lossy(&out.stderr);
let combined = stdout + stderr; let combined = stdout + stderr;
let combined_lower = combined.to_lowercase();
info!("Pushed to repo status={} {combined}", out.status); info!("Pushed to repo status={} {combined}", out.status);
if combined.to_lowercase().contains("could not read") { // Helper to check if this is a credentials error
return Ok(PushResult::NeedsCredentials { url: remote_url.to_string(), error: None }); let is_credentials_error = || {
} combined_lower.contains("could not read")
|| combined_lower.contains("unable to access")
if combined.to_lowercase().contains("unable to access") { || combined_lower.contains("authentication failed")
return Ok(PushResult::NeedsCredentials { };
url: remote_url.to_string(),
error: Some(combined.to_string()), // Check for explicit rejection indicators first (e.g., protected branch rejections)
}); // These can occur even if some git servers don't properly set exit codes
} if combined_lower.contains("rejected") || combined_lower.contains("failed to push") {
if is_credentials_error() {
if combined.to_lowercase().contains("up-to-date") { return Ok(PushResult::NeedsCredentials {
return Ok(PushResult::UpToDate); url: remote_url.to_string(),
error: Some(combined.to_string()),
});
}
return Err(GenericError(format!("Failed to push: {combined}")));
} }
// Check exit status for any other failures
if !out.status.success() { if !out.status.success() {
return Err(GenericError(format!("Failed to push {combined}"))); if combined_lower.contains("could not read") {
return Ok(PushResult::NeedsCredentials { url: remote_url.to_string(), error: None });
}
if combined_lower.contains("unable to access")
|| combined_lower.contains("authentication failed")
{
return Ok(PushResult::NeedsCredentials {
url: remote_url.to_string(),
error: Some(combined.to_string()),
});
}
return Err(GenericError(format!("Failed to push: {combined}")));
}
// Success cases (exit code 0 and no rejection indicators)
if combined_lower.contains("up-to-date") {
return Ok(PushResult::UpToDate);
} }
Ok(PushResult::Success { message: format!("Pushed to {}/{}", remote_name, branch_name) }) Ok(PushResult::Success { message: format!("Pushed to {}/{}", remote_name, branch_name) })

View File

@@ -51,7 +51,11 @@ export function CreateWorkspaceDialog({ hide }: Props) {
gitMutations(syncConfig.filePath, gitCallbacks(syncConfig.filePath)) gitMutations(syncConfig.filePath, gitCallbacks(syncConfig.filePath))
.init.mutateAsync() .init.mutateAsync()
.catch((err) => { .catch((err) => {
showErrorToast('git-init-error', String(err)); showErrorToast({
id: 'git-init-error',
title: 'Error initializing Git',
message: String(err),
});
}); });
} }

View File

@@ -58,12 +58,11 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
'pointer-events-auto overflow-hidden', 'pointer-events-auto overflow-hidden',
'relative pointer-events-auto bg-surface text-text rounded-lg', 'relative pointer-events-auto bg-surface text-text rounded-lg',
'border border-border shadow-lg w-[25rem]', 'border border-border shadow-lg w-[25rem]',
'grid grid-cols-[1fr_auto]',
)} )}
> >
<div className="px-3 py-3 flex items-start gap-2 w-full"> <div className="pl-3 py-3 pr-10 flex items-start gap-2 w-full max-h-[11rem] overflow-auto">
{toastIcon && <Icon icon={toastIcon} color={color} className="mt-1" />} {toastIcon && <Icon icon={toastIcon} color={color} className="mt-1 flex-shrink-0" />}
<VStack space={2} className="w-full"> <VStack space={2} className="w-full min-w-0">
<div className="select-auto">{children}</div> <div className="select-auto">{children}</div>
{action?.({ hide: onClose })} {action?.({ hide: onClose })}
</VStack> </VStack>
@@ -72,7 +71,7 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
<IconButton <IconButton
color={color} color={color}
variant="border" variant="border"
className="opacity-60 border-0" className="opacity-60 border-0 !absolute top-2 right-2"
title="Dismiss" title="Dismiss"
icon="x" icon="x"
onClick={onClose} onClick={onClose}

View File

@@ -66,7 +66,11 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
handlePushResult(r); handlePushResult(r);
onDone(); onDone();
} catch (err) { } catch (err) {
showErrorToast('git-commit-and-push-error', String(err)); showErrorToast({
id: 'git-commit-and-push-error',
title: 'Error committing and pushing',
message: String(err),
});
} finally { } finally {
setIsPushing(false); setIsPushing(false);
} }

View File

@@ -62,6 +62,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
checkout.mutate( checkout.mutate(
{ branch, force }, { branch, force },
{ {
disableToastError: true,
async onError(err) { async onError(err) {
if (!force) { if (!force) {
// Checkout failed so ask user if they want to force it // Checkout failed so ask user if they want to force it
@@ -78,7 +79,11 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
} }
} else { } else {
// Checkout failed // Checkout failed
showErrorToast('git-checkout-error', String(err)); showErrorToast({
id: 'git-checkout-error',
title: 'Error checking out branch',
message: String(err),
});
} }
}, },
async onSuccess(branchName) { async onSuccess(branchName) {
@@ -132,8 +137,13 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
await branch.mutateAsync( await branch.mutateAsync(
{ branch: name }, { branch: name },
{ {
disableToastError: true,
onError: (err) => { onError: (err) => {
showErrorToast('git-branch-error', String(err)); showErrorToast({
id: 'git-branch-error',
title: 'Error creating branch',
message: String(err),
});
}, },
}, },
); );
@@ -163,6 +173,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
await mergeBranch.mutateAsync( await mergeBranch.mutateAsync(
{ branch, force: false }, { branch, force: false },
{ {
disableToastError: true,
onSettled: hide, onSettled: hide,
onSuccess() { onSuccess() {
showToast({ showToast({
@@ -177,7 +188,11 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
sync({ force: true }); sync({ force: true });
}, },
onError(err) { onError(err) {
showErrorToast('git-merged-branch-error', String(err)); showErrorToast({
id: 'git-merged-branch-error',
title: 'Error merging branch',
message: String(err),
});
}, },
}, },
); );
@@ -208,8 +223,13 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
await deleteBranch.mutateAsync( await deleteBranch.mutateAsync(
{ branch: currentBranch }, { branch: currentBranch },
{ {
disableToastError: true,
onError(err) { onError(err) {
showErrorToast('git-delete-branch-error', String(err)); showErrorToast({
id: 'git-delete-branch-error',
title: 'Error deleting branch',
message: String(err),
});
}, },
async onSuccess() { async onSuccess() {
await sync({ force: true }); await sync({ force: true });
@@ -226,9 +246,14 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
waitForOnSelect: true, waitForOnSelect: true,
async onSelect() { async onSelect() {
await push.mutateAsync(undefined, { await push.mutateAsync(undefined, {
disableToastError: true,
onSuccess: handlePullResult, onSuccess: handlePullResult,
onError(err) { onError(err) {
showErrorToast('git-pull-error', String(err)); showErrorToast({
id: 'git-push-error',
title: 'Error pushing changes',
message: String(err),
});
}, },
}); });
}, },
@@ -240,9 +265,14 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
waitForOnSelect: true, waitForOnSelect: true,
async onSelect() { async onSelect() {
await pull.mutateAsync(undefined, { await pull.mutateAsync(undefined, {
disableToastError: true,
onSuccess: handlePullResult, onSuccess: handlePullResult,
onError(err) { onError(err) {
showErrorToast('git-pull-error', String(err)); showErrorToast({
id: 'git-pull-error',
title: 'Error pulling changes',
message: String(err),
});
}, },
}); });
}, },

View File

@@ -38,7 +38,11 @@ export function useSubscribeHttpAuthentication() {
jotaiStore.set(httpAuthenticationSummariesAtom, result); jotaiStore.set(httpAuthenticationSummariesAtom, result);
return result; return result;
} catch (err) { } catch (err) {
showErrorToast('http-authentication-error', err); showErrorToast({
id: 'http-authentication-error',
title: 'HTTP Authentication Error',
message: err,
});
} }
}, },
}); });

View File

@@ -45,11 +45,24 @@ export function hideToast(toHide: ToastInstance) {
}); });
} }
export function showErrorToast<T>(id: string, message: T) { export function showErrorToast<T>({
id,
title,
message,
}: {
id: string;
title: string;
message: T;
}) {
return showToast({ return showToast({
id, id,
message: String(message),
timeout: 8000,
color: 'danger', color: 'danger',
timeout: null,
message: (
<div className="w-full">
<h2 className="text-lg font-bold mb-2">{title}</h2>
<div className="whitespace-pre-wrap break-words">{String(message)}</div>
</div>
),
}); });
} }