Skip to content

Commit 3757504

Browse files
committed
fix: report fork-PR resolution errors instead of a traceback
Errors from fetch_fork_pr / fetch_fork_branch (PR not found, fetch failed) escaped split as raw GitOperationError tracebacks. Catch them and print the prepared message. Also distinguish a same-repo PR from a missing one so the user is told to pass the branch name instead.
1 parent 9482d92 commit 3757504

5 files changed

Lines changed: 50 additions & 4 deletions

File tree

pr_split/cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,11 @@ def split(
732732
if not is_worktree_clean():
733733
console.print(f"[red]{ErrorMsg.DIRTY_WORKTREE()}[/red]")
734734
raise typer.Exit(1)
735-
fork_info = _resolve_fork_ref(dev_branch)
735+
try:
736+
fork_info = _resolve_fork_ref(dev_branch)
737+
except PRSplitError as exc:
738+
console.print(f"[red]{exc}[/red]")
739+
raise typer.Exit(1) from exc
736740
if not fork_info:
737741
console.print(f"[red]{ErrorMsg.BRANCH_NOT_FOUND(branch=dev_branch)}[/red]")
738742
raise typer.Exit(1)

pr_split/exceptions.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ class ErrorMsg(StrEnum):
2121
BRANCH_CREATE_FAILED = "Failed to create branch '{branch}': {detail}"
2222
PR_CREATE_FAILED = "Failed to create PR for group '{group}': {detail}"
2323
MERGE_FAILED = "Merge of '{source}' into '{target}' failed: {detail}"
24-
PR_NOT_FOUND = "PR #{number} not found or is not from a fork"
24+
PR_NOT_FOUND = "PR #{number} not found"
25+
PR_NOT_FROM_FORK = (
26+
"PR #{number} is not from a fork; pass its head branch name instead of the PR number"
27+
)
2528
PR_FETCH_FAILED = "Failed to fetch fork branch for PR #{number}: {detail}"
2629
FORK_FETCH_FAILED = "Failed to fetch {user}:{branch}: {detail}"
2730
HUNK_TOO_LARGE = "Hunk {file}[{index}] has ~{tokens} estimated tokens, exceeds budget {budget}"

pr_split/git_ops/prs.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,11 @@ def fetch_fork_pr(pr_number: int) -> ForkPRInfo:
104104
raise GitOperationError(ErrorMsg.PR_NOT_FOUND(number=pr_number))
105105

106106
head_repo = head.get("repo")
107-
if not isinstance(head_repo, dict) or not head_repo.get("fork"):
107+
if not isinstance(head_repo, dict):
108+
# head.repo is null when the fork was deleted
108109
raise GitOperationError(ErrorMsg.PR_NOT_FOUND(number=pr_number))
110+
if not head_repo.get("fork"):
111+
raise GitOperationError(ErrorMsg.PR_NOT_FROM_FORK(number=pr_number))
109112

110113
clone_url = str(head_repo["clone_url"])
111114
head_ref = str(head["ref"])

tests/test_cli_coverage.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
app,
2727
)
2828
from pr_split.constants import AssignmentType
29-
from pr_split.exceptions import PRSplitError
29+
from pr_split.exceptions import GitOperationError, PRSplitError
3030
from pr_split.schemas import Group, GroupAssignment
3131
from pr_split.types_defs import ForkPRInfo
3232

@@ -464,6 +464,23 @@ def test_split_fork_ref_dirty_worktree(
464464
result = runner.invoke(app, ["split", "#42", "--dry-run"])
465465
assert result.exit_code != 0
466466

467+
@patch("pr_split.cli._resolve_fork_ref", side_effect=GitOperationError("PR #999 not found"))
468+
@patch("pr_split.cli.is_worktree_clean", return_value=True)
469+
@patch("pr_split.cli.check_gh_auth", return_value=True)
470+
@patch("pr_split.cli.branch_exists", return_value=False)
471+
def test_split_fork_ref_error_is_reported_not_raised(
472+
self,
473+
mock_be: MagicMock,
474+
mock_auth: MagicMock,
475+
mock_clean: MagicMock,
476+
mock_resolve: MagicMock,
477+
) -> None:
478+
result = runner.invoke(app, ["split", "#999", "--dry-run"])
479+
assert result.exit_code == 1
480+
assert result.exception is None or isinstance(result.exception, SystemExit)
481+
assert "PR #999 not found" in result.output
482+
assert "Traceback" not in result.output
483+
467484
@patch("pr_split.cli._resolve_fork_ref", return_value=None)
468485
@patch("pr_split.cli.is_worktree_clean", return_value=True)
469486
@patch("pr_split.cli.check_gh_auth", return_value=True)

tests/test_git_prs_coverage.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,22 @@ def test_git_fetch_failure(self, mock_gh: MagicMock, mock_git: MagicMock) -> Non
164164

165165
with pytest.raises(GitOperationError, match="Failed to fetch"):
166166
fetch_fork_branch("user", "branch")
167+
168+
169+
class TestFetchForkPrNotFromFork:
170+
@patch("pr_split.git_ops.prs._run_gh")
171+
def test_same_repo_pr_gets_specific_message(self, mock_gh: MagicMock) -> None:
172+
pr_data = {
173+
"head": {
174+
"ref": "feature",
175+
"repo": {
176+
"fork": False,
177+
"clone_url": "https://github.com/org/repo.git",
178+
"full_name": "org/repo",
179+
},
180+
},
181+
"base": {"ref": "main"},
182+
}
183+
mock_gh.return_value = json.dumps(pr_data)
184+
with pytest.raises(GitOperationError, match="PR #42 is not from a fork"):
185+
fetch_fork_pr(42)

0 commit comments

Comments
 (0)