469b5a8f
Consolidate list filter/sort/paginate into generic helper
a73x 2026-03-22 08:27
Add Listable trait and filter_sort_paginate<T> in cli.rs, implement for IssueState and PatchState, replace 4 duplicated filter+sort+paginate blocks in issue.rs and patch.rs. Closes issue 94d57709. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/src/cli.rs b/src/cli.rs index 86fe1fc..5063e3a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -12,6 +12,38 @@ pub enum SortMode { Alpha, } /// Trait for items that support list filtering, sorting, and pagination. pub trait Listable { fn is_open(&self) -> bool; fn last_updated(&self) -> &str; fn created_at(&self) -> &str; fn title(&self) -> &str; } /// Filter by open status, sort by mode, then apply offset/limit pagination. pub fn filter_sort_paginate<T: Listable>( items: Vec<T>, show_closed: bool, sort: SortMode, offset: Option<usize>, limit: Option<usize>, ) -> Vec<T> { let mut filtered: Vec<T> = items .into_iter() .filter(|item| show_closed || item.is_open()) .collect(); match sort { SortMode::Recent => filtered.sort_by(|a, b| b.last_updated().cmp(a.last_updated())), SortMode::Created => filtered.sort_by(|a, b| b.created_at().cmp(a.created_at())), SortMode::Alpha => filtered.sort_by(|a, b| a.title().cmp(b.title())), } filtered .into_iter() .skip(offset.unwrap_or(0)) .take(limit.unwrap_or(usize::MAX)) .collect() } #[derive(Parser)] #[command( name = "git-collab", diff --git a/src/issue.rs b/src/issue.rs index 05958ab..fbc2b3b 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -1,11 +1,11 @@ use git2::Repository; use crate::cli::SortMode; use crate::cli::{self, SortMode}; use crate::dag; use crate::event::{Action, Event}; use crate::identity::get_author; use crate::signing; use crate::state::{self, IssueState, IssueStatus}; use crate::state::{self, IssueState}; pub fn open( repo: &Repository, @@ -50,24 +50,14 @@ pub fn list( } else { state::list_issues(repo)? }; let mut entries: Vec<_> = issues let filtered = cli::filter_sort_paginate(issues, show_closed, sort, offset, limit); let entries = filtered .into_iter() .filter(|i| show_closed || i.status == IssueStatus::Open) .map(|issue| { let unread = count_unread(repo, &issue.id); ListEntry { issue, unread } }) .collect(); match sort { SortMode::Recent => entries.sort_by(|a, b| b.issue.last_updated.cmp(&a.issue.last_updated)), SortMode::Created => entries.sort_by(|a, b| b.issue.created_at.cmp(&a.issue.created_at)), SortMode::Alpha => entries.sort_by(|a, b| a.issue.title.cmp(&b.issue.title)), } let entries = entries .into_iter() .skip(offset.unwrap_or(0)) .take(limit.unwrap_or(usize::MAX)) .collect(); Ok(entries) } @@ -133,15 +123,7 @@ pub fn list_json(repo: &Repository, show_closed: bool, show_archived: bool, sort } else { state::list_issues(repo)? }; let mut filtered: Vec<_> = issues .into_iter() .filter(|i| show_closed || i.status == IssueStatus::Open) .collect(); match sort { SortMode::Recent => filtered.sort_by(|a, b| b.last_updated.cmp(&a.last_updated)), SortMode::Created => filtered.sort_by(|a, b| b.created_at.cmp(&a.created_at)), SortMode::Alpha => filtered.sort_by(|a, b| a.title.cmp(&b.title)), } let filtered = cli::filter_sort_paginate(issues, show_closed, sort, None, None); Ok(serde_json::to_string_pretty(&filtered)?) } diff --git a/src/patch.rs b/src/patch.rs index a97ad33..d5bbf65 100644 --- a/src/patch.rs +++ b/src/patch.rs @@ -1,6 +1,6 @@ use git2::{DiffFormat, Oid, Repository}; use crate::cli::SortMode; use crate::cli::{self, SortMode}; use crate::dag; use crate::error::Error; use crate::event::{Action, Event, ReviewVerdict}; @@ -141,21 +141,7 @@ pub fn list( } else { state::list_patches(repo)? }; let mut filtered: Vec<_> = patches .into_iter() .filter(|p| show_closed || p.status == PatchStatus::Open) .collect(); match sort { SortMode::Recent => filtered.sort_by(|a, b| b.last_updated.cmp(&a.last_updated)), SortMode::Created => filtered.sort_by(|a, b| b.created_at.cmp(&a.created_at)), SortMode::Alpha => filtered.sort_by(|a, b| a.title.cmp(&b.title)), } let filtered = filtered .into_iter() .skip(offset.unwrap_or(0)) .take(limit.unwrap_or(usize::MAX)) .collect(); Ok(filtered) Ok(cli::filter_sort_paginate(patches, show_closed, sort, offset, limit)) } pub fn list_to_writer( @@ -189,15 +175,7 @@ pub fn list_json(repo: &Repository, show_closed: bool, show_archived: bool, sort } else { state::list_patches(repo)? }; let mut filtered: Vec<_> = patches .into_iter() .filter(|p| show_closed || p.status == PatchStatus::Open) .collect(); match sort { SortMode::Recent => filtered.sort_by(|a, b| b.last_updated.cmp(&a.last_updated)), SortMode::Created => filtered.sort_by(|a, b| b.created_at.cmp(&a.created_at)), SortMode::Alpha => filtered.sort_by(|a, b| a.title.cmp(&b.title)), } let filtered = cli::filter_sort_paginate(patches, show_closed, sort, None, None); Ok(serde_json::to_string_pretty(&filtered)?) } diff --git a/src/state.rs b/src/state.rs index f59f179..6218375 100644 --- a/src/state.rs +++ b/src/state.rs @@ -185,6 +185,36 @@ pub struct PatchState { pub author: Author, } impl crate::cli::Listable for IssueState { fn is_open(&self) -> bool { self.status == IssueStatus::Open } fn last_updated(&self) -> &str { &self.last_updated } fn created_at(&self) -> &str { &self.created_at } fn title(&self) -> &str { &self.title } } impl crate::cli::Listable for PatchState { fn is_open(&self) -> bool { self.status == PatchStatus::Open } fn last_updated(&self) -> &str { &self.last_updated } fn created_at(&self) -> &str { &self.created_at } fn title(&self) -> &str { &self.title } } impl IssueState { pub fn from_ref( repo: &Repository,