Something went wrong. Try again.
atproto git client
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228//! What a pull request record means.
use anyhow::Result;
/// The branch a pull targets.////// Modern records carry `target.branch`; records written before that field/// existed carry a bare `targetBranch`, and reading the field that is/// actually there is not a guess. Defaulting to `main` when neither is/// present *is* one, and it is refused instead: this is the one place a/// wrong answer is unrecoverable, because it names the branch a merge/// writes to.////// This existed six times across the verb files. Five agreed. The sixth was/// on `pr merge`, spelled `.unwrap_or("main")`, and would have landed a/// patch on a branch nobody named.////// `named` is how the record is identified in the refusal — an at-uri where/// the caller has one, since "a pull here" is no help when several are being/// linked at once.pub(crate) fn target_branch(value: &serde_json::Value, named: &str) -> Result<String> { target_branch_of(value).map(str::to_string).ok_or_else(|| { anyhow::anyhow!("{named} names no target branch; refusing to guess where this stack lands") })}
/// The two fields, without the refusal: for a reader that has something to/// say about "unknown" and does not have to pick a branch to write to.////// Three verb-local copies of this outlived the strict one, and they/// disagreed about the legacy field. The copy `pr create` uses to decide/// whether this branch already has an open pull aimed at the same target/// read only `target.branch`, so against a pre-`target` record it answered/// `None`, the comparison failed, and the warning about opening a second/// pull beside an existing one silently did not fire.pub(crate) fn target_branch_of(value: &serde_json::Value) -> Option<&str> { value["target"]["branch"] .as_str() .or_else(|| value["targetBranch"].as_str()) .filter(|b| !b.is_empty())}
/// The same, for a listing row, which carries the record under `value`.pub(crate) fn target_branch_of_row(row: &serde_json::Value) -> Result<String> { target_branch( &row["value"], row["uri"].as_str().unwrap_or("a stack member"), )}
/// One row of a repo's merged listing: the merged item — record under/// `"value"`, at-uri under `"uri"` — and the state label the listing settled/// for it.////// Two facts about one pull, and every chain rule needs both: the walk/// follows `dependentOn` out of the item, and whether a member is closed/// decides whether the walk may read past it. It lived in the listing reader/// that first produced it, which put the type a chain is made of one layer/// above the rules about chains.pub(crate) struct StackRow { pub item: serde_json::Value, pub state: String,}
/// How many review rounds a pull has.////// A record predating the rounds array carries a single inline `patch` and no/// array at all; it is one round by definition, and `pr comment` still has to/// name a round index for it. An array that is present but *empty* is the/// same situation reached from the other side, so it answers one too.////// This existed twice and the two disagreed on exactly that empty array —/// one answering `0`, one `1` — so a pull could be described as having no/// rounds by the reader and round 1 by the reviewer.pub(crate) fn round_count(value: &serde_json::Value) -> usize { value["rounds"].as_array().map_or(1, Vec::len).max(1)}
#[cfg(test)]mod tests { use super::*; use serde_json::json;
#[test] fn target_branch_prefers_the_modern_field() { let v = json!({"target": {"branch": "trunk"}, "targetBranch": "old"}); assert_eq!(target_branch(&v, "x").unwrap(), "trunk"); }
#[test] fn target_branch_falls_back_to_the_legacy_field() { let v = json!({"targetBranch": "old"}); assert_eq!(target_branch(&v, "x").unwrap(), "old"); }
#[test] fn target_branch_refuses_rather_than_guessing_main() { let err = target_branch(&json!({}), "at://x/y/z") .unwrap_err() .to_string(); assert!(err.contains("at://x/y/z"), "{err}"); assert!( !err.contains("main"), "it must not reach for a default: {err}" ); }
#[test] fn target_branch_treats_an_empty_string_as_absent() { assert!(target_branch(&json!({"target": {"branch": ""}}), "x").is_err()); }
#[test] fn a_record_with_no_rounds_array_is_one_round() { assert_eq!(round_count(&json!({"patch": "diff"})), 1); }
#[test] fn an_empty_rounds_array_is_also_one_round() { assert_eq!(round_count(&json!({"rounds": []})), 1); }
#[test] fn rounds_are_counted_when_present() { assert_eq!(round_count(&json!({"rounds": [1, 2, 3]})), 3); }}
/// Where a pull record keeps the bytes of its latest patch.////// Two shapes, both real. A modern record carries `rounds[]`, each round/// naming a gzipped blob; a record written before that field existed carries/// the patch inline as a string, which is the shape Tangled's own backfill/// produced and which `pr view` has always read.////// **The two readers of this disagreed.** `pr`'s handled the inline shape;/// the one every `stack` path uses bailed `"has no rounds; nothing to read a/// patch from"` on exactly that record — so a pre-rounds pull could be/// viewed and diffed but neither merged nor counted, and/// [`round_count`] called it one round while the reader called it none.#[derive(Debug, PartialEq, Eq)]pub(crate) enum PatchBytes { /// A pre-rounds record: the patch is right there, uncompressed. Inline(String), /// A round's blob, to be fetched from the *author's* PDS and gunzipped. Blob { cid: String, size: u64 },}
/// The latest patch of a pull, wherever that record keeps it.////// `named` identifies the record in a refusal, since a caller reading a/// stack has several and "a pull" names none of them.////// An empty `rounds` array is the one case with no answer: [`round_count`]/// reports it as one round because a reader still has to name an index, but/// there are no bytes behind that index and saying so is the only honest/// reply.pub(crate) fn latest_patch(value: &serde_json::Value, named: &str) -> Result<PatchBytes> { if let Some(rounds) = value["rounds"].as_array().filter(|r| !r.is_empty()) { let round = rounds.last().expect("checked non-empty"); let cid = round["patchBlob"]["ref"]["$link"] .as_str() .ok_or_else(|| anyhow::anyhow!("{named}'s latest round has no patch blob reference"))?; return Ok(PatchBytes::Blob { cid: cid.to_string(), size: round["patchBlob"]["size"].as_u64().unwrap_or(0), }); } if let Some(patch) = value["patch"].as_str() { return Ok(PatchBytes::Inline(patch.to_string())); } anyhow::bail!("{named} has neither a round with a patch blob nor an inline patch")}
#[cfg(test)]mod patch_tests { use super::*; use serde_json::json;
#[test] fn the_last_round_is_the_latest_patch() { let v = json!({"rounds": [ {"patchBlob": {"ref": {"$link": "cid1"}, "size": 10}}, {"patchBlob": {"ref": {"$link": "cid2"}, "size": 20}}, ]}); assert_eq!( latest_patch(&v, "x").unwrap(), PatchBytes::Blob { cid: "cid2".to_string(), size: 20 } ); }
#[test] fn a_pre_rounds_record_carries_its_patch_inline() { // The case the stack reader used to refuse outright. let v = json!({"patch": "diff --git a/x b/x\n"}); assert_eq!( latest_patch(&v, "x").unwrap(), PatchBytes::Inline("diff --git a/x b/x\n".to_string()) ); }
#[test] fn an_empty_rounds_array_falls_through_to_the_inline_patch() { let v = json!({"rounds": [], "patch": "inline\n"}); assert_eq!( latest_patch(&v, "x").unwrap(), PatchBytes::Inline("inline\n".to_string()) ); }
#[test] fn a_record_with_neither_is_refused_by_name() { let err = latest_patch(&json!({"rounds": []}), "at://x/y/z") .unwrap_err() .to_string(); assert!(err.contains("at://x/y/z"), "{err}"); }
#[test] fn a_round_with_no_blob_reference_is_refused() { let v = json!({"rounds": [{"patchBlob": {"size": 1}}]}); assert!(latest_patch(&v, "x").is_err()); }}