Fix gh CLI compatibility for older versions missing baseRefOid field

**Changes:**
1. Added `GhApiPr` and `GhApiRef` structs (lines 31-46) to deserialize the GitHub REST API response
2. Added `get_pr_info_via_api()` function (lines 103-141) as a fallback that uses `gh api repos/{owner}/{repo}/pulls/{number}`
3. Modified `get_pr_info()` (lines 166-170) to detect "unknown json field" errors and fall back to the API method

**How it works:**
- Modern `gh` CLI versions continue to use the faster `gh pr view --json` approach
- When an older `gh` CLI returns "Unknown JSON field: baseRefOid", the code catches this error and falls back to `gh api` which uses the stable REST API
- The REST API fields (`base.sha`, `head.sha`, `head.ref`) have been stable for years and work with all `gh` CLI versions
This commit is contained in:
Louis Knight-Webb
2026-01-12 10:14:34 +00:00
committed by GitHub
parent 8fa5b9d098
commit f30606b48a

View File

@@ -28,6 +28,23 @@ struct GhPrView {
head_ref_name: String, head_ref_name: String,
} }
/// Response from `gh api /repos/{owner}/{repo}/pulls/{number}`
/// Used as fallback for older gh CLI versions that don't support baseRefOid/headRefOid fields
#[derive(Debug, Deserialize)]
struct GhApiPr {
title: String,
body: Option<String>,
base: GhApiRef,
head: GhApiRef,
}
#[derive(Debug, Deserialize)]
struct GhApiRef {
sha: String,
#[serde(rename = "ref")]
ref_name: String,
}
/// Parse a GitHub PR URL to extract owner, repo, and PR number /// Parse a GitHub PR URL to extract owner, repo, and PR number
/// ///
/// Expected format: https://github.com/owner/repo/pull/123 /// Expected format: https://github.com/owner/repo/pull/123
@@ -83,6 +100,46 @@ fn ensure_gh_available() -> Result<(), ReviewError> {
Ok(()) Ok(())
} }
/// Get PR information using `gh api` (REST API)
/// This is used as a fallback for older gh CLI versions that don't support
/// the baseRefOid/headRefOid fields in `gh pr view --json`
fn get_pr_info_via_api(owner: &str, repo: &str, pr_number: i64) -> Result<PrInfo, ReviewError> {
debug!("Fetching PR info via gh api for {owner}/{repo}#{pr_number}");
let output = Command::new("gh")
.args(["api", &format!("repos/{owner}/{repo}/pulls/{pr_number}")])
.output()
.map_err(|e| ReviewError::PrInfoFailed(e.to_string()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let lower = stderr.to_ascii_lowercase();
if lower.contains("authentication")
|| lower.contains("gh auth login")
|| lower.contains("unauthorized")
{
return Err(ReviewError::GhNotAuthenticated);
}
return Err(ReviewError::PrInfoFailed(stderr.to_string()));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let api_pr: GhApiPr =
serde_json::from_str(&stdout).map_err(|e| ReviewError::PrInfoFailed(e.to_string()))?;
Ok(PrInfo {
owner: owner.to_string(),
repo: repo.to_string(),
title: api_pr.title,
description: api_pr.body.unwrap_or_default(),
base_commit: api_pr.base.sha,
head_commit: api_pr.head.sha,
head_ref_name: api_pr.head.ref_name,
})
}
/// Get PR information using `gh pr view` /// Get PR information using `gh pr view`
pub fn get_pr_info(owner: &str, repo: &str, pr_number: i64) -> Result<PrInfo, ReviewError> { pub fn get_pr_info(owner: &str, repo: &str, pr_number: i64) -> Result<PrInfo, ReviewError> {
ensure_gh_available()?; ensure_gh_available()?;
@@ -106,6 +163,12 @@ pub fn get_pr_info(owner: &str, repo: &str, pr_number: i64) -> Result<PrInfo, Re
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
let lower = stderr.to_ascii_lowercase(); let lower = stderr.to_ascii_lowercase();
// Check for old gh CLI version that doesn't support these JSON fields
if lower.contains("unknown json field") {
debug!("gh pr view --json failed with unknown field, falling back to gh api");
return get_pr_info_via_api(owner, repo, pr_number);
}
if lower.contains("authentication") if lower.contains("authentication")
|| lower.contains("gh auth login") || lower.contains("gh auth login")
|| lower.contains("unauthorized") || lower.contains("unauthorized")