feat: sort and filter pull requests by review state

This commit is contained in:
Valdemar Lorenzen
2026-10-21 12:41:17 +02:00
parent 5c08aa0a67
commit 37b47ebc8a
7 changed files with 206 additions and 7 deletions
+76 -1
View File
@@ -109,6 +109,40 @@ pub enum PullRequestReviewState {
Requested,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub(crate) enum PullRequestReviewScope {
#[default]
All,
Requested,
Approved,
}
impl PullRequestReviewScope {
pub(crate) fn label(self) -> &'static str {
match self {
Self::All => "All",
Self::Requested => "Requested",
Self::Approved => "Approved",
}
}
fn next(self) -> Self {
match self {
Self::All => Self::Requested,
Self::Requested => Self::Approved,
Self::Approved => Self::All,
}
}
fn matches(self, state: PullRequestReviewState) -> bool {
match self {
Self::All => true,
Self::Requested => state == PullRequestReviewState::Requested,
Self::Approved => state == PullRequestReviewState::Approved,
}
}
}
#[derive(Debug)]
pub struct PullRequestPage {
pub kind: RepoKind,
@@ -582,6 +616,8 @@ pub struct DiffState {
pull_request_categories: [PullRequestCategory; 2],
pull_request_order: BTreeMap<u64, (String, usize, PullRequestReviewState)>,
pub(crate) pull_request_retries: [u64; 2],
pub(crate) pull_request_filter: PullRequestReviewScope,
pub(crate) pull_request_priority: PullRequestReviewScope,
pub phase: Phase,
pub word_wrap: bool,
pub view_mode: ViewMode,
@@ -646,6 +682,8 @@ impl DiffState {
pull_request_categories: Default::default(),
pull_request_order: BTreeMap::new(),
pull_request_retries: [0; 2],
pull_request_filter: PullRequestReviewScope::All,
pull_request_priority: PullRequestReviewScope::All,
phase: Phase::Before,
word_wrap: false,
view_mode: ViewMode::Split,
@@ -1324,6 +1362,10 @@ impl DiffState {
entry.category,
Category::Open | Category::Closed | Category::Drafts
)))
|| (self.pull_request_remote.is_some()
&& !self
.pull_request_filter
.matches(self.pull_request_review_state(entry)))
{
continue;
}
@@ -1359,7 +1401,24 @@ impl DiffState {
self.search_matches.push((score, index));
}
}
self.search_matches.sort_unstable();
let priority = |index: usize| {
if self.pull_request_priority == PullRequestReviewScope::All {
return false;
}
let RevisionTarget::PullRequest { number, .. } = catalog.entries[index].target
else {
return false;
};
!self
.pull_request_priority
.matches(self.pull_request_order[&number].2)
};
if self.pull_request_priority != PullRequestReviewScope::All {
self.visible
.sort_unstable_by_key(|&index| (priority(index), index));
}
self.search_matches
.sort_unstable_by_key(|&(score, index)| (priority(index), score, index));
self.visible
.extend(self.search_matches.iter().map(|(_, index)| *index));
}
@@ -1447,6 +1506,22 @@ impl DiffState {
self.choose()
}
Input::Ctrl('r') if self.pull_request_remote.is_some() => self.retry_pull_requests(),
Input::Ctrl('f' | 's') if self.pull_request_remote.is_some() => {
let selected = self
.entries()
.nth(self.selected)
.map(|entry| entry.target.clone());
let scope = if input == Input::Ctrl('f') {
&mut self.pull_request_filter
} else {
&mut self.pull_request_priority
};
*scope = scope.next();
self.refilter();
if let Some(target) = selected {
self.select_target(&target);
}
}
input if BackspaceKind::from_input(&input).is_some() => {
if BackspaceKind::from_input(&input)
.expect("guard checked backspace input")
+1
View File
@@ -108,6 +108,7 @@ Local reviews: :diff.comment anchors exact Visual text, Visual Line rows, or the
Pull requests: :pull_requests [REMOTE]; GitHub/GitLab/Gitea Open/Closed/Drafts/All tabs, type to search
Enter opens the immutable PR head against its merge base; API auth uses provider tokens or gh/glab
🙏 marks a personal review request; 👍 marks your active approval (API account)
Ctrl+f filters All/Requested/Approved; Ctrl+s sorts Newest/Requested first/Approved first
Symbols: config.pull_request_review_requested_symbol / config.pull_request_approved_symbol
Resolve jj conflicts: :resolve opens the current working-copy conflicts in a dedicated workspace
Editable marker-free Draft vs readonly Alternative; :resolve.history shows inputs/base
+17 -6
View File
@@ -3397,6 +3397,7 @@ fn draw_diff(
.and_then(|catalog| catalog.notice.as_deref())
}
});
let stacked_review_controls = pull_requests && content.width < 68 && content.height >= 8;
let [
stage_area,
tabs_area,
@@ -3406,7 +3407,11 @@ fn draw_diff(
results,
help,
] = Layout::vertical([
Constraint::Length(u16::from(content.height >= 5)),
Constraint::Length(if stacked_review_controls {
2
} else {
u16::from(content.height >= 5)
}),
Constraint::Length(u16::from(content.height >= 4)),
Constraint::Length(1),
Constraint::Length(u16::from(notice.is_some() && content.height >= 3)),
@@ -3420,11 +3425,15 @@ fn draw_diff(
.as_ref()
.map_or("not selected", |revision| revision.label.as_str());
let stage_text = if pull_requests {
if diff.pull_request_partial() {
"Partial search · Select a loaded pull request".into()
} else {
"Select a pull request to compare its branch changes".into()
}
let sort = match diff.pull_request_priority {
crate::diff::PullRequestReviewScope::All => "Newest".into(),
scope => format!("{} first", scope.label()),
};
format!(
"Review: {} · Ctrl+f{}Sort: {sort} · Ctrl+s",
diff.pull_request_filter.label(),
if stacked_review_controls { "\n" } else { " " },
)
} else {
format!(
"{} · Ctrl+b toggle · Before: {}",
@@ -3522,6 +3531,8 @@ fn draw_diff(
diff.category,
&diff.query,
diff.comparison_mode as u8,
diff.pull_request_filter,
diff.pull_request_priority,
diff.catalog
.as_ref()
.map(|catalog| (catalog.entries.as_ptr(), catalog.entries.len())),