Sam Jeston
History
112 lines·
3.8 KB
·Rust
| 1 | //! Keeps database state in step with repository refs after they change, |
| 2 | //! whether through a push or a merge in the web interface. |
| 3 | |
| 4 | use anyhow::Result; |
| 5 | use git2::Oid; |
| 6 | |
| 7 | use crate::db::{self, Repo}; |
| 8 | use crate::git; |
| 9 | use crate::state::AppState; |
| 10 | |
| 11 | enum Change { |
| 12 | /// The head branch moved or the merge base changed. |
| 13 | Updated { |
| 14 | id: i64, |
| 15 | head: Oid, |
| 16 | merge_base: Option<Oid>, |
| 17 | }, |
| 18 | /// The head is now contained in the base, e.g. merged on the command line. |
| 19 | Merged { |
| 20 | id: i64, |
| 21 | head: Oid, |
| 22 | merge_base: String, |
| 23 | }, |
| 24 | } |
| 25 | |
| 26 | /// Called after a successful push. |
| 27 | pub async fn after_push(state: &AppState, repo: &Repo, pusher: i64) -> Result<()> { |
| 28 | db::touch_pushed(&state.db, repo.id).await?; |
| 29 | let path = state.config.repo_path(repo.id); |
| 30 | let preferred = repo.default_branch.clone(); |
| 31 | let head = |
| 32 | tokio::task::spawn_blocking(move || git::ensure_head(&git::open(&path)?, &preferred)) |
| 33 | .await??; |
| 34 | if let Some(head) = head.filter(|h| *h != repo.default_branch) { |
| 35 | db::set_default_branch(&state.db, repo.id, &head).await?; |
| 36 | } |
| 37 | refresh_pulls(state, repo.id, Some(pusher)).await |
| 38 | } |
| 39 | |
| 40 | /// Re-evaluates every open pull request of a repository. |
| 41 | pub async fn refresh_pulls(state: &AppState, repo_id: i64, actor: Option<i64>) -> Result<()> { |
| 42 | let pulls = db::list_pulls(&state.db, repo_id, true).await?; |
| 43 | if pulls.is_empty() { |
| 44 | return Ok(()); |
| 45 | } |
| 46 | let path = state.config.repo_path(repo_id); |
| 47 | let changes = tokio::task::spawn_blocking(move || -> Result<Vec<Change>> { |
| 48 | let repo = git::open(&path)?; |
| 49 | let mut changes = Vec::new(); |
| 50 | for p in pulls { |
| 51 | // A deleted head branch leaves the pull request as it was. |
| 52 | let Some(head) = git::branch_tip(&repo, &p.head_branch) else { |
| 53 | continue; |
| 54 | }; |
| 55 | let head_changed = p.head_sha.as_deref() != Some(head.to_string().as_str()); |
| 56 | if head_changed { |
| 57 | git::set_pull_ref(&repo, p.number, head)?; |
| 58 | } |
| 59 | let base = git::branch_tip(&repo, &p.base_branch); |
| 60 | match base { |
| 61 | Some(base) if git::is_ancestor(&repo, head, base) => { |
| 62 | let merge_base = p |
| 63 | .base_sha |
| 64 | .clone() |
| 65 | .or_else(|| git::merge_base(&repo, head, base).map(|o| o.to_string())) |
| 66 | .unwrap_or_else(|| head.to_string()); |
| 67 | changes.push(Change::Merged { |
| 68 | id: p.id, |
| 69 | head, |
| 70 | merge_base, |
| 71 | }); |
| 72 | } |
| 73 | _ => { |
| 74 | let merge_base = base.and_then(|b| git::merge_base(&repo, b, head)); |
| 75 | let base_changed = |
| 76 | merge_base.map(|m| m.to_string()) != p.base_sha && merge_base.is_some(); |
| 77 | if head_changed || base_changed { |
| 78 | changes.push(Change::Updated { |
| 79 | id: p.id, |
| 80 | head, |
| 81 | merge_base, |
| 82 | }); |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | Ok(changes) |
| 88 | }) |
| 89 | .await??; |
| 90 | |
| 91 | for change in changes { |
| 92 | match change { |
| 93 | Change::Updated { |
| 94 | id, |
| 95 | head, |
| 96 | merge_base, |
| 97 | } => { |
| 98 | let mb = merge_base.map(|m| m.to_string()); |
| 99 | db::update_pull_head(&state.db, id, &head.to_string(), mb.as_deref()).await?; |
| 100 | } |
| 101 | Change::Merged { |
| 102 | id, |
| 103 | head, |
| 104 | merge_base, |
| 105 | } => { |
| 106 | db::mark_merged(&state.db, id, actor, None, &head.to_string(), &merge_base).await?; |
| 107 | db::add_event(&state.db, id, actor, "merged", "").await?; |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | Ok(()) |
| 112 | } |