use super::*;
use crate::api::change::MergeMethod;
use crate::api::model::{AutoMerge, QueueState, Queued};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PrTab {
Conversation,
Commits,
Checks,
Review,
Reviewed(u64),
}
pub(super) fn pr_summary(page: &mut Page, pr: &PrRef, d: &PrDetail, now: u64) {
let author = login_seg(page, &d.author, Role::Strong);
let head = match &d.head_repo {
Some(repo) if *repo != pr.repo.to_string() => {
format!("{}:{}", repo.split('/').next().unwrap_or(repo), d.head_ref)
}
_ => d.head_ref.clone(),
};
let wants = match d.state {
PrState::Merged => " merged into ",
PrState::Closed => " wanted to merge into ",
PrState::Open | PrState::Draft => " wants to merge into ",
};
let mut segs = vec![
author,
Seg::new(wants, Role::Meta),
chip(&d.base_ref, Bg::PrimaryContainer),
Seg::new(" from ", Role::Meta),
chip(head, Bg::PrimaryContainer),
];
if d.head_gone {
segs.push(Seg::new(" · branch deleted", Role::Meta));
}
page.wrapped(segs, 0, Frame::None);
let mut meta = Vec::from(changes(d.additions, d.deletions));
meta.push(Seg::new(
format!(
" · opened {} · updated {}",
time::ago_iso(&d.created_at, now),
time::ago_iso(&d.updated_at, now)
),
Role::Meta,
));
if let Some(locked) = d.locked {
meta.push(Seg::new(" ", Role::Meta));
meta.push(chip(
super::conversation::locked_said(locked),
Bg::TertiaryContainer,
));
}
labels(&mut meta, &d.labels);
page.wrapped(meta, 0, Frame::None);
page.rule(0, Frame::None);
}
pub(super) fn merge_box(page: &mut Page, pr: &PrRef, d: &PrDetail, icons: Icons, timeline: bool) {
let readiness = d.readiness();
if matches!(readiness, Readiness::Merged | Readiness::Closed) {
return;
}
if timeline {
connector(page);
}
let (headline, role) = match (readiness, d.checks, d.review) {
_ if d.queued.is_some() => ("Queued to merge", Role::Accent),
(Readiness::Draft, ..) => ("This pull request is still a draft", Role::Meta),
(Readiness::Conflicts, ..) => (
"This branch has conflicts that must be resolved",
Role::Error,
),
(_, Some(ChecksState::Failing), _) => ("Some checks were not successful", Role::Error),
(_, Some(ChecksState::Pending), _) => ("Some checks haven't completed yet", Role::Accent),
(_, _, Some(ReviewDecision::ChangesRequested)) => ("Changes requested", Role::Error),
(_, _, Some(ReviewDecision::ReviewRequired)) => ("Review required", Role::Accent),
(Readiness::Checking, ..) => ("Checking whether it can be merged…", Role::Accent),
(Readiness::Behind, ..) => (
"This branch is out-of-date with the base branch",
Role::Error,
),
(Readiness::Blocked, ..) => ("Merging is blocked", Role::Error),
_ => ("This branch can be merged", Role::Success),
};
page.box_top(vec![Seg::new(headline, role)], Vec::new());
let review = d.review.map(|r| match r {
ReviewDecision::Approved => (Some(true), "Changes approved"),
ReviewDecision::ChangesRequested => (Some(false), "Changes requested"),
ReviewDecision::ReviewRequired => (None, "Review required"),
});
let checks = d.checks.map(|c| match c {
ChecksState::Passing => (Some(true), "All checks have passed"),
ChecksState::Failing => (Some(false), "Some checks were not successful"),
ChecksState::Pending => (None, "Some checks haven't completed yet"),
});
let conflicts = match d.mergeable {
Mergeable::Yes => (Some(true), "No conflicts with the base branch".to_owned()),
Mergeable::Conflicting => (Some(false), "This branch has conflicts".to_owned()),
Mergeable::Unknown => (None, "Checking for conflicts…".to_owned()),
};
let behind = d.behind_by.filter(|n| *n > 0).map(|n| {
let mark = if readiness == Readiness::Behind {
("✗", Role::Error)
} else {
("◦", Role::Meta)
};
(
mark,
format!("{} behind {}", plural(n, "commit"), d.base_ref),
)
});
let queue = d.queued.map(|q| {
let ok = (q.state == QueueState::Unmergeable).then_some(false);
(ok, queue_place(&q, &d.base_ref))
});
let auto = (d.auto_merge.as_ref()).map(|a| (None, auto_merging(a, d.merge_queue)));
let rows = [review, checks].into_iter().flatten();
let rows = (queue.into_iter().chain(auto))
.chain(rows.map(|(ok, text)| (ok, text.to_owned())))
.chain([conflicts]);
let rows = rows.map(|(ok, text)| {
let mark = match ok {
Some(true) => ("✓", Role::Success),
Some(false) => ("✗", Role::Error),
None => ("●", Role::Accent),
};
(mark, text)
});
let joins = (d.merge_queue && d.queued.is_none()).then(|| {
(
("◦", Role::Meta),
format!("{} merges through a merge queue", d.base_ref),
)
});
for ((icon, role), text) in rows.chain(behind).chain(joins) {
body(
page,
vec![
Seg::new(format!("{icon} "), role),
Seg::new(text, Role::Body),
],
);
}
page.box_rule();
item(page, url::pull_tab(pr, "files"), |page, link| {
let review = match d.changed_files {
1 => "Review the changed file".to_owned(),
n => format!("Review the {n} changed files"),
};
body(
page,
vec![
Seg::new(format!("{} ", icons.external()), Role::Meta),
Seg::linked(review, Role::Link, link),
],
);
});
page.box_bottom();
}
pub fn auto_merging(auto: &AutoMerge, queue: bool) -> String {
let how = if queue {
"added to the merge queue"
} else {
match auto.method {
MergeMethod::Merge => "merged with a merge commit",
MergeMethod::Squash => "squashed and merged",
MergeMethod::Rebase => "rebased and merged",
}
};
format!(
"Auto-merge enabled by {}: {how} when every requirement is met",
auto.by
)
}
pub fn queue_place(queued: &Queued, base: &str) -> String {
let place = match queued.position {
1 => "Next".to_owned(),
n => format!("#{n}"),
};
let state = match queued.state {
QueueState::AwaitingChecks => "waiting for its checks",
QueueState::Queued => "waiting its turn",
QueueState::Mergeable => "ready to merge",
QueueState::Locked => "merging now",
QueueState::Unmergeable => "it can't be merged",
};
format!("{place} in the merge queue for {base}: {state}")
}
pub fn pr_conversation(
page: &mut Page,
pr: &PrRef,
d: &PrDetail,
activity: Fetched<'_, PrActivity>,
projects: Fetched<'_, Capped<crate::api::project::InProject>>,
marks: Marks<'_>,
cx: PageCtx<'_>,
) {
let PageCtx { icons, keys, now } = cx;
#[expect(
clippy::disallowed_methods,
reason = "the conversation says how it's going"
)]
let a = activity.ready_unchecked();
let aside = page.build_aside(|side| {
match a {
Some(a) => {
timeline::reviewers_aside(
side,
pr,
&d.head_oid,
&a.latest,
&a.requested,
&d.author,
);
}
None => people(side, "Reviewers", &[], None, ""),
}
let more = (d.assignees.left_out() > 0)
.then(|| left_out_text(d.assignees.left_out(), "more", "more"));
people(side, "Assignees", &d.assignees, more, "No one assigned");
label_list(side, &d.labels);
milestone_aside(side, &pr.repo, d.milestone.as_ref());
timeline::projects_aside(side, projects);
if let Some(a) = a {
let none = "No issues linked";
timeline::linked_aside(side, icons, &pr.repo, "Closes", &a.closing, none);
if let Some(oid) = &a.merge_commit {
aside_heading(side, "Merge commit");
let target = url::commit(&pr.repo, oid);
let sha = link_seg(side, short_sha(oid), target, Role::Link);
side.wrapped(vec![sha], 0, Frame::None);
}
}
aside_heading(side, "Size");
let mut size = Vec::from(changes(d.additions, d.deletions));
size.push(Seg::new(
format!(" in {}", plural(d.changed_files, "file")),
Role::Meta,
));
side.wrapped(size, 0, Frame::None);
});
pr_summary(page, pr, d, now);
if !aside && let Some(a) = a {
timeline::pr_inline(page, icons, pr, d, a);
timeline::projects_inline(page, projects);
}
let op = &d.author;
let talk = Conversation {
base: LinkBase::new(&pr.repo, &d.head_oid, ""),
op,
now,
unfolded: marks.unfolded,
};
talk.said(
page,
op,
"opened",
&d.created_at,
&d.body,
true,
&d.editable.reactions,
);
let Some(a) = activity.show_or(page, "the conversation", |page| {
connector(page);
page.line(vec![Seg::new(" Loading the conversation…", Role::Meta)]);
}) else {
return;
};
earlier_here(page, &a.timeline, "comment or event", "comments and events");
let place = timeline::Place {
repo: &pr.repo,
pr: Some(pr),
threads: &a.threads,
icons,
};
timeline::entries(page, &talk, &place, &a.timeline, marks);
merge_box(page, pr, d, icons, true);
add_comment(page, keys);
}
pub fn pr_checks(page: &mut Page, pr: &PrRef, d: &PrDetail, c: Fetched<'_, Checks>, now: u64) {
pr_summary(page, pr, d, now);
page.blank();
checks(page, c, now);
}
pub fn actions(
page: &mut Page,
repo: &RepoId,
query: &str,
runs: Fetched<'_, Results<RunSummary>>,
cx: PageCtx<'_>,
) {
filter_field(page, query, cx.keys, "filter");
let ignored = RunFilter::parse(query).ignored;
if !ignored.is_empty() {
let words = ignored.iter().map(|w| format!("“{w}”")).collect::<Vec<_>>();
let text = format!(
"Not filtered by {}: runs filter by workflow:, branch:, is:, event: and actor:",
words.join(", ")
);
page.wrapped(vec![Seg::new(text, Role::Meta)], 0, Frame::None);
}
page.blank();
let empty = if query.is_empty() {
"No runs yet."
} else {
"No runs match this filter."
};
run_list(page, repo, runs, empty, cx.now);
}