Repository navigation
fix(node): split absent ref from unreadable object in issue reads #437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
4fa1081
326f5e5
b5048f7
f2b6ceb
5da280b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,44 @@ | |
| Ok(()) | ||
| } | ||
|
|
||
| /// Read the blob behind an issue ref. | ||
| /// | ||
| /// `Ok(None)` means the ref itself is absent. A ref that resolves but whose | ||
| /// object cannot be read (corrupt object, unreadable store, a mid-read gc) is | ||
| /// an error, not an absence: `git cat-file` exits nonzero for both cases, so | ||
| /// the ref is re-resolved to tell them apart. `rev-parse` resolves the name | ||
| /// without reading the object, which is exactly the split needed (#426). | ||
| fn read_issue_blob(repo_path: &Path, ref_name: &str) -> Result<Option<String>> { | ||
| // allow-unbounded-git: this module's spawns predate the bounded runner and | ||
| // its callers run inside issue handlers that already hold the repo guard. | ||
| let cat_output = Command::new("git") | ||
| .args(["cat-file", "blob", ref_name]) | ||
| .current_dir(repo_path) | ||
| .output() | ||
| .context("failed to run git cat-file")?; | ||
|
|
||
| if cat_output.status.success() { | ||
| return Ok(Some( | ||
| String::from_utf8_lossy(&cat_output.stdout).to_string(), | ||
| )); | ||
| } | ||
|
|
||
| // allow-unbounded-git: failure-path existence recheck, at most one extra | ||
| // spawn per failed read. | ||
| let resolves = Command::new("git") | ||
| .args(["rev-parse", "--verify", "--quiet", ref_name]) | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| .current_dir(repo_path) | ||
| .output() | ||
| .context("failed to run git rev-parse")?; | ||
|
|
||
| if !resolves.status.success() { | ||
| return Ok(None); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the repository or ref store becomes unreadable after the initial ref resolution, both |
||
|
|
||
| let stderr = String::from_utf8_lossy(&cat_output.stderr); | ||
| anyhow::bail!("git cat-file failed for {ref_name}: {}", stderr.trim()); | ||
| } | ||
|
|
||
| /// List all issue refs and return their JSON content. | ||
| pub fn list_issues(repo_path: &Path) -> Result<Vec<String>> { | ||
| // List all refs under refs/gitlawb/issues/ | ||
|
|
@@ -88,16 +126,12 @@ | |
| continue; | ||
| } | ||
|
|
||
| // Read the blob content | ||
| let cat_output = Command::new("git") | ||
| .args(["cat-file", "blob", ref_name]) | ||
| .current_dir(repo_path) | ||
| .output() | ||
| .context("failed to run git cat-file")?; | ||
|
|
||
| if cat_output.status.success() { | ||
| let content = String::from_utf8_lossy(&cat_output.stdout).to_string(); | ||
| issues.push(content); | ||
| // Read the blob content. A ref that vanished between for-each-ref and | ||
| // here is skipped; a ref that resolves but fails to read propagates so | ||
| // a degraded object store errors the listing instead of under-reporting. | ||
| match read_issue_blob(repo_path, ref_name)? { | ||
| Some(content) => issues.push(content), | ||
| None => continue, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -161,8 +195,12 @@ | |
| None => return Ok(None), | ||
| }; | ||
|
|
||
| let raw = get_issue(repo_path, &full_id)? | ||
| .expect("ref existed in resolve but not in get — should be impossible"); | ||
| // A ref that resolved and is now absent was deleted between the two calls; | ||
| // any other read failure propagates as an error rather than panic (#426). | ||
| let raw = match get_issue(repo_path, &full_id)? { | ||
| Some(raw) => raw, | ||
| None => return Ok(None), | ||
| }; | ||
|
|
||
| let mut issue: serde_json::Value = | ||
| serde_json::from_str(&raw).context("invalid issue JSON in git ref")?; | ||
|
|
@@ -181,18 +219,7 @@ | |
| }; | ||
|
|
||
| let ref_name = format!("refs/gitlawb/issues/{full_id}"); | ||
| let cat_output = Command::new("git") | ||
| .args(["cat-file", "blob", &ref_name]) | ||
| .current_dir(repo_path) | ||
| .output() | ||
| .context("failed to run git cat-file")?; | ||
|
|
||
| if !cat_output.status.success() { | ||
| return Ok(None); | ||
| } | ||
|
|
||
| let content = String::from_utf8_lossy(&cat_output.stdout).to_string(); | ||
| Ok(Some(content)) | ||
| read_issue_blob(repo_path, &ref_name) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
|
|
@@ -294,4 +321,68 @@ | |
| let v: serde_json::Value = serde_json::from_str(&updated).unwrap(); | ||
| assert_eq!(v["status"], "closed"); | ||
| } | ||
|
|
||
| /// Overwrite the loose object behind an issue ref with garbage, so | ||
| /// `cat-file` fails while `rev-parse` still resolves the ref (#426). | ||
| fn corrupt_issue_object(dir: &TempDir, full_id: &str) { | ||
| let oid_out = Command::new("git") | ||
| .args(["rev-parse", &format!("refs/gitlawb/issues/{full_id}")]) | ||
| .current_dir(dir.path()) | ||
| .output() | ||
| .unwrap(); | ||
| let oid = String::from_utf8(oid_out.stdout) | ||
| .unwrap() | ||
| .trim() | ||
| .to_string(); | ||
| let obj = dir | ||
| .path() | ||
| .join(".git/objects") | ||
| .join(&oid[..2]) | ||
| .join(&oid[2..]); | ||
| std::fs::remove_file(&obj).unwrap(); | ||
| std::fs::write(&obj, b"garbage").unwrap(); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_get_and_close_missing_issue_return_none() { | ||
| let dir = TempDir::new().unwrap(); | ||
| init_repo(&dir); | ||
| assert_eq!(get_issue(dir.path(), "nosuch00").unwrap(), None); | ||
| assert_eq!(close_issue(dir.path(), "nosuch00").unwrap(), None); | ||
| } | ||
|
|
||
| // #426: a ref that resolves but whose object is corrupt is a read error, | ||
| // not an absent issue. `close_issue` used to `expect()` on that None and | ||
| // panic inside a request handler holding the write guard. | ||
| #[test] | ||
| fn test_corrupt_issue_object_errors_instead_of_panicking() { | ||
| let dir = TempDir::new().unwrap(); | ||
| init_repo(&dir); | ||
| let full_id = "bad00000-0000-0000-0000-000000000000"; | ||
| create_issue(dir.path(), full_id, r#"{"status":"open"}"#).unwrap(); | ||
| corrupt_issue_object(&dir, full_id); | ||
|
|
||
| assert!(get_issue(dir.path(), full_id).is_err()); | ||
| assert!(close_issue(dir.path(), full_id).is_err()); | ||
| assert!(list_issues(dir.path()).is_err()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_list_issues_returns_all() { | ||
| let dir = TempDir::new().unwrap(); | ||
| init_repo(&dir); | ||
| create_issue( | ||
| dir.path(), | ||
| "aaa00000-0000-0000-0000-000000000000", | ||
| r#"{"status":"open"}"#, | ||
| ) | ||
| .unwrap(); | ||
| create_issue( | ||
| dir.path(), | ||
| "bbb00000-0000-0000-0000-000000000000", | ||
| r#"{"status":"open"}"#, | ||
| ) | ||
| .unwrap(); | ||
| assert_eq!(list_issues(dir.path()).unwrap().len(), 2); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.