{"record":{"id":"c867f806e3f780da","repo":"gitbutlerapp/gitbutler","slug":"failed-to-get-pr-base-repo","errorCode":null,"errorMessage":"Failed to get PR base repo: {}","messagePattern":"Failed to get PR base repo: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-github/src/client.rs","lineNumber":437,"sourceCode":"        struct Base {\n            #[serde(default)]\n            repo: Option<Repo>,\n        }\n        #[derive(Debug, Deserialize)]\n        struct Repo {\n            #[serde(default)]\n            git_url: Option<String>,\n            #[serde(default)]\n            clone_url: Option<String>,\n        }\n\n        let url = format!(\n            \"{}/repos/{}/{}/pulls/{}\",\n            self.base_url, owner, repo, pr_number\n        );\n        let response = self.client.get(&url).send().await?;\n        if !response.status().is_success() {\n            bail!(\"Failed to get PR base repo: {}\", response.status());\n        }\n        let body: BaseRepoResponse = response.json().await?;\n        // git_url matches what TS's parseRemoteUrl sees for the\n        // project's own base remote.\n        Ok(body.base.repo.and_then(|r| r.git_url.or(r.clone_url)))\n    }\n\n    /// Fetch just the GitHub-computed merge status for a PR\n    /// (`mergeable_state` and the comment count). These fields are\n    /// expensive enough that GitHub recomputes them per call, so we\n    /// expose them through a focused endpoint that the UI only\n    /// subscribes to where it actually displays them.\n    pub async fn get_pull_request_merge_status(\n        &self,\n        owner: &str,\n        repo: &str,\n        pr_number: i64,\n    ) -> Result<PullRequestMergeStatus> {","sourceCodeStart":419,"sourceCodeEnd":455,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-github/src/client.rs#L419-L455","documentation":"get_pull_request_base_repo_url fetches a pull request to determine the git URL of its base repository. A non-success HTTP status from /repos/{owner}/{repo}/pulls/{n} bails with this message and the status. Means the PR could not be read (doesn't exist or inaccessible).","triggerScenarios":"Calling get_pull_request_base_repo_url for a PR number that doesn't exist in owner/repo, a private repo the token can't read, or an API error (403 rate limit, 5xx).","commonSituations":"Wrong owner/repo pair for the PR number; stale PR reference after deletion; token expired or scope-stripped; rate limiting on shared CI tokens.","solutions":["Confirm the PR exists: curl the same /pulls/{n} endpoint with the token","Check HTTP status in the message: 404 → wrong repo/PR, 403 → permissions or rate limit, 401 → token invalid","Regenerate or rescope the GitHub token if 401/403"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match client.get_pull_request_base_repo_url(owner, repo, pr).await {\n    Ok(url) => url,\n    Err(e) if e.to_string().contains(\"404\") => {\n        eprintln!(\"PR #{pr} not found in {owner}/{repo}\");\n        Default::default()\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Confirm the PR number belongs to this owner/repo","Keep tokens fresh and scoped for repo read","Handle 403 as rate-limit/permission distinctly from 404"],"tags":["github","api","http","pull-request"],"backgroundTag":"http-error-response","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}