| 32 | 32 | | } |
| 33 | 33 | | } |
| 34 | 34 | | |
| 35 | + | /// The commit a pushed branch or tag is announced at, once the store has |
| 36 | + | /// the push: `None` when the store kept it elsewhere (it can refuse one |
| 37 | + | /// ref and accept another). |
| 38 | + | /// |
| 39 | + | /// A branch must be at the commit the push named. A tag reads back as the |
| 40 | + | /// commit it leads to, which for an annotated tag is not the tag object |
| 41 | + | /// the push named (`cli-v0.3.6`: tag accae3e8, commit 42a29574), so a tag |
| 42 | + | /// whose commit differs is taken as pushed unless what the push named is a |
| 43 | + | /// commit itself, and is announced at that commit, as `git.push` says its |
| 44 | + | /// `after` is. A tag the store cannot read back is taken as pushed. |
| 45 | + | pub(crate) async fn pushed_to<R: GitRepo>(git: &R, pushed: &crate::git_http::Pushed) -> Result<Option<String>> { |
| 46 | + | if let Some(branch) = pushed.branch() { |
| 47 | + | let head = git.log(branch, 1).await?; |
| 48 | + | return Ok(head.first().filter(|commit| commit.hash == pushed.after).map(|_| pushed.after.clone())); |
| 49 | + | } |
| 50 | + | let Ok(head) = git.log(&pushed.git_ref, 1).await else { |
| 51 | + | return Ok(Some(pushed.after.clone())); |
| 52 | + | }; |
| 53 | + | let Some(commit) = head.into_iter().next() else { |
| 54 | + | return Ok(Some(pushed.after.clone())); |
| 55 | + | }; |
| 56 | + | if commit.hash == pushed.after { |
| 57 | + | return Ok(Some(commit.hash)); |
| 58 | + | } |
| 59 | + | // A lightweight tag the store left at another commit. |
| 60 | + | if let Ok(Some(_)) = git.parents(&pushed.after).await { |
| 61 | + | return Ok(None); |
| 62 | + | } |
| 63 | + | Ok(Some(commit.hash)) |
| 64 | + | } |
| 65 | + | |
| 35 | 66 | | /// [`Repos::pushed`] on one repository's commits. |
| 36 | 67 | | pub(crate) async fn brought_in<R: GitRepo>(git: &R, a: PushedArgs) -> Result<Pushed> { |
| 37 | 68 | | let before = match &a.before { |
| ⋯ |
| 131 | 162 | | #[derive(Default)] |
| 132 | 163 | | struct Fake { |
| 133 | 164 | | commits: HashMap<String, Commit>, |
| 165 | + | /// Branches and tags by name, at the commit each leads to, as the |
| 166 | + | /// store reads them back. |
| 167 | + | refs: HashMap<String, String>, |
| 134 | 168 | | logs: RefCell<u32>, |
| 135 | 169 | | } |
| 136 | 170 | | |
| ⋯ |
| 150 | 184 | | async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>> { |
| 151 | 185 | | *self.logs.borrow_mut() += 1; |
| 152 | 186 | | let mut out = Vec::new(); |
| 187 | + | let git_ref = self.refs.get(git_ref).map_or(git_ref, String::as_str); |
| 153 | 188 | | let mut at = self.commits.get(git_ref); |
| 154 | 189 | | while let Some(commit) = at { |
| 155 | 190 | | if out.len() as u32 >= limit { |
| ⋯ |
| 277 | 312 | | assert_eq!(pushed.commits.len(), 21); |
| 278 | 313 | | assert_eq!(*git.logs.borrow(), 3); |
| 279 | 314 | | } |
| 315 | + | |
| 316 | + | fn push(git_ref: &str, before: Option<&str>, after: &str) -> crate::git_http::Pushed { |
| 317 | + | crate::git_http::Pushed { git_ref: git_ref.into(), before: before.map(str::to_owned), after: after.into() } |
| 318 | + | } |
| 319 | + | |
| 320 | + | fn with_refs(refs: &[(&str, &str)]) -> Fake { |
| 321 | + | Fake { refs: refs.iter().map(|&(name, at)| (name.to_owned(), at.to_owned())).collect(), ..Fake::with(history()) } |
| 322 | + | } |
| 323 | + | |
| 324 | + | #[test] |
| 325 | + | fn an_annotated_tag_is_announced_at_its_commit() { |
| 326 | + | // `git push g1t cli-v0.3.6`: the push names the tag object, and the |
| 327 | + | // store reads the tag back as the commit it leads to, main's head. |
| 328 | + | let git = with_refs(&[("main", "m2"), ("refs/tags/cli-v0.3.6", "m2")]); |
| 329 | + | let tag = push("refs/tags/cli-v0.3.6", None, "accae3e8"); |
| 330 | + | assert_eq!(run(pushed_to(&git, &tag)).unwrap().as_deref(), Some("m2")); |
| 331 | + | // Moved by a new tag object. |
| 332 | + | let moved = push("refs/tags/cli-v0.3.6", Some("accae3e8"), "b0b0b0b0"); |
| 333 | + | assert_eq!(run(pushed_to(&git, &moved)).unwrap().as_deref(), Some("m2")); |
| 334 | + | } |
| 335 | + | |
| 336 | + | #[test] |
| 337 | + | fn a_lightweight_tag_is_announced_where_the_store_has_it() { |
| 338 | + | let git = with_refs(&[("refs/tags/v1", "f2")]); |
| 339 | + | assert_eq!(run(pushed_to(&git, &push("refs/tags/v1", None, "f2"))).unwrap().as_deref(), Some("f2")); |
| 340 | + | // The push named a commit the store did not move the tag to. |
| 341 | + | assert_eq!(run(pushed_to(&git, &push("refs/tags/v1", Some("f2"), "m1"))).unwrap(), None); |
| 342 | + | // A tag the store cannot read back is taken as pushed. |
| 343 | + | assert_eq!(run(pushed_to(&git, &push("refs/tags/v2", None, "m1"))).unwrap().as_deref(), Some("m1")); |
| 344 | + | } |
| 345 | + | |
| 346 | + | #[test] |
| 347 | + | fn a_branch_is_announced_only_at_the_commit_pushed() { |
| 348 | + | let git = with_refs(&[("main", "m2")]); |
| 349 | + | assert_eq!(run(pushed_to(&git, &push("refs/heads/main", Some("m1"), "m2"))).unwrap().as_deref(), Some("m2")); |
| 350 | + | assert_eq!(run(pushed_to(&git, &push("refs/heads/main", Some("m2"), "f2"))).unwrap(), None); |
| 351 | + | assert_eq!(run(pushed_to(&git, &push("refs/heads/gone", None, "f2"))).unwrap(), None); |
| 352 | + | } |
| 280 | 353 | | } |