Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

882 linesCodeBlameRaw
1//! Where a pull request stands, and merging it on the repository's say.
2//!
3//! A pull request g1t's agent once made (`made_by_g1t`, `managed = 1`)
4//! still shows where it stands: its checks, the approvals the branch
5//! wants, the merge queue, and whether a person has to step in. Where the
6//! repository's settings say ready pull requests land by themselves
7//! (`auto_merge`), such a pull request is merged here once everything its
8//! branch's rules ask for is met, attributed to `g1t` (`POLICY_ACTOR_ID`).
9//!
10//! A merge asked for while a pull request is behind is recorded here
11//! (`request_landing`) and lands once the push that brings it up to date
12//! arrives (`land_if_requested`).
13
14use g1t_contracts::repos::{GetByIdArgs, Repo, RepoPath};
15use g1t_contracts::time::rfc3339;
16use g1t_contracts::work::*;
17use g1t_contracts::{FailureCode, Membership, Outcome, User, Viewer};
18use g1t_kit::now_ms;
19use std::collections::HashMap;
20
21use serde::Deserialize;
22use worker::Result;
23use worker::wasm_bindgen::JsValue;
24
25use crate::Work;
26use crate::prefetch::Slot;
27use crate::reviews::{AGENT_ID, AGENT_NAME};
28use crate::statuses::{self, WorkflowFacts};
29use crate::rows::ValueRow;
30
31/// How long a claimed step is waited for before it may be taken again.
32const CATCH_UP_MINUTES: u64 = 30;
33const MERGE_MINUTES: u64 = 2;
34/// Who a merge made by a repository's settings is attributed to. Not an
35/// account: `g1t` cannot be registered.
36pub(crate) const POLICY_ACTOR_ID: &str = "g1t_policy";
37pub(crate) const POLICY_ACTOR_NAME: &str = "g1t";
38const MANAGED_PAGE: u32 = 200;
39
40/// The part of a pull request's row that tracks its lifecycle.
41#[derive(Deserialize)]
42struct Progress {
43 managed: u8,
44 revisions: u32,
45 revised_at: Option<String>,
46 working_on: Option<String>,
47 working_until: Option<String>,
48 stalled: Option<String>,
49}
50
51/// A merge asked for while the pull request was behind, as stored: a
52/// fast-forward, which needs it up to date first (merging.rs).
53#[derive(serde::Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55struct LandRequest {
56 actor: User,
57 keep_issue_open: bool,
58 /// Delete its branch once merged, as asked; the repository's setting
59 /// when absent.
60 #[serde(default)]
61 delete_branch: Option<bool>,
62}
63
64#[derive(Deserialize)]
65struct LandRow {
66 land_requested: Option<String>,
67 land_requested_at: Option<String>,
68 stalled: Option<String>,
69}
70
71/// What should happen next, if it is g1t's turn.
72pub(crate) enum Next {
73 /// A step is under way, or it is a person's turn.
74 Wait,
75 /// Land it, because the repository says ready pull requests land.
76 Merge,
77}
78
79/// Whether g1t's agent made this pull request, and so g1t shows where it
80/// stands. Only pull requests from before g1t stopped hosting agents.
81pub(crate) fn made_by_g1t(pull: &Pull) -> bool {
82 pull.runtime == Runtime::Hosted && pull.agent == AGENT_NAME && pull.fork.is_some()
83}
84
85/// Whether a verdict by `reviewer` is a person's other than the pull
86/// request's owner (whoever asked g1t for it, or its author): neither
87/// theirs nor g1t's agent's.
88pub(crate) fn from_someone_else(pull: &Pull, reviewer: &str) -> bool {
89 !pull.is_owned_by(reviewer) && reviewer != AGENT_ID
90}
91
92fn at(stage: Stage, detail: impl Into<String>, revisions: u32) -> Lifecycle {
93 Lifecycle {
94 stage,
95 detail: detail.into(),
96 revisions,
97 }
98}
99
100/// Everything the next step depends on.
101struct Facts {
102 /// Still being made: not yet marked ready for review.
103 draft: bool,
104 /// Why g1t stopped, if it has.
105 stalled: Option<String>,
106 /// The step under way, if one was claimed and is still being waited for.
107 working_on: Option<String>,
108 /// `failed` when the merge queue took it out.
109 check_status: Option<CheckStatus>,
110 revisions: u32,
111 /// Whether the branch it would land on has moved without it.
112 behind: bool,
113 /// Whether it is known to conflict with the branch it would land on.
114 conflicting: bool,
115 /// Whether the repository lands a ready pull request by itself.
116 auto_merge: bool,
117 /// Whether the repository refuses to merge one that is behind.
118 require_up_to_date: bool,
119 /// What the repository's approval rule still wants, if anything.
120 approvals_missing: Option<String>,
121 /// The person who asked for changes since the last revision, if one did.
122 person_request: Option<String>,
123 /// Its place in the merge queue, and what is ahead of it there.
124 queued: Option<(QueueState, Vec<u32>)>,
125 /// What the checks on its head say, against the required ones.
126 workflows: WorkflowFacts,
127}
128
129/// Where a pull request stands, and whether to merge it.
130///
131/// The order is: nothing while a step is under way; a person asking for
132/// changes holds it; its checks must finish, and the required ones pass;
133/// it must not conflict, and where the repository insists, it must be up
134/// to date; then the approvals the branch wants. Once everything is met,
135/// it merges where the repository says so.
136fn decide(facts: Facts) -> (Lifecycle, Next) {
137 let revisions = facts.revisions;
138 let wait = |stage, detail: &str| (at(stage, detail, revisions), Next::Wait);
139
140 if facts.draft {
141 return wait(Stage::Working, "It is still a draft.");
142 }
143 if let Some(reason) = &facts.stalled {
144 return wait(Stage::NeedsYou, reason);
145 }
146 if let Some((state, ahead)) = &facts.queued {
147 let named = ahead.iter().map(|n| format!("#{n}")).collect::<Vec<_>>().join(", ");
148 let detail = match (state, ahead.is_empty()) {
149 (QueueState::Testing, true) => "In the merge queue: being tested on the default branch as it is.".to_owned(),
150 (QueueState::Testing, false) => {
151 format!("In the merge queue: being tested together with {named}, ahead of it.")
152 }
153 (QueueState::Passed, true) => "Passed in the merge queue. Landing.".to_owned(),
154 (QueueState::Passed, false) => {
155 format!("Passed in the merge queue together with {named}. It lands once they have.")
156 }
157 _ => "In the merge queue, waiting for its turn to be tested.".to_owned(),
158 };
159 return wait(Stage::Queued, &detail);
160 }
161 match facts.working_on.as_deref() {
162 Some("catch_up") => {
163 return wait(Stage::CatchingUp, "Bringing it up to date with the branch it will land on.");
164 }
165 Some("merge") => return wait(Stage::Ready, "Merging."),
166 _ => {}
167 }
168 if let Some(name) = &facts.person_request {
169 return wait(Stage::NeedsYou, &format!("{name} asked for changes."));
170 }
171 match facts.check_status {
172 Some(CheckStatus::Failed) => return wait(Stage::NeedsYou, "It failed in the merge queue."),
173 Some(CheckStatus::Errored) => return wait(Stage::NeedsYou, "Its checks could not be run."),
174 _ => {}
175 }
176 // Only the checks the branch requires hold it.
177 let required = facts.workflows.required_failed();
178 if !required.is_empty() {
179 return wait(
180 Stage::NeedsYou,
181 &format!(
182 "The required {} {} {}.",
183 if required.len() == 1 { "check" } else { "checks" },
184 statuses::list(&required),
185 if required.len() == 1 { "fails" } else { "fail" },
186 ),
187 );
188 }
189 if !facts.workflows.pending.is_empty() {
190 return wait(
191 Stage::Checking,
192 &format!("Waiting for {} to finish.", statuses::list(&facts.workflows.pending)),
193 );
194 }
195 let expected = facts.workflows.expected();
196 if !expected.is_empty() {
197 return wait(
198 Stage::Checking,
199 &format!(
200 "Waiting for the required {} {} to report on its latest commit.",
201 if expected.len() == 1 { "check" } else { "checks" },
202 statuses::list(&expected)
203 ),
204 );
205 }
206 if facts.conflicting {
207 return wait(
208 Stage::NeedsYou,
209 "It conflicts with the branch it will land on. Merge that branch in, resolve the conflicts and push.",
210 );
211 }
212 if facts.behind && facts.require_up_to_date {
213 return wait(
214 Stage::NeedsYou,
215 "The branch it will land on has moved, and this repository requires pull requests to be up to date. Catch up with it first.",
216 );
217 }
218 if let Some(missing) = &facts.approvals_missing {
219 return wait(Stage::NeedsYou, missing);
220 }
221 if facts.auto_merge {
222 return (
223 at(
224 Stage::Ready,
225 "Everything this repository asks for is met. Merging, as its settings say.",
226 revisions,
227 ),
228 Next::Merge,
229 );
230 }
231 wait(
232 Stage::Ready,
233 if facts.behind {
234 "Ready to merge. Merging brings it up to date with the default branch first."
235 } else {
236 "Everything this repository asks for is met. Ready to merge."
237 },
238 )
239}
240
241impl Work {
242 /// Where a pull request stands and whether g1t merges it, remembered
243 /// so lists can show it without working it out again. `None` for one
244 /// g1t is not seeing through.
245 pub(crate) async fn assess(
246 &self,
247 pull: &Pull,
248 _issue: &Option<Issue>,
249 behind: bool,
250 ) -> Result<Option<(Lifecycle, Next)>> {
251 let assessed = self.assess_now(pull, behind).await?;
252 if let Some((lifecycle, _)) = &assessed
253 && !self.remembered_as(&pull.id, lifecycle)?
254 {
255 self.remember(&pull.id, lifecycle).await?;
256 }
257 Ok(assessed)
258 }
259
260 /// Whether the row read for this request already says `lifecycle`, so
261 /// that showing a pull request does not write it again unchanged.
262 fn remembered_as(&self, pull_id: &str, lifecycle: &Lifecycle) -> Result<bool> {
263 let Some(found) = self.prefetched_pull(pull_id) else {
264 return Ok(false);
265 };
266 Ok(found
267 .first::<crate::rows::Snapshot>(Slot::Pull)?
268 .is_some_and(|stored| {
269 stored.stage == Some(lifecycle.stage) && stored.stage_detail.as_deref() == Some(lifecycle.detail.as_str())
270 }))
271 }
272
273 /// Saves where a pull request stands.
274 pub(crate) async fn remember(&self, pull_id: &str, lifecycle: &Lifecycle) -> Result<()> {
275 let stage = serde_json::to_value(lifecycle.stage)?;
276 self.db
277 .prepare("UPDATE pulls SET stage = ?, stage_detail = ? WHERE id = ?")
278 .bind(&[
279 stage.as_str().unwrap_or_default().into(),
280 lifecycle.detail.as_str().into(),
281 pull_id.into(),
282 ])?
283 .run()
284 .await?;
285 Ok(())
286 }
287
288 async fn assess_now(&self, pull: &Pull, behind: bool) -> Result<Option<(Lifecycle, Next)>> {
289 if !pull.status.is_active() {
290 return Ok(None);
291 }
292 // As read for this request (prefetch.rs), or now.
293 let prefetched = self.prefetched_pull(&pull.id);
294 let progress = match &prefetched {
295 Some(found) => found.first::<Progress>(Slot::Pull)?,
296 None => {
297 self.db
298 .prepare(
299 "SELECT managed, revisions, revised_at, working_on, working_until, stalled
300 FROM pulls WHERE id = ?",
301 )
302 .bind(&[pull.id.as_str().into()])?
303 .first::<Progress>(None)
304 .await?
305 }
306 };
307 let Some(progress) = progress.filter(|progress| progress.managed != 0) else {
308 return Ok(None);
309 };
310 let now = rfc3339(now_ms());
311 let working_on = progress
312 .working_until
313 .as_deref()
314 .is_some_and(|until| until > now.as_str())
315 .then(|| progress.working_on.clone().unwrap_or_default());
316 let settings = self.settings_for(pull).await?;
317 let statuses = self.statuses(&pull.repo_id, pull.head_commit.as_deref()).await?;
318 let workflows = self.workflow_facts(&pull.repo_id, pull.head_commit.as_deref(), &statuses, &settings.required_checks).await?;
319 // The rules of the branch it merges into, as g1t sees them when it
320 // merges by itself: what people must still do, and whether an
321 // agent's change may land here unattended.
322 let gate = match self.repo_for(pull).await? {
323 Some(repo) => Some(self.merge_gate(&repo, pull, Some(&User::system(&repo.namespace)), false, true).await?),
324 None => None,
325 };
326 let rules_allow_auto_merge = gate
327 .as_ref()
328 .is_none_or(|gate| g1t_rules::merge::auto_merge_refusal(&gate.requirements).is_none());
329 let (lifecycle, next) = decide(Facts {
330 draft: pull.status == PullStatus::Draft,
331 stalled: progress.stalled,
332 working_on,
333 check_status: pull.check_status,
334 revisions: progress.revisions,
335 behind,
336 conflicting: self.conflicting_files(pull).await?.is_some(),
337 auto_merge: settings.auto_merge && rules_allow_auto_merge,
338 require_up_to_date: settings.require_up_to_date,
339 approvals_missing: gate.as_ref().and_then(|gate| gate.people_gap()),
340 person_request: self.person_request(pull, progress.revised_at.as_deref()).await?,
341 queued: self.queued_entry(&pull.id).await?,
342 workflows,
343 });
344 Ok(Some((lifecycle, next)))
345 }
346
347 /// Who last asked for changes, if it was a person other than its owner
348 /// (whoever asked g1t for it, or its author), that person's latest
349 /// verdict, and after the last revision.
350 async fn person_request(&self, pull: &Pull, revised_at: Option<&str>) -> Result<Option<String>> {
351 #[derive(Deserialize)]
352 struct Verdicts {
353 author_id: String,
354 author_name: String,
355 verdict: Verdict,
356 created_at: String,
357 }
358 let rows = match self.prefetched_pull(&pull.id) {
359 Some(found) => found
360 .rows::<Verdicts>(Slot::Verdicts)?
361 .into_iter()
362 .filter(|row| from_someone_else(pull, &row.author_id))
363 .collect(),
364 None => self
365 .db
366 .prepare(
367 "SELECT author_id, author_name, verdict, created_at FROM comments
368 WHERE repo_id = ? AND number = ? AND verdict IS NOT NULL
369 AND author_id != ? AND author_id != ?
370 ORDER BY id",
371 )
372 .bind(&[
373 pull.repo_id.as_str().into(),
374 pull.number.into(),
375 pull.owner().id.as_str().into(),
376 AGENT_ID.into(),
377 ])?
378 .all()
379 .await?
380 .results::<Verdicts>()?,
381 };
382 // Each person's latest verdict is the one that stands.
383 let mut latest: HashMap<String, Verdicts> = HashMap::new();
384 for row in rows {
385 latest.insert(row.author_id.clone(), row);
386 }
387 Ok(latest
388 .into_values()
389 .filter(|row| row.verdict == Verdict::RequestChanges)
390 .filter(|row| revised_at.is_none_or(|revised| row.created_at.as_str() > revised))
391 .max_by(|a, b| a.created_at.cmp(&b.created_at))
392 .map(|row| row.author_name))
393 }
394
395 /// Takes a step for a pull request, if nobody else has. One statement,
396 /// so that two callers cannot both take it.
397 pub(crate) async fn claim(&self, pull_id: &str, step: &str, minutes: u64) -> Result<bool> {
398 let now = now_ms();
399 Ok(self
400 .db
401 .prepare(
402 "UPDATE pulls SET working_on = ?2, working_until = ?3
403 WHERE id = ?4 AND status = 'open' AND stalled IS NULL
404 AND (working_until IS NULL OR working_until < ?1)
405 RETURNING id AS value",
406 )
407 .bind(&[
408 rfc3339(now).into(),
409 step.into(),
410 rfc3339(now + minutes * 60 * 1000).into(),
411 pull_id.into(),
412 ])?
413 .first::<ValueRow>(None)
414 .await?
415 .is_some())
416 }
417
418 /// Merges a pull request g1t is seeing through once it is ready, where
419 /// the repository's settings say ready pull requests land by
420 /// themselves. Returns what the runner is to do, which is now always
421 /// nothing: no step is carried out in a sandbox any more.
422 pub(crate) async fn advance(&self, a: AdvanceArgs) -> Result<Advance> {
423 let Some(pull) = self.pull_by_id(&a.pull_id).await? else {
424 return Ok(Advance::None);
425 };
426 if pull.status != PullStatus::Open {
427 return Ok(Advance::None);
428 }
429 // A person asked for it to merge on its own (automerge.rs).
430 if pull.auto_merge.is_some() {
431 self.try_auto_merge(&pull).await?;
432 return Ok(Advance::None);
433 }
434 // As a member: a private repository would look missing otherwise,
435 // and the pull request would never move.
436 let viewer: Viewer = self.owner_viewer(&pull).await?;
437 let repo: Outcome<Repo> = g1t_kit::call(
438 &self.repos,
439 "get_by_id",
440 &GetByIdArgs {
441 id: pull.repo_id.clone(),
442 viewer,
443 },
444 )
445 .await?;
446 let Outcome::Ok(repo) = crate::retired::unless_archived(repo) else {
447 return Ok(Advance::None);
448 };
449 let behind = self.is_behind(&pull).await?;
450 if let Some((_, Next::Merge)) = self.assess(&pull, &None, behind).await? {
451 self.merge_by_policy(&repo, &pull).await?;
452 }
453 Ok(Advance::None)
454 }
455
456 /// Lands a pull request that is ready, on the authority of the
457 /// repository's settings instead of a person's click.
458 async fn merge_by_policy(&self, repo: &Repo, pull: &Pull) -> Result<()> {
459 if !self.claim(&pull.id, "merge", MERGE_MINUTES).await? {
460 return Ok(());
461 }
462 // g1t acts for the workspace whose members turned this on, with the
463 // default base permission (Write), which merging needs.
464 let actor = User {
465 id: POLICY_ACTOR_ID.to_owned(),
466 username: POLICY_ACTOR_NAME.to_owned(),
467 verified: true,
468 workspaces: vec![Membership::member(repo.namespace.to_lowercase())],
469 ..User::default()
470 };
471 let merged = self
472 .merge_pull(PullActionArgs {
473 actor,
474 repo: RepoPath {
475 namespace: repo.namespace.clone(),
476 name: repo.name.clone(),
477 },
478 number: pull.number,
479 summary: String::new(),
480 keep_issue_open: false,
481 ignore_checks: false,
482 bypass_rules: false,
483 // The repository's default method, message and branch
484 // setting.
485 merge_method: None,
486 commit_title: None,
487 commit_message: None,
488 expected_head: pull.head_commit.clone(),
489 delete_branch: None,
490 update_method: None,
491 })
492 .await?;
493 match merged {
494 Outcome::Ok(_) => Ok(()),
495 // Most likely the branch moved in the moment between: let go, and
496 // the next look at it tries again.
497 Outcome::Fail(failure) if failure.code == FailureCode::Conflict => {
498 self.db
499 .prepare(
500 "UPDATE pulls SET working_on = NULL, working_until = NULL
501 WHERE id = ? AND working_on = 'merge'",
502 )
503 .bind(&[pull.id.as_str().into()])?
504 .run()
505 .await?;
506 Ok(())
507 }
508 Outcome::Fail(failure) => {
509 self.stall(StallArgs {
510 pull_id: pull.id.clone(),
511 reason: format!("g1t could not merge this: {}", failure.message),
512 by: None,
513 })
514 .await?;
515 Ok(())
516 }
517 }
518 }
519
520 /// Records that a merge was asked for while the pull request was
521 /// behind, so that it lands once the push that brings it up to date
522 /// arrives, and announces it.
523 pub(crate) async fn request_landing(
524 &self,
525 pull: &Pull,
526 actor: &User,
527 keep_issue_open: bool,
528 delete_branch: Option<bool>,
529 ) -> Result<()> {
530 let now = now_ms();
531 let request = serde_json::to_string(&LandRequest {
532 actor: actor.clone(),
533 keep_issue_open,
534 delete_branch,
535 })?;
536 self.db
537 .prepare(
538 "UPDATE pulls
539 SET land_requested = ?, land_requested_at = ?, stalled = NULL,
540 working_on = 'catch_up', working_until = ?
541 WHERE id = ?",
542 )
543 .bind(&[
544 request.into(),
545 rfc3339(now).into(),
546 rfc3339(now + CATCH_UP_MINUTES * 60 * 1000).into(),
547 pull.id.as_str().into(),
548 ])?
549 .run()
550 .await?;
551 self.publish(
552 "pull.merge_requested",
553 &pull.repo_id,
554 actor,
555 Self::pull_event(pull),
556 )
557 .await
558 }
559
560 /// The merge waiting on a pull request, if one was asked for recently
561 /// enough to still stand.
562 async fn land_request(&self, pull_id: &str) -> Result<Option<LandRequest>> {
563 let row = self.land_row(pull_id).await?;
564 let oldest = rfc3339(now_ms().saturating_sub(CATCH_UP_MINUTES * 60 * 1000));
565 Ok(row
566 .filter(|row| {
567 row.land_requested_at
568 .as_deref()
569 .is_some_and(|at| at >= oldest.as_str())
570 })
571 .and_then(|row| row.land_requested)
572 .and_then(|request| serde_json::from_str(&request).ok()))
573 }
574
575 /// Whether a merge is waiting on a pull request, and why g1t stopped
576 /// working on it if it did.
577 pub(crate) async fn landing_state(&self, pull_id: &str) -> Result<(bool, Option<String>)> {
578 let stalled = self.land_row(pull_id).await?.and_then(|row| row.stalled);
579 Ok((self.land_request(pull_id).await?.is_some(), stalled))
580 }
581
582 /// The merge waiting on a pull request and why g1t stopped, as read
583 /// for this request (prefetch.rs) or now.
584 async fn land_row(&self, pull_id: &str) -> Result<Option<LandRow>> {
585 if let Some(found) = self.prefetched_pull(pull_id) {
586 return found.first::<LandRow>(Slot::Pull);
587 }
588 self.db
589 .prepare("SELECT land_requested, land_requested_at, stalled FROM pulls WHERE id = ?")
590 .bind(&[pull_id.into()])?
591 .first::<LandRow>(None)
592 .await
593 }
594
595 /// Forgets a merge that was waiting on a pull request, and the step
596 /// that was bringing it up to date.
597 pub(crate) async fn forget_landing(&self, pull_id: &str) -> Result<()> {
598 self.db
599 .prepare(
600 "UPDATE pulls SET land_requested = NULL, land_requested_at = NULL,
601 working_on = CASE WHEN working_on = 'catch_up' THEN NULL ELSE working_on END,
602 working_until = CASE WHEN working_on = 'catch_up' THEN NULL ELSE working_until END
603 WHERE id = ?",
604 )
605 .bind(&[pull_id.into()])?
606 .run()
607 .await?;
608 Ok(())
609 }
610
611 /// Lands a pull request whose head has just moved, if a merge of it was
612 /// waiting for exactly that. What it was caught up to was already
613 /// checked and reviewed apart from the merge, so the checks are not
614 /// waited for again; a repository that wants them rerun turns on
615 /// "require up to date", and then nothing is landed this way.
616 pub(crate) async fn land_if_requested(&self, pull_id: &str) -> Result<()> {
617 let Some(request) = self.land_request(pull_id).await? else {
618 return Ok(());
619 };
620 self.forget_landing(pull_id).await?;
621 let Some(pull) = self.pull_by_id(pull_id).await? else {
622 return Ok(());
623 };
624 let repo: Outcome<Repo> = g1t_kit::call(
625 &self.repos,
626 "get_by_id",
627 &GetByIdArgs {
628 id: pull.repo_id.clone(),
629 viewer: Some(request.actor.clone()),
630 },
631 )
632 .await?;
633 let Outcome::Ok(repo) = crate::retired::unless_archived(repo) else {
634 return Ok(());
635 };
636 let merged = self
637 .merge_pull(PullActionArgs {
638 actor: request.actor,
639 repo: RepoPath {
640 namespace: repo.namespace,
641 name: repo.name,
642 },
643 number: pull.number,
644 summary: String::new(),
645 keep_issue_open: request.keep_issue_open,
646 ignore_checks: true,
647 bypass_rules: false,
648 // Only a fast-forward waits to be brought up to date.
649 merge_method: Some(g1t_contracts::rules::MergeMethod::FastForward),
650 commit_title: None,
651 commit_message: None,
652 expected_head: None,
653 delete_branch: request.delete_branch,
654 update_method: None,
655 })
656 .await?;
657 if let Outcome::Fail(failure) = merged {
658 self.stall(StallArgs {
659 pull_id: pull.id,
660 reason: format!(
661 "It was brought up to date but could not be merged: {}",
662 failure.message
663 ),
664 by: None,
665 })
666 .await?;
667 }
668 Ok(())
669 }
670
671 /// Stops seeing a pull request through until a person steps in. The
672 /// first stop is published (`pull.stalled`), which tells its people
673 /// that it needs them; a stop on one already stopped only says why.
674 pub(crate) async fn stall(&self, a: StallArgs) -> Result<bool> {
675 let before = self.pull_by_id(&a.pull_id).await?;
676 let was_stalled = self.is_stalled(&a.pull_id).await?;
677 self.db
678 .prepare(
679 "UPDATE pulls
680 SET stalled = ?, working_on = NULL, working_until = NULL,
681 land_requested = NULL, land_requested_at = NULL
682 WHERE id = ? AND status = 'open'",
683 )
684 .bind(&[a.reason.trim().into(), a.pull_id.as_str().into()])?
685 .run()
686 .await?;
687 self.db
688 .prepare("UPDATE pulls SET stage = 'needs_you', stage_detail = ? WHERE id = ?")
689 .bind(&[a.reason.trim().into(), a.pull_id.as_str().into()])?
690 .run()
691 .await?;
692 if let Some(pull) = before.filter(|pull| !was_stalled && pull.status == PullStatus::Open) {
693 self.publish_as(
694 "pull.stalled",
695 &pull.repo_id,
696 a.by.clone(),
697 g1t_contracts::events::PullEvent {
698 detail: Some(a.reason.trim().to_owned()),
699 ..Self::pull_event(&pull)
700 },
701 )
702 .await?;
703 }
704 Ok(true)
705 }
706
707 /// Whether g1t has stopped seeing the pull request through.
708 pub(crate) async fn is_stalled(&self, pull_id: &str) -> Result<bool> {
709 Ok(self
710 .db
711 .prepare("SELECT 1 AS value FROM pulls WHERE id = ? AND stalled IS NOT NULL")
712 .bind(&[pull_id.into()])?
713 .first::<u32>(Some("value"))
714 .await?
715 .is_some())
716 }
717
718 /// Says that a pull request g1t had stopped on is going again, which
719 /// closes what it was waiting on a person for.
720 pub(crate) async fn announce_resumed(&self, pull_id: &str, actor: Option<String>) -> Result<()> {
721 if let Some(pull) = self.pull_by_id(pull_id).await? {
722 self.publish_as("pull.resumed", &pull.repo_id, actor, Self::pull_event(&pull)).await?;
723 }
724 Ok(())
725 }
726
727 pub(crate) async fn managed_pulls(&self, a: ManagedPullsArgs) -> Result<Vec<String>> {
728 let rows = self
729 .db
730 .prepare(
731 "SELECT id AS value FROM pulls
732 WHERE status = 'open' AND ((managed = 1 AND stalled IS NULL) OR auto_merge IS NOT NULL)
733 AND (?1 IS NULL OR repo_id = ?1)
734 ORDER BY updated_at DESC LIMIT ?2",
735 )
736 .bind(&[
737 a.repo_id.map_or(JsValue::NULL, JsValue::from),
738 MANAGED_PAGE.into(),
739 ])?
740 .all()
741 .await?
742 .results::<ValueRow>()?;
743 Ok(rows.into_iter().map(|row| row.value).collect())
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750
751 #[test]
752 fn a_verdict_from_whoever_asked_for_g1t_s_change_is_not_someone_else_s() {
753 use crate::rows::stored::{ASKER, G1T, pull};
754 let made = pull(G1T, Some(ASKER));
755 assert!(made_by_g1t(&made));
756 assert!(!from_someone_else(&made, ASKER.0));
757 assert!(!from_someone_else(&made, AGENT_ID));
758 assert!(from_someone_else(&made, "usr_reviewer"));
759 let own = pull(ASKER, None);
760 assert!(!from_someone_else(&own, ASKER.0));
761 assert!(from_someone_else(&own, "usr_reviewer"));
762 }
763
764 /// Checks on a head commit: `(context, state)` statuses, against the
765 /// required check names.
766 fn checks(statuses: &[(&str, &str)], required: &[&str]) -> WorkflowFacts {
767 let statuses: Vec<CommitStatus> = statuses
768 .iter()
769 .map(|(context, state)| CommitStatus {
770 context: (*context).to_owned(),
771 state: (*state).to_owned(),
772 description: None,
773 target_url: None,
774 updated_at: String::new(),
775 source: None,
776 check_run_id: None,
777 })
778 .collect();
779 let required: Vec<String> = required.iter().map(|name| (*name).to_owned()).collect();
780 WorkflowFacts::of(&statuses, &required)
781 }
782
783 /// A pull request that is ready for review, whose required check
784 /// passed, and nothing else yet.
785 fn facts() -> Facts {
786 Facts {
787 draft: false,
788 stalled: None,
789 working_on: None,
790 check_status: None,
791 revisions: 0,
792 behind: false,
793 conflicting: false,
794 auto_merge: false,
795 require_up_to_date: false,
796 approvals_missing: None,
797 person_request: None,
798 queued: None,
799 workflows: checks(&[("CI / pull_request", "success")], &["CI"]),
800 }
801 }
802
803 /// The stage, and a word for the step to take.
804 fn outcome(facts: Facts) -> (Stage, &'static str) {
805 let (lifecycle, next) = decide(facts);
806 let step = match next {
807 Next::Wait => "wait",
808 Next::Merge => "merge",
809 };
810 (lifecycle.stage, step)
811 }
812
813 #[test]
814 fn a_ready_change_lands_by_itself_only_where_the_repository_says_so() {
815 assert_eq!(outcome(facts()), (Stage::Ready, "wait"));
816 assert_eq!(outcome(Facts { auto_merge: true, ..facts() }), (Stage::Ready, "merge"));
817 // One that is behind is merged too: merging brings it up to date.
818 assert_eq!(outcome(Facts { auto_merge: true, behind: true, ..facts() }), (Stage::Ready, "merge"));
819 // Unless the repository wants it caught up first.
820 let strict = Facts { auto_merge: true, behind: true, require_up_to_date: true, ..facts() };
821 assert_eq!(outcome(strict), (Stage::NeedsYou, "wait"));
822 }
823
824 #[test]
825 fn approvals_the_repository_wants_are_waited_for() {
826 let short = Facts {
827 approvals_missing: Some("This repository requires 1 approving review.".to_owned()),
828 auto_merge: true,
829 ..facts()
830 };
831 let (lifecycle, next) = decide(short);
832 assert_eq!(lifecycle.stage, Stage::NeedsYou);
833 assert_eq!(lifecycle.detail, "This repository requires 1 approving review.");
834 assert!(matches!(next, Next::Wait));
835 }
836
837 #[test]
838 fn checks_are_waited_for_and_a_failing_required_one_needs_a_person() {
839 let running = Facts { workflows: checks(&[("CI / pull_request", "pending")], &["CI"]), auto_merge: true, ..facts() };
840 let (lifecycle, next) = decide(running);
841 assert_eq!(lifecycle.stage, Stage::Checking);
842 assert!(lifecycle.detail.contains("Waiting for CI / pull_request"));
843 assert!(matches!(next, Next::Wait));
844 let failed = Facts { workflows: checks(&[("CI / pull_request", "failure")], &["CI"]), ..facts() };
845 let (lifecycle, _) = decide(failed);
846 assert_eq!(lifecycle.stage, Stage::NeedsYou);
847 assert_eq!(lifecycle.detail, "The required check CI fails.");
848 // A check the branch does not require does not hold it.
849 let lint = checks(&[("CI / pull_request", "success"), ("Lint / pull_request", "failure")], &["CI"]);
850 assert_eq!(outcome(Facts { workflows: lint, ..facts() }), (Stage::Ready, "wait"));
851 let missing = Facts { workflows: checks(&[("CI / pull_request", "success")], &["CI", "Deploy"]), auto_merge: true, ..facts() };
852 let (lifecycle, next) = decide(missing);
853 assert_eq!(lifecycle.detail, "Waiting for the required check Deploy to report on its latest commit.");
854 assert!(matches!(next, Next::Wait), "auto-merge must not land it");
855 }
856
857 #[test]
858 fn the_merge_queue_conflicts_and_people_hold_it() {
859 let queued = Facts { queued: Some((QueueState::Testing, vec![12, 14])), auto_merge: true, ..facts() };
860 let (lifecycle, _) = decide(queued);
861 assert_eq!(lifecycle.stage, Stage::Queued);
862 assert!(lifecycle.detail.contains("#12, #14"));
863 assert_eq!(outcome(Facts { check_status: Some(CheckStatus::Failed), auto_merge: true, ..facts() }), (Stage::NeedsYou, "wait"));
864 assert_eq!(outcome(Facts { check_status: Some(CheckStatus::Errored), ..facts() }), (Stage::NeedsYou, "wait"));
865 assert_eq!(outcome(Facts { conflicting: true, auto_merge: true, ..facts() }), (Stage::NeedsYou, "wait"));
866 let asked = Facts { person_request: Some("ana".to_owned()), auto_merge: true, ..facts() };
867 let (lifecycle, next) = decide(asked);
868 assert_eq!(lifecycle.detail, "ana asked for changes.");
869 assert!(matches!(next, Next::Wait));
870 }
871
872 #[test]
873 fn drafts_steps_under_way_and_stops_wait() {
874 assert_eq!(outcome(Facts { draft: true, ..facts() }), (Stage::Working, "wait"));
875 assert_eq!(outcome(Facts { working_on: Some("catch_up".into()), auto_merge: true, ..facts() }), (Stage::CatchingUp, "wait"));
876 assert_eq!(outcome(Facts { working_on: Some("merge".into()), auto_merge: true, ..facts() }), (Stage::Ready, "wait"));
877 let (lifecycle, next) = decide(Facts { stalled: Some("It could not be merged.".to_owned()), auto_merge: true, ..facts() });
878 assert_eq!(lifecycle.stage, Stage::NeedsYou);
879 assert_eq!(lifecycle.detail, "It could not be merged.");
880 assert!(matches!(next, Next::Wait));
881 }
882}