a73x

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,