diff --git a/src/main/azure-devops/pull-request-creation.test.ts b/src/main/azure-devops/pull-request-creation.test.ts index c9fb01ed367..d4340eb5ac6 100644 --- a/src/main/azure-devops/pull-request-creation.test.ts +++ b/src/main/azure-devops/pull-request-creation.test.ts @@ -186,7 +186,8 @@ describe('Azure DevOps pull request creation', () => { ) ).resolves.toMatchObject({ ok: false, - code: 'auth_required' + code: 'auth_required', + reason: 'auth_required' }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['remote', 'get-url', 'origin'], { cwd: '/repo', diff --git a/src/main/azure-devops/pull-request-creation.ts b/src/main/azure-devops/pull-request-creation.ts index 07aa284d564..5af884f8d55 100644 --- a/src/main/azure-devops/pull-request-creation.ts +++ b/src/main/azure-devops/pull-request-creation.ts @@ -125,21 +125,24 @@ function classifyCreateError(error: unknown): CreateHostedReviewResult { ok: false, code: 'auth_required', error: - 'Create PR failed: Azure DevOps is not authenticated. Next step: set ORCA_AZURE_DEVOPS_TOKEN in this environment.' + 'Create PR failed: Azure DevOps is not authenticated. Next step: set ORCA_AZURE_DEVOPS_TOKEN in this environment.', + reason: 'auth_required' } } if (status === 409 || lower.includes('already exists') || lower.includes('active pull request')) { return { ok: false, code: 'already_exists', - error: 'A pull request already exists for this branch.' + error: 'A pull request already exists for this branch.', + reason: 'existing_review' } } if (error instanceof HostedReviewApiRequestError && error.timedOut) { return { ok: false, code: 'unknown_completion', - error: 'PR creation may have completed. Refreshing branch review state...' + error: 'PR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } if (status === 400 || status === 422 || lower.includes('validation')) { @@ -147,14 +150,16 @@ function classifyCreateError(error: unknown): CreateHostedReviewResult { ok: false, code: 'validation', error: - 'Create PR failed: Azure DevOps rejected the pull request. Check the base branch and branch state, then try again.' + 'Create PR failed: Azure DevOps rejected the pull request. Check the base branch and branch state, then try again.', + reason: 'rejected' } } return { ok: false, code: 'unknown', error: - 'Create PR failed: Azure DevOps could not create the pull request. Try again in a moment.' + 'Create PR failed: Azure DevOps could not create the pull request. Try again in a moment.', + reason: 'create_failed' } } @@ -176,7 +181,8 @@ export async function createAzureDevOpsPullRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating reviews for this provider is not supported yet.' + error: 'Creating reviews for this provider is not supported yet.', + reason: 'provider_not_supported' } } @@ -188,7 +194,8 @@ export async function createAzureDevOpsPullRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating pull requests requires an Azure DevOps remote.' + error: 'Creating pull requests requires an Azure DevOps remote.', + reason: 'unsupported_provider' } } @@ -206,7 +213,8 @@ export async function createAzureDevOpsPullRequest( return { ok: false, code: 'validation', - error: 'Create PR failed: choose a different base branch before creating a pull request.' + error: 'Create PR failed: choose a different base branch before creating a pull request.', + reason: 'same_base_branch' } } @@ -246,7 +254,8 @@ export async function createAzureDevOpsPullRequest( : { ok: false, code: 'unknown_completion', - error: 'PR creation may have completed. Refreshing branch review state...' + error: 'PR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } catch (error) { const classified = classifyCreateError(error) @@ -260,6 +269,7 @@ export async function createAzureDevOpsPullRequest( ok: false, code: 'already_exists', error: 'A pull request already exists for this branch.', + reason: 'existing_review', existingReview: existing } } diff --git a/src/main/bitbucket/pull-request-creation.test.ts b/src/main/bitbucket/pull-request-creation.test.ts index cd3568e1e11..02a1257aa02 100644 --- a/src/main/bitbucket/pull-request-creation.test.ts +++ b/src/main/bitbucket/pull-request-creation.test.ts @@ -131,7 +131,7 @@ describe('Bitbucket pull request creation', () => { const result = await createBitbucketPullRequest('/repo', CREATE_INPUT, 'local') - expect(result).toMatchObject({ ok: false, code: 'auth_required' }) + expect(result).toMatchObject({ ok: false, code: 'auth_required', reason: 'auth_required' }) expect(!result.ok && result.error).toContain('Settings') }) diff --git a/src/main/bitbucket/pull-request-creation.ts b/src/main/bitbucket/pull-request-creation.ts index d33b74f28ab..b6aa19b022d 100644 --- a/src/main/bitbucket/pull-request-creation.ts +++ b/src/main/bitbucket/pull-request-creation.ts @@ -46,7 +46,8 @@ function classifyCreateError(error: unknown): CreateHostedReviewResult { ok: false, code: 'auth_required', error: - 'Create PR failed: Bitbucket is not authenticated. Next step: connect Bitbucket in Settings > Integrations, or set ORCA_BITBUCKET_* in this environment.' + 'Create PR failed: Bitbucket is not authenticated. Next step: connect Bitbucket in Settings > Integrations, or set ORCA_BITBUCKET_* in this environment.', + reason: 'auth_required' } } // Duplicate branches return HTTP 400, so inspect the message. @@ -54,14 +55,16 @@ function classifyCreateError(error: unknown): CreateHostedReviewResult { return { ok: false, code: 'already_exists', - error: 'A pull request already exists for this branch.' + error: 'A pull request already exists for this branch.', + reason: 'existing_review' } } if (error instanceof HostedReviewApiRequestError && error.timedOut) { return { ok: false, code: 'unknown_completion', - error: 'PR creation may have completed. Refreshing branch review state...' + error: 'PR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } if (status === 400 || status === 422) { @@ -69,7 +72,8 @@ function classifyCreateError(error: unknown): CreateHostedReviewResult { ok: false, code: 'validation', error: - 'Create PR failed: Bitbucket rejected the pull request. Check the base branch and branch state, then try again.' + 'Create PR failed: Bitbucket rejected the pull request. Check the base branch and branch state, then try again.', + reason: 'rejected' } } if (status === 404) { @@ -77,13 +81,15 @@ function classifyCreateError(error: unknown): CreateHostedReviewResult { ok: false, code: 'validation', error: - 'Create PR failed: Bitbucket could not find the repository. Check that the credential has write access to it.' + 'Create PR failed: Bitbucket could not find the repository. Check that the credential has write access to it.', + reason: 'repository_not_found' } } return { ok: false, code: 'unknown', - error: 'Create PR failed: Bitbucket could not create the pull request. Try again in a moment.' + error: 'Create PR failed: Bitbucket could not create the pull request. Try again in a moment.', + reason: 'create_failed' } } @@ -113,7 +119,8 @@ export async function createBitbucketPullRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating reviews for this provider is not supported yet.' + error: 'Creating reviews for this provider is not supported yet.', + reason: 'provider_not_supported' } } @@ -126,7 +133,8 @@ export async function createBitbucketPullRequest( ok: false, code: 'auth_required', error: - 'Create PR failed: Bitbucket is not connected. Next step: connect Bitbucket in Settings > Integrations.' + 'Create PR failed: Bitbucket is not connected. Next step: connect Bitbucket in Settings > Integrations.', + reason: 'auth_required' } } @@ -139,7 +147,8 @@ export async function createBitbucketPullRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating pull requests requires a Bitbucket remote.' + error: 'Creating pull requests requires a Bitbucket remote.', + reason: 'unsupported_provider' } } @@ -157,7 +166,8 @@ export async function createBitbucketPullRequest( return { ok: false, code: 'validation', - error: 'Create PR failed: choose a different base branch before creating a pull request.' + error: 'Create PR failed: choose a different base branch before creating a pull request.', + reason: 'same_base_branch' } } // Ignore saved draft defaults because Bitbucket's composer has no control to clear them. @@ -200,7 +210,8 @@ export async function createBitbucketPullRequest( : { ok: false, code: 'unknown_completion', - error: 'PR creation may have completed. Refreshing branch review state...' + error: 'PR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } catch (error) { const classified = classifyCreateError(error) @@ -216,6 +227,7 @@ export async function createBitbucketPullRequest( ok: false, code: 'already_exists', error: 'A pull request already exists for this branch.', + reason: 'existing_review', existingReview: existing } } diff --git a/src/main/gitea/pull-request-creation.test.ts b/src/main/gitea/pull-request-creation.test.ts index 30a79586db9..7f3dea7ffdb 100644 --- a/src/main/gitea/pull-request-creation.test.ts +++ b/src/main/gitea/pull-request-creation.test.ts @@ -113,7 +113,8 @@ describe('Gitea pull request creation', () => { ) ).resolves.toMatchObject({ ok: false, - code: 'validation' + code: 'validation', + reason: 'rejected' }) }) }) diff --git a/src/main/gitea/pull-request-creation.ts b/src/main/gitea/pull-request-creation.ts index 7761a59dbc1..9bfd9583f89 100644 --- a/src/main/gitea/pull-request-creation.ts +++ b/src/main/gitea/pull-request-creation.ts @@ -70,21 +70,24 @@ function classifyCreateError(error: unknown): CreateHostedReviewResult { ok: false, code: 'auth_required', error: - 'Create PR failed: Gitea is not authenticated. Next step: set ORCA_GITEA_TOKEN in this environment.' + 'Create PR failed: Gitea is not authenticated. Next step: set ORCA_GITEA_TOKEN in this environment.', + reason: 'auth_required' } } if (status === 409 || lower.includes('already exists') || lower.includes('already open')) { return { ok: false, code: 'already_exists', - error: 'A pull request already exists for this branch.' + error: 'A pull request already exists for this branch.', + reason: 'existing_review' } } if (error instanceof HostedReviewApiRequestError && error.timedOut) { return { ok: false, code: 'unknown_completion', - error: 'PR creation may have completed. Refreshing branch review state...' + error: 'PR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } if (status === 400 || status === 422 || lower.includes('validation')) { @@ -92,13 +95,15 @@ function classifyCreateError(error: unknown): CreateHostedReviewResult { ok: false, code: 'validation', error: - 'Create PR failed: Gitea rejected the pull request. Check the base branch and branch state, then try again.' + 'Create PR failed: Gitea rejected the pull request. Check the base branch and branch state, then try again.', + reason: 'rejected' } } return { ok: false, code: 'unknown', - error: 'Create PR failed: Gitea could not create the pull request. Try again in a moment.' + error: 'Create PR failed: Gitea could not create the pull request. Try again in a moment.', + reason: 'create_failed' } } @@ -125,7 +130,8 @@ export async function createGiteaPullRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating reviews for this provider is not supported yet.' + error: 'Creating reviews for this provider is not supported yet.', + reason: 'provider_not_supported' } } @@ -137,7 +143,8 @@ export async function createGiteaPullRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating pull requests requires a Gitea remote.' + error: 'Creating pull requests requires a Gitea remote.', + reason: 'unsupported_provider' } } @@ -155,7 +162,8 @@ export async function createGiteaPullRequest( return { ok: false, code: 'validation', - error: 'Create PR failed: choose a different base branch before creating a pull request.' + error: 'Create PR failed: choose a different base branch before creating a pull request.', + reason: 'same_base_branch' } } @@ -196,7 +204,8 @@ export async function createGiteaPullRequest( : { ok: false, code: 'unknown_completion', - error: 'PR creation may have completed. Refreshing branch review state...' + error: 'PR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } catch (error) { const classified = classifyCreateError(error) @@ -210,6 +219,7 @@ export async function createGiteaPullRequest( ok: false, code: 'already_exists', error: 'A pull request already exists for this branch.', + reason: 'existing_review', existingReview: existing } } diff --git a/src/main/github/client-create-pr.test.ts b/src/main/github/client-create-pr.test.ts index 20b342a80bc..9e2f41c7dc1 100644 --- a/src/main/github/client-create-pr.test.ts +++ b/src/main/github/client-create-pr.test.ts @@ -387,6 +387,7 @@ describe('createGitHubPullRequest', () => { ok: false, code: 'already_exists', error: 'A pull request already exists for this branch.', + reason: 'existing_review', existingReview: { number: 44, url: 'https://github.com/acme/widgets/pull/44' @@ -432,7 +433,8 @@ describe('createGitHubPullRequest', () => { ).resolves.toEqual({ ok: false, code: 'validation', - error: 'Create PR failed: choose a different base branch before creating a pull request.' + error: 'Create PR failed: choose a different base branch before creating a pull request.', + reason: 'same_base_branch' }) expect(ghExecFileAsyncMock).not.toHaveBeenCalled() diff --git a/src/main/github/client/create/create-github-pull-request.ts b/src/main/github/client/create/create-github-pull-request.ts index 915451b8f58..40728c330ed 100644 --- a/src/main/github/client/create/create-github-pull-request.ts +++ b/src/main/github/client/create/create-github-pull-request.ts @@ -35,7 +35,8 @@ export async function createGitHubPullRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating reviews for this provider is not supported yet.' + error: 'Creating reviews for this provider is not supported yet.', + reason: 'provider_not_supported' } } @@ -52,7 +53,8 @@ export async function createGitHubPullRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating pull requests requires a GitHub remote.' + error: 'Creating pull requests requires a GitHub remote.', + reason: 'unsupported_provider' } } // The runner qualifies --repo using options.host. @@ -72,7 +74,8 @@ export async function createGitHubPullRequest( return { ok: false, code: 'validation', - error: 'Create PR failed: choose a different base branch before creating a pull request.' + error: 'Create PR failed: choose a different base branch before creating a pull request.', + reason: 'same_base_branch' } } @@ -133,7 +136,8 @@ export async function createGitHubPullRequest( return { ok: false, code: 'unknown_completion', - error: 'PR creation may have completed. Refreshing branch review state...' + error: 'PR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } catch (error) { const classified = classifyCreatePRError(error) @@ -155,6 +159,7 @@ export async function createGitHubPullRequest( ok: false, code: 'already_exists', error: 'A pull request already exists for this branch.', + reason: 'existing_review', existingReview: existing } } diff --git a/src/main/github/client/create/create-pr-error-classification.ts b/src/main/github/client/create/create-pr-error-classification.ts index 18ae0553ce3..012b2becde3 100644 --- a/src/main/github/client/create/create-pr-error-classification.ts +++ b/src/main/github/client/create/create-pr-error-classification.ts @@ -18,21 +18,24 @@ export function classifyCreatePRError(error: unknown): CreateHostedReviewResult ok: false, code: 'auth_required', error: - 'Create PR failed: GitHub is not authenticated. Next step: run gh auth login in this environment.' + 'Create PR failed: GitHub is not authenticated. Next step: run gh auth login in this environment.', + reason: 'auth_required' } } if (lower.includes('already exists') || lower.includes('a pull request already exists')) { return { ok: false, code: 'already_exists', - error: 'A pull request already exists for this branch.' + error: 'A pull request already exists for this branch.', + reason: 'existing_review' } } if (lower.includes('timed out') || lower.includes('timeout')) { return { ok: false, code: 'unknown_completion', - error: 'PR creation may have completed. Refreshing branch review state...' + error: 'PR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } if (lower.includes('validation failed') || lower.includes('http 422')) { @@ -40,13 +43,15 @@ export function classifyCreatePRError(error: unknown): CreateHostedReviewResult ok: false, code: 'validation', error: - 'Create PR failed: GitHub rejected the pull request. Check the base branch and branch state, then try again.' + 'Create PR failed: GitHub rejected the pull request. Check the base branch and branch state, then try again.', + reason: 'rejected' } } return { ok: false, code: 'unknown', - error: 'Create PR failed: GitHub could not create the pull request. Try again in a moment.' + error: 'Create PR failed: GitHub could not create the pull request. Try again in a moment.', + reason: 'create_failed' } } diff --git a/src/main/gitlab/merge-request-creation.test.ts b/src/main/gitlab/merge-request-creation.test.ts index fea76d9bae9..6acb317068e 100644 --- a/src/main/gitlab/merge-request-creation.test.ts +++ b/src/main/gitlab/merge-request-creation.test.ts @@ -212,6 +212,7 @@ describe('createGitLabMergeRequest', () => { ok: false, code: 'already_exists', error: 'A merge request already exists for this branch.', + reason: 'existing_review', existingReview: { number: 77, url: 'https://gitlab.com/acme/widgets/-/merge_requests/77' @@ -286,7 +287,7 @@ describe('createGitLabMergeRequest', () => { }, 'ssh:ssh-1' ) - ).resolves.toMatchObject({ ok: false, code: 'already_exists' }) + ).resolves.toMatchObject({ ok: false, code: 'already_exists', reason: 'existing_review' }) const [args, options] = glabExecFileAsyncMock.mock.calls[1] expect(args).not.toContain('--hostname') diff --git a/src/main/gitlab/merge-request-creation.ts b/src/main/gitlab/merge-request-creation.ts index ca3f71a442e..84ae80b5220 100644 --- a/src/main/gitlab/merge-request-creation.ts +++ b/src/main/gitlab/merge-request-creation.ts @@ -50,21 +50,24 @@ function classifyCreateMRError(error: unknown): CreateHostedReviewResult { ok: false, code: 'auth_required', error: - 'Create MR failed: GitLab is not authenticated. Next step: run glab auth login in this environment.' + 'Create MR failed: GitLab is not authenticated. Next step: run glab auth login in this environment.', + reason: 'auth_required' } } if (lower.includes('already exists') || lower.includes('merge request already exists')) { return { ok: false, code: 'already_exists', - error: 'A merge request already exists for this branch.' + error: 'A merge request already exists for this branch.', + reason: 'existing_review' } } if (lower.includes('timed out') || lower.includes('timeout')) { return { ok: false, code: 'unknown_completion', - error: 'MR creation may have completed. Refreshing branch review state...' + error: 'MR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } if (lower.includes('validation failed') || lower.includes('http 422')) { @@ -72,13 +75,15 @@ function classifyCreateMRError(error: unknown): CreateHostedReviewResult { ok: false, code: 'validation', error: - 'Create MR failed: GitLab rejected the merge request. Check the base branch and branch state, then try again.' + 'Create MR failed: GitLab rejected the merge request. Check the base branch and branch state, then try again.', + reason: 'rejected' } } return { ok: false, code: 'unknown', - error: 'Create MR failed: GitLab could not create the merge request. Try again in a moment.' + error: 'Create MR failed: GitLab could not create the merge request. Try again in a moment.', + reason: 'create_failed' } } @@ -121,7 +126,8 @@ export async function createGitLabMergeRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating reviews for this provider is not supported yet.' + error: 'Creating reviews for this provider is not supported yet.', + reason: 'provider_not_supported' } } @@ -137,7 +143,8 @@ export async function createGitLabMergeRequest( return { ok: false, code: 'unsupported_provider', - error: 'Creating merge requests requires a GitLab remote.' + error: 'Creating merge requests requires a GitLab remote.', + reason: 'unsupported_provider' } } @@ -155,7 +162,8 @@ export async function createGitLabMergeRequest( return { ok: false, code: 'validation', - error: 'Create MR failed: choose a different base branch before creating a merge request.' + error: 'Create MR failed: choose a different base branch before creating a merge request.', + reason: 'same_base_branch' } } @@ -212,7 +220,8 @@ export async function createGitLabMergeRequest( return { ok: false, code: 'unknown_completion', - error: 'MR creation may have completed. Refreshing branch review state...' + error: 'MR creation may have completed. Refreshing branch review state...', + reason: 'creation_unconfirmed' } } catch (error) { const classified = classifyCreateMRError(error) @@ -234,6 +243,7 @@ export async function createGitLabMergeRequest( ok: false, code: 'already_exists', error: 'A merge request already exists for this branch.', + reason: 'existing_review', existingReview: existing } } diff --git a/src/main/source-control/hosted-review-creation-blocking.ts b/src/main/source-control/hosted-review-creation-blocking.ts index 455d55049ed..fc14144d07e 100644 --- a/src/main/source-control/hosted-review-creation-blocking.ts +++ b/src/main/source-control/hosted-review-creation-blocking.ts @@ -17,52 +17,63 @@ function blockedCreateResultForReason( auth_required: { ok: false, code: 'auth_required', - error: `Create ${copy.shortLabel} failed: ${copy.providerName} is not authenticated. Next step: ${copy.authInstruction} in this environment.` + error: `Create ${copy.shortLabel} failed: ${copy.providerName} is not authenticated. Next step: ${copy.authInstruction} in this environment.`, + reason: 'auth_required' }, unsupported_provider: { ok: false, code: 'unsupported_provider', - error: `Creating ${copy.reviewLabel}s requires a ${copy.providerName} remote.` + error: `Creating ${copy.reviewLabel}s requires a ${copy.providerName} remote.`, + reason: 'unsupported_provider' }, dirty: { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: commit or discard local changes before creating a ${copy.reviewLabel}.` + error: `Create ${copy.shortLabel} failed: commit or discard local changes before creating a ${copy.reviewLabel}.`, + reason: 'dirty' }, detached_head: { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: switch to a branch before creating a ${copy.reviewLabel}.` + error: `Create ${copy.shortLabel} failed: switch to a branch before creating a ${copy.reviewLabel}.`, + reason: 'detached_head' }, default_branch: { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: choose a feature branch before creating a ${copy.reviewLabel}.` + error: `Create ${copy.shortLabel} failed: choose a feature branch before creating a ${copy.reviewLabel}.`, + reason: 'default_branch' }, no_upstream: { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: publish this branch before creating a ${copy.reviewLabel}.` + error: `Create ${copy.shortLabel} failed: publish this branch before creating a ${copy.reviewLabel}.`, + reason: 'no_upstream' }, needs_push: { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: push this branch before creating a ${copy.reviewLabel}.` + error: `Create ${copy.shortLabel} failed: push this branch before creating a ${copy.reviewLabel}.`, + reason: 'needs_push' }, needs_sync: { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: sync this branch before creating a ${copy.reviewLabel}.` + error: `Create ${copy.shortLabel} failed: sync this branch before creating a ${copy.reviewLabel}.`, + reason: 'needs_sync' }, fork_head_unsupported: { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: refresh source control status and try again.` + error: `Create ${copy.shortLabel} failed: refresh source control status and try again.`, + reason: 'fork_head_unsupported' }, base_not_on_remote: { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: the base branch ${baseLabel}hasn't been pushed to the remote. Choose a pushed base or push it first.` + error: `Create ${copy.shortLabel} failed: the base branch ${baseLabel}hasn't been pushed to the remote. Choose a pushed base or push it first.`, + reason: 'base_not_on_remote', + ...(submittedBase?.trim() ? { base: submittedBase.trim() } : {}) } } satisfies Partial< Record, CreateHostedReviewResult> @@ -83,6 +94,7 @@ export function blockedEligibilityToCreateResult( ok: false, code: 'already_exists', error: `A ${copy.reviewLabel} already exists for this branch.`, + reason: 'existing_review', existingReview: eligibility.review } } @@ -97,6 +109,7 @@ export function blockedEligibilityToCreateResult( return { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: refresh source control status and try again.` + error: `Create ${copy.shortLabel} failed: refresh source control status and try again.`, + reason: 'branch_status_unverified' } } diff --git a/src/main/source-control/hosted-review-creation.test.ts b/src/main/source-control/hosted-review-creation.test.ts index 34834aee94c..5298f081c63 100644 --- a/src/main/source-control/hosted-review-creation.test.ts +++ b/src/main/source-control/hosted-review-creation.test.ts @@ -286,7 +286,8 @@ describe('createHostedReview', () => { ).resolves.toEqual({ ok: false, code: 'validation', - error: 'Create PR failed: push this branch before creating a pull request.' + error: 'Create PR failed: push this branch before creating a pull request.', + reason: 'needs_push' }) expect(createGitHubPullRequestMock).not.toHaveBeenCalled() }) @@ -313,7 +314,8 @@ describe('createHostedReview', () => { ).resolves.toEqual({ ok: false, code: 'validation', - error: 'Create PR failed: switch back to the selected branch before creating a pull request.' + error: 'Create PR failed: switch back to the selected branch before creating a pull request.', + reason: 'branch_changed' }) expect(createGitHubPullRequestMock).not.toHaveBeenCalled() }) @@ -344,7 +346,9 @@ describe('createHostedReview', () => { ok: false, code: 'validation', error: - 'Create PR failed: the base branch "stacked-parent" hasn\'t been pushed to the remote. Choose a pushed base or push it first.' + 'Create PR failed: the base branch "stacked-parent" hasn\'t been pushed to the remote. Choose a pushed base or push it first.', + reason: 'base_not_on_remote', + base: 'stacked-parent' }) expect(createGitHubPullRequestMock).not.toHaveBeenCalled() }) @@ -583,6 +587,7 @@ describe('createHostedReview', () => { ok: false, code: 'already_exists', error: 'A pull request already exists for this branch.', + reason: 'existing_review', existingReview: { number: 31, url: 'https://github.com/acme/orca/pull/31' diff --git a/src/main/source-control/hosted-review-creation.ts b/src/main/source-control/hosted-review-creation.ts index 414b1cb1254..7819ea31ff8 100644 --- a/src/main/source-control/hosted-review-creation.ts +++ b/src/main/source-control/hosted-review-creation.ts @@ -53,7 +53,8 @@ async function validateCurrentBranchCanCreateReview( return { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: switch back to the selected branch before creating a ${copy.reviewLabel}.` + error: `Create ${copy.shortLabel} failed: switch back to the selected branch before creating a ${copy.reviewLabel}.`, + reason: 'branch_changed' } } @@ -81,7 +82,8 @@ async function validateCurrentBranchCanCreateReview( return { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: Orca could not confirm whether this branch already has a ${copy.reviewLabel}. Retry once the ${copy.providerName} lookup succeeds.` + error: `Create ${copy.shortLabel} failed: Orca could not confirm whether this branch already has a ${copy.reviewLabel}. Retry once the ${copy.providerName} lookup succeeds.`, + reason: 'review_lookup_unavailable' } } // Recheck stale renderer eligibility in main before submission. @@ -91,7 +93,8 @@ async function validateCurrentBranchCanCreateReview( return { ok: false, code: 'validation', - error: `Create ${copy.shortLabel} failed: could not verify branch status. Refresh source control and try again.` + error: `Create ${copy.shortLabel} failed: could not verify branch status. Refresh source control and try again.`, + reason: 'branch_status_unverified' } } } @@ -239,7 +242,8 @@ export async function getHostedReviewCreationEligibility( export const UNSUPPORTED_HOSTED_REVIEW_PROVIDER: CreateHostedReviewResult = { ok: false, code: 'unsupported_provider', - error: 'Creating reviews for this provider is not supported yet.' + error: 'Creating reviews for this provider is not supported yet.', + reason: 'provider_not_supported' } export async function createHostedReview( @@ -261,7 +265,8 @@ export async function createHostedReview( return { ok: false, code: 'unsupported_provider', - error: `Creating ${copy.reviewLabel}s requires a ${copy.providerName} remote.` + error: `Creating ${copy.reviewLabel}s requires a ${copy.providerName} remote.`, + reason: 'unsupported_provider' } } const blocked = await validateCurrentBranchCanCreateReview( diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.tsx index e949627cf07..3a504185dfd 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.tsx @@ -150,7 +150,12 @@ export function useChecksPanelCreateReview(model: ChecksPanelCreateReviewInput) if (shouldPushBeforeCreate) { const ok = await pushBeforeCreatePullRequest() if (!ok) { - showCreateError('Push failed. Resolve the push error, then try again.') + showCreateError( + translate( + 'sourceControl.createReviewFailure.pushFailed', + 'Push failed. Resolve the push error, then try again.' + ) + ) return } pushed = true @@ -237,7 +242,7 @@ export function useChecksPanelCreateReview(model: ChecksPanelCreateReviewInput) ) } } - showCreateError(formatCreateError(result, pushed, hostedReviewCreateCopy.shortLabel)) + showCreateError(formatCreateError(result, pushed, hostedReviewCreateProvider)) } catch (error) { showCreateError( error instanceof Error @@ -268,7 +273,6 @@ export function useChecksPanelCreateReview(model: ChecksPanelCreateReviewInput) handlePullRequestCreated, hostedReviewCreateCopy.providerName, hostedReviewCreateCopy.reviewLabel, - hostedReviewCreateCopy.shortLabel, hostedReviewCreateCopy.titleLabel, hostedReviewCreateProvider, hostedReviewCreation?.blockedReason, diff --git a/src/renderer/src/components/right-sidebar/create-pull-request-review-copy.ts b/src/renderer/src/components/right-sidebar/create-pull-request-review-copy.ts index bedabfb6205..ddcd7fe1c19 100644 --- a/src/renderer/src/components/right-sidebar/create-pull-request-review-copy.ts +++ b/src/renderer/src/components/right-sidebar/create-pull-request-review-copy.ts @@ -1,8 +1,17 @@ import type { CreateHostedReviewResult, - CreateStackedHostedReviewResult + CreateStackedHostedReviewResult, + HostedReviewProvider } from '../../../../shared/hosted-review' import { translate } from '@/i18n/i18n' +import { + localizedHostedReviewCopy, + resolveSupportedHostedReviewCopyProvider +} from '@/i18n/hosted-review-localized-copy' +import { + localizedCreateFailure, + localizedCreateFailureDetail +} from './hosted-review-create-failure-copy' export type { LocalizedHostedReviewCopy as CreatePullRequestReviewCopy } from '@/i18n/hosted-review-localized-copy' @@ -11,21 +20,21 @@ export { localizedHostedReviewCopy as reviewCopy } from '@/i18n/hosted-review-lo export function formatCreateError( result: CreateHostedReviewResult | CreateStackedHostedReviewResult, pushed: boolean, - shortLabel: string + provider: HostedReviewProvider ): string { if (result.ok) { return '' } - if (pushed) { - const prefix = new RegExp(`^Create ${shortLabel} failed:\\s*`, 'i') - return translate( - 'auto.components.right.sidebar.create.pull.request.review.copy.a1f8c3d2e4', - 'Push succeeded, but {{value0}} creation failed: {{value1}}', - { - value0: shortLabel, - value1: result.error.replace(prefix, '') - } - ) + if (!pushed) { + return localizedCreateFailure(result, provider) } - return result.error + return translate( + 'auto.components.right.sidebar.create.pull.request.review.copy.a1f8c3d2e4', + 'Push succeeded, but {{value0}} creation failed: {{value1}}', + { + value0: localizedHostedReviewCopy(resolveSupportedHostedReviewCopyProvider(provider)) + .shortLabel, + value1: localizedCreateFailureDetail(result, provider) + } + ) } diff --git a/src/renderer/src/components/right-sidebar/hosted-review-create-failure-copy.test.ts b/src/renderer/src/components/right-sidebar/hosted-review-create-failure-copy.test.ts new file mode 100644 index 00000000000..df16ee23cdc --- /dev/null +++ b/src/renderer/src/components/right-sidebar/hosted-review-create-failure-copy.test.ts @@ -0,0 +1,105 @@ +import { afterEach, describe, expect, it } from 'vitest' +import type { + CreateHostedReviewResult, + HostedReviewProvider +} from '../../../../shared/hosted-review' +import { i18n } from '@/i18n/i18n' +import { localizedCreateFailure } from './hosted-review-create-failure-copy' +import { formatCreateError } from './create-pull-request-review-copy' + +type FailedResult = Extract + +function failed(overrides: Partial = {}): FailedResult { + return { ok: false, code: 'validation', error: 'English text from the host.', ...overrides } +} + +afterEach(async () => { + await i18n.changeLanguage('en') +}) + +describe('localizedCreateFailure', () => { + it.each<[HostedReviewProvider, string]>([ + [ + 'github', + 'Create PR failed: GitHub is not authenticated. Next step: run gh auth login in this environment.' + ], + [ + 'gitlab', + 'Create MR failed: GitLab is not authenticated. Next step: run glab auth login in this environment.' + ], + [ + 'azure-devops', + 'Create PR failed: Azure DevOps is not authenticated. Next step: set ORCA_AZURE_DEVOPS_TOKEN in this environment.' + ], + [ + 'gitea', + 'Create PR failed: Gitea is not authenticated. Next step: set ORCA_GITEA_TOKEN in this environment.' + ], + [ + 'bitbucket', + 'Create PR failed: Bitbucket is not authenticated. Next step: connect Bitbucket in Settings > Integrations, or set ORCA_BITBUCKET_* in this environment.' + ] + ])('gives %s its own sign-in step', (provider, expected) => { + expect( + localizedCreateFailure(failed({ code: 'auth_required', reason: 'auth_required' }), provider) + ).toBe(expected) + }) + + it('names the base branch that is missing from the remote', () => { + expect( + localizedCreateFailure( + failed({ reason: 'base_not_on_remote', base: 'stacked-parent' }), + 'github' + ) + ).toBe( + 'Create PR failed: the base branch "stacked-parent" hasn\'t been pushed to the remote. Choose a pushed base or push it first.' + ) + expect(localizedCreateFailure(failed({ reason: 'base_not_on_remote' }), 'github')).toBe( + "Create PR failed: the base branch hasn't been pushed to the remote. Choose a pushed base or push it first." + ) + }) + + it.each<[FailedResult['reason'], string]>([ + ['existing_review', 'A merge request already exists for this branch.'], + ['creation_unconfirmed', 'MR creation may have completed. Refreshing branch review state...'], + ['unsupported_provider', 'Creating a merge request requires a remote hosted on GitLab.'] + ])('shows %s as a sentence of its own', (reason, expected) => { + expect(localizedCreateFailure(failed({ reason }), 'gitlab')).toBe(expected) + }) + + it("shows an older host's English error when the result has no reason", () => { + expect(localizedCreateFailure(failed(), 'github')).toBe('English text from the host.') + }) + + it("shows the host's English error for a reason this build does not know", () => { + const fromNewerHost: FailedResult = JSON.parse( + '{"ok":false,"code":"validation","error":"English text from the host.","reason":"future_reason"}' + ) + expect(localizedCreateFailure(fromNewerHost, 'github')).toBe('English text from the host.') + }) + + it('follows the UI language', async () => { + await i18n.changeLanguage('ja') + expect(localizedCreateFailure(failed({ reason: 'dirty' }), 'gitlab')).toBe( + 'MR の作成に失敗しました: MR を作成する前に、ローカルの変更をコミットするか破棄してください。' + ) + }) +}) + +describe('formatCreateError after a successful push', () => { + it('reports the localized detail without repeating the create-failed lead-in', () => { + expect(formatCreateError(failed({ reason: 'needs_push' }), true, 'github')).toBe( + 'Push succeeded, but PR creation failed: push this branch before creating a pull request.' + ) + }) + + it("strips the English lead-in from an older host's error", () => { + expect( + formatCreateError( + failed({ error: 'Create MR failed: GitLab could not create the merge request.' }), + true, + 'gitlab' + ) + ).toBe('Push succeeded, but MR creation failed: GitLab could not create the merge request.') + }) +}) diff --git a/src/renderer/src/components/right-sidebar/hosted-review-create-failure-copy.ts b/src/renderer/src/components/right-sidebar/hosted-review-create-failure-copy.ts new file mode 100644 index 00000000000..adcc65d7b01 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/hosted-review-create-failure-copy.ts @@ -0,0 +1,305 @@ +import type { + CreateHostedReviewFailureReason, + CreateHostedReviewResult, + CreateStackedHostedReviewResult, + HostedReviewProvider +} from '../../../../shared/hosted-review' +import { translate } from '@/i18n/i18n' +import { + localizedHostedReviewCopy, + resolveSupportedHostedReviewCopyProvider, + type LocalizedHostedReviewCopy, + type SupportedHostedReviewCopyProvider +} from '@/i18n/hosted-review-localized-copy' + +type FailedCreateResult = + | Extract + | Extract + +/** `detail` follows "Create PR failed:"; `message` stands alone. */ +type CreateFailureCopy = { kind: 'detail' | 'message'; text: string } + +// Commands and variable names stay untranslated. +const SIGN_IN_COMMANDS = { github: 'gh auth login', gitlab: 'glab auth login' } as const +const SIGN_IN_VARIABLES = { + 'azure-devops': 'ORCA_AZURE_DEVOPS_TOKEN', + gitea: 'ORCA_GITEA_TOKEN' +} as const + +function authRequiredDetail( + provider: SupportedHostedReviewCopyProvider, + copy: LocalizedHostedReviewCopy +): string { + if (provider === 'bitbucket') { + return translate( + 'sourceControl.createReviewFailure.authRequiredBitbucket', + 'Bitbucket is not authenticated. Next step: connect Bitbucket in Settings > Integrations, or set ORCA_BITBUCKET_* in this environment.' + ) + } + if (provider === 'github' || provider === 'gitlab') { + return translate( + 'sourceControl.createReviewFailure.authRequiredCommand', + '{{providerName}} is not authenticated. Next step: run {{command}} in this environment.', + { providerName: copy.providerName, command: SIGN_IN_COMMANDS[provider] } + ) + } + return translate( + 'sourceControl.createReviewFailure.authRequiredVariable', + '{{providerName}} is not authenticated. Next step: set {{variable}} in this environment.', + { providerName: copy.providerName, variable: SIGN_IN_VARIABLES[provider] } + ) +} + +/** Short sign-in step for space-constrained surfaces like the Create PR dropdown hint. */ +export function hostedReviewSignInHint(provider: HostedReviewProvider | undefined): string { + const supported = resolveSupportedHostedReviewCopyProvider(provider) + if (supported === 'bitbucket') { + return translate( + 'sourceControl.createReviewFailure.signInHintBitbucket', + 'Connect Bitbucket in Settings > Integrations' + ) + } + if (supported === 'github' || supported === 'gitlab') { + return translate( + 'sourceControl.createReviewFailure.signInHintCommand', + 'Run {{command}} in this environment', + { command: SIGN_IN_COMMANDS[supported] } + ) + } + return translate( + 'sourceControl.createReviewFailure.signInHintVariable', + 'Set {{variable}} in this environment', + { variable: SIGN_IN_VARIABLES[supported] } + ) +} + +// Returns undefined for a reason from a newer host that this build does not know. +function createFailureCopy( + reason: CreateHostedReviewFailureReason, + provider: SupportedHostedReviewCopyProvider, + copy: LocalizedHostedReviewCopy, + base: string | undefined +): CreateFailureCopy | undefined { + const vars = { + reviewLabel: copy.reviewLabel, + shortLabel: copy.shortLabel, + providerName: copy.providerName + } + const detail = (text: string): CreateFailureCopy => ({ kind: 'detail', text }) + const message = (text: string): CreateFailureCopy => ({ kind: 'message', text }) + switch (reason) { + case 'auth_required': + return detail(authRequiredDetail(provider, copy)) + case 'dirty': + return detail( + translate( + 'sourceControl.createReviewFailure.dirty', + 'commit or discard local changes before creating a {{reviewLabel}}.', + vars + ) + ) + case 'detached_head': + return detail( + translate( + 'sourceControl.createReviewFailure.detachedHead', + 'switch to a branch before creating a {{reviewLabel}}.', + vars + ) + ) + case 'default_branch': + return detail( + translate( + 'sourceControl.createReviewFailure.defaultBranch', + 'choose a feature branch before creating a {{reviewLabel}}.', + vars + ) + ) + case 'no_upstream': + return detail( + translate( + 'sourceControl.createReviewFailure.noUpstream', + 'publish this branch before creating a {{reviewLabel}}.', + vars + ) + ) + case 'needs_push': + return detail( + translate( + 'sourceControl.createReviewFailure.needsPush', + 'push this branch before creating a {{reviewLabel}}.', + vars + ) + ) + case 'needs_sync': + return detail( + translate( + 'sourceControl.createReviewFailure.needsSync', + 'sync this branch before creating a {{reviewLabel}}.', + vars + ) + ) + case 'fork_head_unsupported': + return detail( + translate( + 'sourceControl.createReviewFailure.refreshStatus', + 'refresh source control status and try again.' + ) + ) + case 'base_not_on_remote': + return detail( + base + ? translate( + 'sourceControl.createReviewFailure.namedBaseNotOnRemote', + 'the base branch "{{base}}" hasn\'t been pushed to the remote. Choose a pushed base or push it first.', + { base } + ) + : translate( + 'sourceControl.createReviewFailure.baseNotOnRemote', + "the base branch hasn't been pushed to the remote. Choose a pushed base or push it first." + ) + ) + case 'branch_changed': + return detail( + translate( + 'sourceControl.createReviewFailure.branchChanged', + 'switch back to the selected branch before creating a {{reviewLabel}}.', + vars + ) + ) + case 'branch_status_unverified': + return detail( + translate( + 'sourceControl.createReviewFailure.branchStatusUnverified', + 'could not verify branch status. Refresh source control and try again.' + ) + ) + case 'review_lookup_unavailable': + return detail( + translate( + 'sourceControl.createReviewFailure.reviewLookupUnavailable', + 'Orca could not confirm whether this branch already has a {{reviewLabel}}. Retry once the {{providerName}} lookup succeeds.', + vars + ) + ) + case 'same_base_branch': + return detail( + translate( + 'sourceControl.createReviewFailure.sameBaseBranch', + 'choose a different base branch before creating a {{reviewLabel}}.', + vars + ) + ) + case 'rejected': + return detail( + translate( + 'sourceControl.createReviewFailure.rejected', + '{{providerName}} rejected the {{reviewLabel}}. Check the base branch and branch state, then try again.', + vars + ) + ) + case 'repository_not_found': + return detail( + translate( + 'sourceControl.createReviewFailure.repositoryNotFound', + '{{providerName}} could not find the repository. Check that the credential has write access to it.', + vars + ) + ) + case 'create_failed': + return detail( + translate( + 'sourceControl.createReviewFailure.createFailed', + '{{providerName}} could not create the {{reviewLabel}}. Try again in a moment.', + vars + ) + ) + case 'unsupported_provider': + return message( + translate( + 'sourceControl.createReviewFailure.unsupportedProvider', + 'Creating a {{reviewLabel}} requires a remote hosted on {{providerName}}.', + vars + ) + ) + case 'provider_not_supported': + return message( + translate( + 'sourceControl.createReviewFailure.providerNotSupported', + 'Creating reviews for this provider is not supported yet.' + ) + ) + case 'existing_review': + return message( + translate( + 'sourceControl.createReviewFailure.existingReview', + 'A {{reviewLabel}} already exists for this branch.', + vars + ) + ) + case 'creation_unconfirmed': + return message( + translate( + 'sourceControl.createReviewFailure.creationUnconfirmed', + '{{shortLabel}} creation may have completed. Refreshing branch review state...', + vars + ) + ) + } +} + +function failureCopyFor( + reason: CreateHostedReviewFailureReason, + provider: HostedReviewProvider, + base?: string +): { copy: CreateFailureCopy; shortLabel: string } | null { + const supported = resolveSupportedHostedReviewCopyProvider(provider) + const labels = localizedHostedReviewCopy(supported) + const copy = createFailureCopy(reason, supported, labels, base) + return copy ? { copy, shortLabel: labels.shortLabel } : null +} + +/** The sentence for an Orca-authored create failure in the UI language, or null for an unknown reason. */ +export function hostedReviewCreateFailureMessage( + reason: CreateHostedReviewFailureReason, + provider: HostedReviewProvider, + base?: string +): string | null { + const resolved = failureCopyFor(reason, provider, base) + if (!resolved) { + return null + } + if (resolved.copy.kind === 'message') { + return resolved.copy.text + } + return translate( + 'sourceControl.createReviewFailure.withDetail', + 'Create {{shortLabel}} failed: {{detail}}', + { shortLabel: resolved.shortLabel, detail: resolved.copy.text } + ) +} + +// Hosts that send no reason only have the English sentence to strip. +const ENGLISH_CREATE_FAILED_PREFIX = /^Create (?:PR|MR) failed:\s*/i + +/** A failed create result in the UI language; hosts that send no reason keep their English `error`. */ +export function localizedCreateFailure( + result: FailedCreateResult, + provider: HostedReviewProvider +): string { + return ( + (result.reason + ? hostedReviewCreateFailureMessage(result.reason, provider, result.base) + : null) ?? result.error + ) +} + +/** Like localizedCreateFailure, without the "Create PR failed:" lead-in. */ +export function localizedCreateFailureDetail( + result: FailedCreateResult, + provider: HostedReviewProvider +): string { + return ( + (result.reason ? failureCopyFor(result.reason, provider, result.base)?.copy.text : null) ?? + result.error.replace(ENGLISH_CREATE_FAILED_PREFIX, '') + ) +} diff --git a/src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.test.ts b/src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.test.ts index c40468b9e49..5a588bfee3b 100644 --- a/src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.test.ts @@ -1,8 +1,7 @@ import { describe, expect, it } from 'vitest' import { canClickBlockedCreateReviewReason, - resolveBlockedCreateReviewNoticeMessage, - resolveUnavailableCreateReviewLookupNoticeMessage + resolveBlockedCreateReviewNoticeMessage } from './source-control-create-review-blocked-action' import type { HostedReviewCreationEligibility } from '../../../../shared/hosted-review' @@ -53,7 +52,7 @@ describe('source-control-create-review-blocked-action', () => { }) ) ).toBe( - 'Create MR failed: GitLab is not authenticated. Next step: Run glab auth login in this environment.' + 'Create MR failed: GitLab is not authenticated. Next step: run glab auth login in this environment.' ) }) @@ -86,14 +85,11 @@ describe('source-control-create-review-blocked-action', () => { }) ) ).toBe( - 'Create MR failed: GitLab is not authenticated. Next step: Run glab auth login in this environment.' + 'Create MR failed: GitLab is not authenticated. Next step: run glab auth login in this environment.' ) }) it('reports unavailable review lookup authority when no local blocker is known', () => { - expect(resolveUnavailableCreateReviewLookupNoticeMessage('gitlab')).toBe( - 'Create MR failed: Orca could not confirm whether this branch already has a merge request. Retry once the GitLab lookup succeeds.' - ) expect( resolveBlockedCreateReviewNoticeMessage( eligibility({ diff --git a/src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.ts b/src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.ts index 5165eb79433..7ae7eedfd16 100644 --- a/src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.ts +++ b/src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.ts @@ -1,16 +1,17 @@ import type { HostedReviewCreationBlockedReason, - HostedReviewCreationEligibility, - HostedReviewProvider + HostedReviewCreationEligibility } from '../../../../shared/hosted-review' -import { - localizedHostedReviewCopy, - resolveSupportedHostedReviewCopyProvider -} from '@/i18n/hosted-review-localized-copy' +import { hostedReviewCreateFailureMessage } from './hosted-review-create-failure-copy' + +type ClickableBlockedCreateReviewReason = Extract< + HostedReviewCreationBlockedReason, + 'dirty' | 'default_branch' | 'no_upstream' | 'needs_push' | 'needs_sync' | 'auth_required' +> export function canClickBlockedCreateReviewReason( reason: HostedReviewCreationBlockedReason | undefined -): boolean { +): reason is ClickableBlockedCreateReviewReason { // Keep actionable blockers clickable so inline guidance can explain the next step. return ( reason === 'dirty' || @@ -22,29 +23,6 @@ export function canClickBlockedCreateReviewReason( ) } -export function resolveHostedReviewAuthInstruction(provider: HostedReviewProvider): string { - if (provider === 'gitlab') { - return 'Run glab auth login' - } - if (provider === 'azure-devops') { - return 'Set ORCA_AZURE_DEVOPS_TOKEN' - } - if (provider === 'gitea') { - return 'Set ORCA_GITEA_TOKEN' - } - if (provider === 'bitbucket') { - return 'Connect Bitbucket in Settings > Integrations' - } - return 'Run gh auth login' -} - -export function resolveUnavailableCreateReviewLookupNoticeMessage( - provider: HostedReviewProvider -): string { - const copy = localizedHostedReviewCopy(resolveSupportedHostedReviewCopyProvider(provider)) - return `Create ${copy.shortLabel} failed: Orca could not confirm whether this branch already has a ${copy.reviewLabel}. Retry once the ${copy.providerName} lookup succeeds.` -} - export function resolveBlockedCreateReviewNoticeMessage( eligibility: HostedReviewCreationEligibility | null | undefined ): string | null { @@ -53,35 +31,10 @@ export function resolveBlockedCreateReviewNoticeMessage( } const reason = eligibility.blockedReason if (eligibility.reviewLookupOutcome === 'unavailable' && reason === null) { - return resolveUnavailableCreateReviewLookupNoticeMessage(eligibility.provider) + return hostedReviewCreateFailureMessage('review_lookup_unavailable', eligibility.provider) } if (!canClickBlockedCreateReviewReason(reason)) { return null } - const copy = localizedHostedReviewCopy( - resolveSupportedHostedReviewCopyProvider(eligibility.provider) - ) - switch (reason) { - case 'dirty': - return `Create ${copy.shortLabel} failed: commit or discard local changes before creating a ${copy.reviewLabel}.` - case 'default_branch': - return `Create ${copy.shortLabel} failed: choose a feature branch before creating a ${copy.reviewLabel}.` - case 'no_upstream': - return `Create ${copy.shortLabel} failed: publish this branch before creating a ${copy.reviewLabel}.` - case 'needs_push': - return `Create ${copy.shortLabel} failed: push this branch before creating a ${copy.reviewLabel}.` - case 'needs_sync': - return `Create ${copy.shortLabel} failed: sync this branch before creating a ${copy.reviewLabel}.` - case 'auth_required': - return `Create ${copy.shortLabel} failed: ${copy.providerName} is not authenticated. Next step: ${resolveHostedReviewAuthInstruction(eligibility.provider)} in this environment.` - case 'detached_head': - case 'existing_review': - case 'fork_head_unsupported': - case 'unsupported_provider': - // base_not_on_remote is a create-time hard failure, not an actionable eligibility state. - // falls through - case 'base_not_on_remote': - case null: - return null - } + return hostedReviewCreateFailureMessage(reason, eligibility.provider) } diff --git a/src/renderer/src/components/right-sidebar/source-control-dropdown-review-items.ts b/src/renderer/src/components/right-sidebar/source-control-dropdown-review-items.ts index c953cd2ba58..af65ab1fe22 100644 --- a/src/renderer/src/components/right-sidebar/source-control-dropdown-review-items.ts +++ b/src/renderer/src/components/right-sidebar/source-control-dropdown-review-items.ts @@ -6,11 +6,8 @@ import { localizedHostedReviewCopy, resolveSupportedHostedReviewCopyProvider } from '@/i18n/hosted-review-localized-copy' -import { - canClickBlockedCreateReviewReason, - resolveHostedReviewAuthInstruction -} from './source-control-create-review-blocked-action' -import type { PrimaryActionInputs } from './source-control-primary-action' +import { canClickBlockedCreateReviewReason } from './source-control-create-review-blocked-action' +import { hostedReviewSignInHint } from './hosted-review-create-failure-copy' import type { DropdownItem } from './source-control-dropdown-item-types' import type { DropdownActionContext } from './source-control-dropdown-action-context' @@ -19,22 +16,13 @@ export type HostedReviewDropdownItems = { pushCreatePR: DropdownItem } -function reviewCopy( - provider: NonNullable['provider'] | undefined -): ReturnType & { - authInstruction: string -} { - return { - ...localizedHostedReviewCopy(resolveSupportedHostedReviewCopyProvider(provider)), - authInstruction: resolveHostedReviewAuthInstruction(provider ?? 'github') - } -} - export function buildHostedReviewDropdownItems( ctx: DropdownActionContext ): HostedReviewDropdownItems { const { hostedReviewCreation, globalBusy, upstreamLoading, shouldForcePushWithLease } = ctx - const createReviewCopy = reviewCopy(hostedReviewCreation?.provider) + const createReviewCopy = localizedHostedReviewCopy( + resolveSupportedHostedReviewCopyProvider(hostedReviewCreation?.provider) + ) const createBlockedHint = ((): string => { switch (hostedReviewCreation?.blockedReason) { @@ -74,11 +62,7 @@ export function buildHostedReviewDropdownItems( 'Sync first' ) case 'auth_required': - return translate( - 'auto.components.right.sidebar.source.control.dropdown.items.blockedAuthRequired', - '{{authInstruction}} in this environment', - { authInstruction: createReviewCopy.authInstruction } - ) + return hostedReviewSignInHint(hostedReviewCreation?.provider) case 'unsupported_provider': return translate( 'auto.components.right.sidebar.source.control.dropdown.items.blockedUnsupportedProvider', diff --git a/src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-review.ts b/src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-review.ts index 9220270b9bc..982977cd7c9 100644 --- a/src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-review.ts +++ b/src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-review.ts @@ -9,6 +9,7 @@ import { operationGitContext } from '../listing/operation-target' import { translate } from '@/i18n/i18n' import { generateRuntimePullRequestFields } from '@/runtime/runtime-git-generation-client' import type { HostedReviewCreationEligibility } from '../../../../../../shared/hosted-review' +import { localizedCreateFailure } from '../../hosted-review-create-failure-copy' import { normalizeHostedReviewHeadRef } from '../../../../../../shared/hosted-review-refs' import { resolveCreateReviewSeedText } from '../../create-review-draft-title' import { @@ -255,7 +256,7 @@ export function useSourceControlCreatePrIntentReview({ setCreatePrIntentNoticeForWorktree(token.worktreeId, { tone: 'destructive', - message: result.error + message: localizedCreateFailure(result, eligibility.provider) }) return false } catch (error) { diff --git a/src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-creation.ts b/src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-creation.ts index 3819344e808..68414014236 100644 --- a/src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-creation.ts +++ b/src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-creation.ts @@ -3,6 +3,7 @@ import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import { normalizeHostedReviewHeadRef } from '../../../../../../shared/hosted-review-refs' import { resolveBlockedCreateReviewNoticeMessage } from '../../source-control-create-review-blocked-action' +import { localizedCreateFailure } from '../../hosted-review-create-failure-copy' import type { SourceControlAi } from '../ai/use-ai' import { stripBaseRef } from '../../create-pull-request-base-ref-normalization' import type { SourceControlStoreActions } from '../listing/use-store-actions' @@ -222,7 +223,7 @@ export function useSourceControlHostedReviewCreation({ setCreatePrIntentNoticeForWorktree(activeWorktreeId, { tone: 'destructive', - message: result.error + message: localizedCreateFailure(result, hostedReviewCreateProvider) }) } catch (error) { setCreatePrIntentNoticeForWorktree(activeWorktreeId, { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index b7d12f46d0f..e64d801da12 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -14665,7 +14665,6 @@ "blockedPushFirst": "Push first", "blockedForcePushFirst": "Force Push first", "blockedSyncFirst": "Sync first", - "blockedAuthRequired": "{{authInstruction}} in this environment", "blockedUnsupportedProvider": "Unsupported provider", "blockedExistingReview": "A {{reviewLabel}} already exists", "blockedForkHeadUnsupported": "Fork head unsupported", @@ -20798,6 +20797,36 @@ }, "unlinkedPr": { "status": "PR #{{number}} unlinked" + }, + "createReviewFailure": { + "authRequiredCommand": "{{providerName}} is not authenticated. Next step: run {{command}} in this environment.", + "authRequiredVariable": "{{providerName}} is not authenticated. Next step: set {{variable}} in this environment.", + "authRequiredBitbucket": "Bitbucket is not authenticated. Next step: connect Bitbucket in Settings > Integrations, or set ORCA_BITBUCKET_* in this environment.", + "signInHintCommand": "Run {{command}} in this environment", + "signInHintVariable": "Set {{variable}} in this environment", + "signInHintBitbucket": "Connect Bitbucket in Settings > Integrations", + "dirty": "commit or discard local changes before creating a {{reviewLabel}}.", + "detachedHead": "switch to a branch before creating a {{reviewLabel}}.", + "defaultBranch": "choose a feature branch before creating a {{reviewLabel}}.", + "noUpstream": "publish this branch before creating a {{reviewLabel}}.", + "needsPush": "push this branch before creating a {{reviewLabel}}.", + "needsSync": "sync this branch before creating a {{reviewLabel}}.", + "refreshStatus": "refresh source control status and try again.", + "namedBaseNotOnRemote": "the base branch \"{{base}}\" hasn't been pushed to the remote. Choose a pushed base or push it first.", + "baseNotOnRemote": "the base branch hasn't been pushed to the remote. Choose a pushed base or push it first.", + "branchChanged": "switch back to the selected branch before creating a {{reviewLabel}}.", + "branchStatusUnverified": "could not verify branch status. Refresh source control and try again.", + "reviewLookupUnavailable": "Orca could not confirm whether this branch already has a {{reviewLabel}}. Retry once the {{providerName}} lookup succeeds.", + "sameBaseBranch": "choose a different base branch before creating a {{reviewLabel}}.", + "rejected": "{{providerName}} rejected the {{reviewLabel}}. Check the base branch and branch state, then try again.", + "repositoryNotFound": "{{providerName}} could not find the repository. Check that the credential has write access to it.", + "createFailed": "{{providerName}} could not create the {{reviewLabel}}. Try again in a moment.", + "unsupportedProvider": "Creating a {{reviewLabel}} requires a remote hosted on {{providerName}}.", + "providerNotSupported": "Creating reviews for this provider is not supported yet.", + "existingReview": "A {{reviewLabel}} already exists for this branch.", + "creationUnconfirmed": "{{shortLabel}} creation may have completed. Refreshing branch review state...", + "withDetail": "Create {{shortLabel}} failed: {{detail}}", + "pushFailed": "Push failed. Resolve the push error, then try again." } }, "agentsSidebarIntro": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 55a1aa637f2..531a3462a02 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -20957,6 +20957,36 @@ }, "unlinkedPr": { "status": "PR #{{number}} desvinculada" + }, + "createReviewFailure": { + "authRequiredCommand": "{{providerName}} no está autenticado. Siguiente paso: ejecuta {{command}} en este entorno.", + "authRequiredVariable": "{{providerName}} no está autenticado. Siguiente paso: define {{variable}} en este entorno.", + "authRequiredBitbucket": "Bitbucket no está autenticado. Siguiente paso: conecta Bitbucket en Ajustes > Integraciones o define ORCA_BITBUCKET_* en este entorno.", + "signInHintCommand": "Ejecuta {{command}} en este entorno", + "signInHintVariable": "Define {{variable}} en este entorno", + "signInHintBitbucket": "Conecta Bitbucket en Ajustes > Integraciones", + "dirty": "haz commit de los cambios locales o descártalos antes de crear {{reviewLabel}}.", + "detachedHead": "cambia a una rama antes de crear {{reviewLabel}}.", + "defaultBranch": "elige una rama de características antes de crear {{reviewLabel}}.", + "noUpstream": "publica esta rama antes de crear {{reviewLabel}}.", + "needsPush": "haz push de esta rama antes de crear {{reviewLabel}}.", + "needsSync": "sincroniza esta rama antes de crear {{reviewLabel}}.", + "refreshStatus": "actualiza el estado del control de código fuente y vuelve a intentarlo.", + "namedBaseNotOnRemote": "la rama base \"{{base}}\" no se ha enviado al remoto. Elige una base que ya esté en el remoto o haz push de ella primero.", + "baseNotOnRemote": "la rama base no se ha enviado al remoto. Elige una base que ya esté en el remoto o haz push de ella primero.", + "branchChanged": "vuelve a la rama seleccionada antes de crear {{reviewLabel}}.", + "branchStatusUnverified": "no se pudo verificar el estado de la rama. Actualiza el control de código fuente y vuelve a intentarlo.", + "reviewLookupUnavailable": "Orca no pudo confirmar si esta rama ya tiene {{reviewLabel}}. Reintenta cuando la consulta a {{providerName}} funcione.", + "sameBaseBranch": "elige otra rama base antes de crear {{reviewLabel}}.", + "rejected": "{{providerName}} rechazó la creación de {{reviewLabel}}. Comprueba la rama base y el estado de la rama, y vuelve a intentarlo.", + "repositoryNotFound": "{{providerName}} no encontró el repositorio. Comprueba que la credencial tenga acceso de escritura a él.", + "createFailed": "{{providerName}} no pudo crear {{reviewLabel}}. Vuelve a intentarlo en un momento.", + "unsupportedProvider": "Para crear {{reviewLabel}} se necesita un remoto alojado en {{providerName}}.", + "providerNotSupported": "Todavía no se admite crear revisiones para este proveedor.", + "existingReview": "Esta rama ya tiene {{reviewLabel}}.", + "creationUnconfirmed": "Puede que la creación de {{shortLabel}} se haya completado. Actualizando el estado de revisión de la rama...", + "withDetail": "No se pudo crear {{shortLabel}}: {{detail}}", + "pushFailed": "El push falló. Resuelve el error de push y vuelve a intentarlo." } }, "aiVault": { diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 82a4acb2a89..19097770a81 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -20524,6 +20524,36 @@ "unstagedChangesDescription": "Cela annulera les modifications non planifiées apportées à ce fichier. Cela ne peut pas être annulé.", "unstagedSelectionIncludingUntrackedDescription": "Cela annulera les modifications non indexées et supprimera définitivement tous les fichiers non suivis de cette sélection. Cette action est irréversible.", "unstagedFilesIncludingUntrackedDescription": "Cela annulera les modifications non indexées et supprimera définitivement tous les fichiers non suivis parmi ces {{count}} fichiers. Cette action est irréversible." + }, + "createReviewFailure": { + "authRequiredCommand": "{{providerName}} n'est pas authentifié. Étape suivante : exécutez {{command}} dans cet environnement.", + "authRequiredVariable": "{{providerName}} n'est pas authentifié. Étape suivante : définissez {{variable}} dans cet environnement.", + "authRequiredBitbucket": "Bitbucket n'est pas authentifié. Étape suivante : connectez Bitbucket dans Paramètres > Intégrations, ou définissez ORCA_BITBUCKET_* dans cet environnement.", + "signInHintCommand": "Exécutez {{command}} dans cet environnement", + "signInHintVariable": "Définissez {{variable}} dans cet environnement", + "signInHintBitbucket": "Connectez Bitbucket dans Paramètres > Intégrations", + "dirty": "commitez ou abandonnez les modifications locales avant de créer une {{reviewLabel}}.", + "detachedHead": "passez sur une branche avant de créer une {{reviewLabel}}.", + "defaultBranch": "choisissez une branche de fonctionnalité avant de créer une {{reviewLabel}}.", + "noUpstream": "publiez cette branche avant de créer une {{reviewLabel}}.", + "needsPush": "poussez cette branche avant de créer une {{reviewLabel}}.", + "needsSync": "synchronisez cette branche avant de créer une {{reviewLabel}}.", + "refreshStatus": "actualisez l'état du contrôle de code source et réessayez.", + "namedBaseNotOnRemote": "la branche de base « {{base}} » n'a pas été poussée vers le dépôt distant. Choisissez une base déjà poussée ou poussez-la d'abord.", + "baseNotOnRemote": "la branche de base n'a pas été poussée vers le dépôt distant. Choisissez une base déjà poussée ou poussez-la d'abord.", + "branchChanged": "revenez sur la branche sélectionnée avant de créer une {{reviewLabel}}.", + "branchStatusUnverified": "impossible de vérifier l'état de la branche. Actualisez le contrôle de code source et réessayez.", + "reviewLookupUnavailable": "Orca n'a pas pu confirmer si cette branche a déjà une {{reviewLabel}}. Réessayez une fois que la recherche sur {{providerName}} aura réussi.", + "sameBaseBranch": "choisissez une autre branche de base avant de créer une {{reviewLabel}}.", + "rejected": "{{providerName}} a refusé la {{reviewLabel}}. Vérifiez la branche de base et l'état de la branche, puis réessayez.", + "repositoryNotFound": "{{providerName}} n'a pas trouvé le dépôt. Vérifiez que l'identifiant dispose d'un accès en écriture à ce dépôt.", + "createFailed": "{{providerName}} n'a pas pu créer la {{reviewLabel}}. Réessayez dans un instant.", + "unsupportedProvider": "La création d'une {{reviewLabel}} nécessite un dépôt distant hébergé sur {{providerName}}.", + "providerNotSupported": "La création de revues n'est pas encore prise en charge pour ce fournisseur.", + "existingReview": "Une {{reviewLabel}} existe déjà pour cette branche.", + "creationUnconfirmed": "La création de {{shortLabel}} est peut-être terminée. Actualisation de l'état de revue de la branche...", + "withDetail": "La création de {{shortLabel}} a échoué : {{detail}}", + "pushFailed": "Le push a échoué. Résolvez l'erreur de push, puis réessayez." } }, "agentsSidebarIntro": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 8e063f2e28e..7483161fcf8 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -20601,6 +20601,36 @@ "unstagedChangesDescription": "これにより、このファイルに対するステージングされていない変更が元に戻ります。これを元に戻すことはできません。", "unstagedSelectionIncludingUntrackedDescription": "ステージングされていない変更を元に戻し、この選択内の未追跡ファイルを完全に削除します。この操作は元に戻せません。", "unstagedFilesIncludingUntrackedDescription": "ステージングされていない変更を元に戻し、これらの {{count}} 個のファイルに含まれる未追跡ファイルを完全に削除します。この操作は元に戻せません。" + }, + "createReviewFailure": { + "authRequiredCommand": "{{providerName}} が認証されていません。次の手順: この環境で {{command}} を実行してください。", + "authRequiredVariable": "{{providerName}} が認証されていません。次の手順: この環境で {{variable}} を設定してください。", + "authRequiredBitbucket": "Bitbucket が認証されていません。次の手順: 設定 > 連携 で Bitbucket を接続するか、この環境で ORCA_BITBUCKET_* を設定してください。", + "signInHintCommand": "この環境で {{command}} を実行してください", + "signInHintVariable": "この環境で {{variable}} を設定してください", + "signInHintBitbucket": "設定 > 連携 で Bitbucket を接続してください", + "dirty": "{{reviewLabel}} を作成する前に、ローカルの変更をコミットするか破棄してください。", + "detachedHead": "{{reviewLabel}} を作成する前に、ブランチに切り替えてください。", + "defaultBranch": "{{reviewLabel}} を作成する前に、フィーチャーブランチを選んでください。", + "noUpstream": "{{reviewLabel}} を作成する前に、このブランチを公開してください。", + "needsPush": "{{reviewLabel}} を作成する前に、このブランチをプッシュしてください。", + "needsSync": "{{reviewLabel}} を作成する前に、このブランチを同期してください。", + "refreshStatus": "ソース管理の状態を更新して、もう一度お試しください。", + "namedBaseNotOnRemote": "ベースブランチ「{{base}}」がリモートにプッシュされていません。プッシュ済みのベースを選ぶか、先にプッシュしてください。", + "baseNotOnRemote": "ベースブランチがリモートにプッシュされていません。プッシュ済みのベースを選ぶか、先にプッシュしてください。", + "branchChanged": "{{reviewLabel}} を作成する前に、選択したブランチに切り替え直してください。", + "branchStatusUnverified": "ブランチの状態を確認できませんでした。ソース管理を更新して、もう一度お試しください。", + "reviewLookupUnavailable": "このブランチに {{reviewLabel}} がすでにあるかどうかを Orca が確認できませんでした。{{providerName}} での確認が成功してから再試行してください。", + "sameBaseBranch": "{{reviewLabel}} を作成する前に、別のベースブランチを選んでください。", + "rejected": "{{providerName}} が {{reviewLabel}} を拒否しました。ベースブランチとブランチの状態を確認してから、もう一度お試しください。", + "repositoryNotFound": "{{providerName}} がリポジトリを見つけられませんでした。認証情報にリポジトリへの書き込み権限があることを確認してください。", + "createFailed": "{{providerName}} が {{reviewLabel}} を作成できませんでした。しばらくしてから、もう一度お試しください。", + "unsupportedProvider": "{{reviewLabel}} を作成するには、{{providerName}} でホストされているリモートが必要です。", + "providerNotSupported": "このプロバイダーでのレビューの作成には、まだ対応していません。", + "existingReview": "このブランチの {{reviewLabel}} はすでに存在します。", + "creationUnconfirmed": "{{shortLabel}} の作成は完了している可能性があります。ブランチのレビュー状態を更新しています…", + "withDetail": "{{shortLabel}} の作成に失敗しました: {{detail}}", + "pushFailed": "プッシュに失敗しました。プッシュのエラーを解決してから、もう一度お試しください。" } }, "rendererRecovery": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 9cddb0c5f2b..7c2ee099cde 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -20538,6 +20538,36 @@ "unstagedChangesDescription": "이렇게 하면 단계가 지정되지 않은 변경 사항이 이 파일로 되돌아갑니다. 이 작업은 취소할 수 없습니다.", "unstagedSelectionIncludingUntrackedDescription": "스테이징되지 않은 변경 사항을 되돌리고 이 선택 항목의 추적되지 않은 파일을 영구적으로 삭제합니다. 이 작업은 취소할 수 없습니다.", "unstagedFilesIncludingUntrackedDescription": "스테이징되지 않은 변경 사항을 되돌리고 이 {{count}}개 파일 중 추적되지 않은 파일을 영구적으로 삭제합니다. 이 작업은 취소할 수 없습니다." + }, + "createReviewFailure": { + "authRequiredCommand": "{{providerName}}이(가) 인증되지 않았습니다. 다음 단계: 이 환경에서 {{command}}을(를) 실행하세요.", + "authRequiredVariable": "{{providerName}}이(가) 인증되지 않았습니다. 다음 단계: 이 환경에서 {{variable}}을(를) 설정하세요.", + "authRequiredBitbucket": "Bitbucket이 인증되지 않았습니다. 다음 단계: 설정 > 연동에서 Bitbucket을 연결하거나 이 환경에서 ORCA_BITBUCKET_*을(를) 설정하세요.", + "signInHintCommand": "이 환경에서 {{command}}을(를) 실행하세요", + "signInHintVariable": "이 환경에서 {{variable}}을(를) 설정하세요", + "signInHintBitbucket": "설정 > 연동에서 Bitbucket을 연결하세요", + "dirty": "{{reviewLabel}}을(를) 만들기 전에 로컬 변경 사항을 커밋하거나 버리세요.", + "detachedHead": "{{reviewLabel}}을(를) 만들기 전에 브랜치로 전환하세요.", + "defaultBranch": "{{reviewLabel}}을(를) 만들기 전에 기능 브랜치를 선택하세요.", + "noUpstream": "{{reviewLabel}}을(를) 만들기 전에 이 브랜치를 게시하세요.", + "needsPush": "{{reviewLabel}}을(를) 만들기 전에 이 브랜치를 푸시하세요.", + "needsSync": "{{reviewLabel}}을(를) 만들기 전에 이 브랜치를 동기화하세요.", + "refreshStatus": "소스 제어 상태를 새로고침한 후 다시 시도하세요.", + "namedBaseNotOnRemote": "기본 브랜치 \"{{base}}\"이(가) 원격에 푸시되지 않았습니다. 푸시된 기본 브랜치를 선택하거나 먼저 푸시하세요.", + "baseNotOnRemote": "기본 브랜치가 원격에 푸시되지 않았습니다. 푸시된 기본 브랜치를 선택하거나 먼저 푸시하세요.", + "branchChanged": "{{reviewLabel}}을(를) 만들기 전에 선택한 브랜치로 다시 전환하세요.", + "branchStatusUnverified": "브랜치 상태를 확인할 수 없습니다. 소스 제어를 새로고침한 후 다시 시도하세요.", + "reviewLookupUnavailable": "이 브랜치에 이미 {{reviewLabel}}이(가) 있는지 Orca가 확인할 수 없습니다. {{providerName}} 조회가 성공하면 다시 시도하세요.", + "sameBaseBranch": "{{reviewLabel}}을(를) 만들기 전에 다른 기본 브랜치를 선택하세요.", + "rejected": "{{providerName}}이(가) {{reviewLabel}}을(를) 거부했습니다. 기본 브랜치와 브랜치 상태를 확인한 후 다시 시도하세요.", + "repositoryNotFound": "{{providerName}}이(가) 저장소를 찾을 수 없습니다. 자격 증명에 저장소 쓰기 권한이 있는지 확인하세요.", + "createFailed": "{{providerName}}이(가) {{reviewLabel}}을(를) 만들 수 없습니다. 잠시 후 다시 시도하세요.", + "unsupportedProvider": "{{reviewLabel}}을(를) 만들려면 {{providerName}}에 호스팅된 원격이 필요합니다.", + "providerNotSupported": "이 제공자에서는 아직 리뷰를 만들 수 없습니다.", + "existingReview": "이 브랜치에 대한 {{reviewLabel}}이(가) 이미 있습니다.", + "creationUnconfirmed": "{{shortLabel}} 생성이 완료되었을 수 있습니다. 브랜치 리뷰 상태를 새로고침하는 중...", + "withDetail": "{{shortLabel}} 생성에 실패했습니다: {{detail}}", + "pushFailed": "푸시에 실패했습니다. 푸시 오류를 해결한 후 다시 시도하세요." } }, "rendererRecovery": { diff --git a/src/renderer/src/i18n/locales/pt.json b/src/renderer/src/i18n/locales/pt.json index 73e5db1e31d..7d6f6591aae 100644 --- a/src/renderer/src/i18n/locales/pt.json +++ b/src/renderer/src/i18n/locales/pt.json @@ -25,6 +25,36 @@ "unstagedChangesDescription": "Isso reverterá as alterações não preparadas neste arquivo. Essa ação não pode ser desfeita.", "unstagedSelectionIncludingUntrackedDescription": "Isso reverterá as alterações não preparadas e excluirá permanentemente quaisquer arquivos não rastreados nesta seleção. Essa ação não pode ser desfeita.", "unstagedFilesIncludingUntrackedDescription": "Isso reverterá as alterações não preparadas e excluirá permanentemente quaisquer arquivos não rastreados entre estes {{count}} arquivos. Essa ação não pode ser desfeita." + }, + "createReviewFailure": { + "authRequiredCommand": "{{providerName}} não está autenticado. Próximo passo: execute {{command}} neste ambiente.", + "authRequiredVariable": "{{providerName}} não está autenticado. Próximo passo: defina {{variable}} neste ambiente.", + "authRequiredBitbucket": "Bitbucket não está autenticado. Próximo passo: conecte o Bitbucket em Configurações > Integrações ou defina ORCA_BITBUCKET_* neste ambiente.", + "signInHintCommand": "Execute {{command}} neste ambiente", + "signInHintVariable": "Defina {{variable}} neste ambiente", + "signInHintBitbucket": "Conecte o Bitbucket em Configurações > Integrações", + "dirty": "faça commit das alterações locais ou descarte-as antes de criar um(a) {{reviewLabel}}.", + "detachedHead": "mude para uma branch antes de criar um(a) {{reviewLabel}}.", + "defaultBranch": "escolha uma branch de funcionalidade antes de criar um(a) {{reviewLabel}}.", + "noUpstream": "publique esta branch antes de criar um(a) {{reviewLabel}}.", + "needsPush": "faça push desta branch antes de criar um(a) {{reviewLabel}}.", + "needsSync": "sincronize esta branch antes de criar um(a) {{reviewLabel}}.", + "refreshStatus": "atualize o status do controle de código-fonte e tente novamente.", + "namedBaseNotOnRemote": "a branch base \"{{base}}\" ainda não foi enviada para o remoto. Escolha uma base que já esteja no remoto ou faça push dela primeiro.", + "baseNotOnRemote": "a branch base ainda não foi enviada para o remoto. Escolha uma base que já esteja no remoto ou faça push dela primeiro.", + "branchChanged": "volte para a branch selecionada antes de criar um(a) {{reviewLabel}}.", + "branchStatusUnverified": "não foi possível verificar o status da branch. Atualize o controle de código-fonte e tente novamente.", + "reviewLookupUnavailable": "O Orca não conseguiu confirmar se esta branch já tem um(a) {{reviewLabel}}. Tente novamente quando a consulta ao {{providerName}} funcionar.", + "sameBaseBranch": "escolha outra branch base antes de criar um(a) {{reviewLabel}}.", + "rejected": "{{providerName}} rejeitou o(a) {{reviewLabel}}. Verifique a branch base e o estado da branch e tente novamente.", + "repositoryNotFound": "{{providerName}} não encontrou o repositório. Verifique se a credencial tem acesso de escrita a ele.", + "createFailed": "{{providerName}} não conseguiu criar o(a) {{reviewLabel}}. Tente novamente em instantes.", + "unsupportedProvider": "Para criar um(a) {{reviewLabel}}, é preciso um remoto hospedado no {{providerName}}.", + "providerNotSupported": "Ainda não há suporte para criar revisões neste provedor.", + "existingReview": "Já existe um(a) {{reviewLabel}} para esta branch.", + "creationUnconfirmed": "A criação do(a) {{shortLabel}} pode ter sido concluída. Atualizando o estado de revisão da branch...", + "withDetail": "Falha ao criar {{shortLabel}}: {{detail}}", + "pushFailed": "O push falhou. Resolva o erro de push e tente novamente." } }, "githubChecks": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index ed399c2fc8a..e7d709557a0 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -20539,6 +20539,36 @@ "unstagedChangesDescription": "这将恢复对此文件的未暂存更改。此操作无法撤消。", "unstagedSelectionIncludingUntrackedDescription": "这将还原未暂存的更改,并永久删除此选择中的任何未跟踪文件。此操作无法撤销。", "unstagedFilesIncludingUntrackedDescription": "这将还原未暂存的更改,并永久删除这 {{count}} 个文件中的任何未跟踪文件。此操作无法撤销。" + }, + "createReviewFailure": { + "authRequiredCommand": "{{providerName}} 未通过身份验证。下一步:在此环境中运行 {{command}}。", + "authRequiredVariable": "{{providerName}} 未通过身份验证。下一步:在此环境中设置 {{variable}}。", + "authRequiredBitbucket": "Bitbucket 未通过身份验证。下一步:在“设置 > 集成”中连接 Bitbucket,或在此环境中设置 ORCA_BITBUCKET_*。", + "signInHintCommand": "在此环境中运行 {{command}}", + "signInHintVariable": "在此环境中设置 {{variable}}", + "signInHintBitbucket": "在“设置 > 集成”中连接 Bitbucket", + "dirty": "请在创建 {{reviewLabel}} 之前提交或放弃本地更改。", + "detachedHead": "请在创建 {{reviewLabel}} 之前切换到某个分支。", + "defaultBranch": "请在创建 {{reviewLabel}} 之前选择功能分支。", + "noUpstream": "请在创建 {{reviewLabel}} 之前发布此分支。", + "needsPush": "请在创建 {{reviewLabel}} 之前推送此分支。", + "needsSync": "请在创建 {{reviewLabel}} 之前同步此分支。", + "refreshStatus": "请刷新源代码控制状态后重试。", + "namedBaseNotOnRemote": "基础分支“{{base}}”尚未推送到远程。请选择已推送的基础分支,或先推送它。", + "baseNotOnRemote": "基础分支尚未推送到远程。请选择已推送的基础分支,或先推送它。", + "branchChanged": "请在创建 {{reviewLabel}} 之前切换回所选分支。", + "branchStatusUnverified": "无法验证分支状态。请刷新源代码控制后重试。", + "reviewLookupUnavailable": "Orca 无法确认此分支是否已有 {{reviewLabel}}。请在 {{providerName}} 查询成功后重试。", + "sameBaseBranch": "请在创建 {{reviewLabel}} 之前选择其他基础分支。", + "rejected": "{{providerName}} 拒绝了该 {{reviewLabel}}。请检查基础分支和分支状态,然后重试。", + "repositoryNotFound": "{{providerName}} 找不到该仓库。请确认凭据对该仓库具有写入权限。", + "createFailed": "{{providerName}} 无法创建 {{reviewLabel}}。请稍后重试。", + "unsupportedProvider": "创建 {{reviewLabel}} 需要托管在 {{providerName}} 上的远程仓库。", + "providerNotSupported": "尚不支持为此提供商创建评审。", + "existingReview": "此分支已存在 {{reviewLabel}}。", + "creationUnconfirmed": "{{shortLabel}} 可能已创建完成。正在刷新分支评审状态...", + "withDetail": "创建 {{shortLabel}} 失败:{{detail}}", + "pushFailed": "推送失败。请解决推送错误后重试。" } }, "rendererRecovery": { diff --git a/src/shared/hosted-review.ts b/src/shared/hosted-review.ts index f4e9c5ec8fa..62da1405ad9 100644 --- a/src/shared/hosted-review.ts +++ b/src/shared/hosted-review.ts @@ -110,12 +110,30 @@ export type CreateHostedReviewErrorCode = | 'push_failed' | 'unknown' +/** An Orca-authored create failure that clients can show in their own language. */ +export type CreateHostedReviewFailureReason = + | NonNullable + | 'branch_changed' + | 'branch_status_unverified' + | 'review_lookup_unavailable' + | 'provider_not_supported' + | 'same_base_branch' + | 'rejected' + | 'repository_not_found' + | 'create_failed' + | 'creation_unconfirmed' + export type CreateHostedReviewResult = | { ok: true; number: number; url: string } | { ok: false code: CreateHostedReviewErrorCode + /** English text for mobile, older clients and logs. */ error: string + /** Absent from older hosts; clients then show `error`. */ + reason?: CreateHostedReviewFailureReason + /** The base branch named by the `base_not_on_remote` reason. */ + base?: string existingReview?: HostedReviewSummary } @@ -131,6 +149,9 @@ export type CreateStackedHostedReviewResult = ok: false code: CreateHostedReviewErrorCode error: string + /** Set when the failure comes from the plain create step. */ + reason?: CreateHostedReviewFailureReason + base?: string createdReview?: HostedReviewSummary }