src/issue.rs
Ref: Size: 7.1 KiB
use git2::Repository;
use crate::cli::{self, SortMode};
use crate::dag;
use crate::event::Action;
use crate::state::{self, IssueState};
pub fn open(
repo: &Repository,
title: &str,
body: &str,
relates_to: Option<&str>,
) -> Result<String, crate::error::Error> {
let oid = dag::create_root_action(
repo,
Action::IssueOpen {
title: title.to_string(),
body: body.to_string(),
relates_to: relates_to.map(|s| s.to_string()),
},
)?;
let id = oid.to_string();
let ref_name = format!("refs/collab/issues/{}", id);
repo.reference(&ref_name, oid, false, "issue open")?;
Ok(id)
}
pub struct ListEntry {
pub issue: IssueState,
pub unread: Option<usize>,
}
fn load_issues(repo: &Repository, show_archived: bool) -> Result<Vec<state::IssueState>, crate::error::Error> {
if show_archived {
state::list_issues_with_archived(repo)
} else {
state::list_issues(repo)
}
}
pub fn list(
repo: &Repository,
show_closed: bool,
show_archived: bool,
limit: Option<usize>,
offset: Option<usize>,
sort: SortMode,
) -> Result<Vec<ListEntry>, crate::error::Error> {
let issues = load_issues(repo, show_archived)?;
let filtered = cli::filter_sort_paginate(issues, show_closed, sort, offset, limit);
let entries = filtered
.into_iter()
.map(|issue| {
let unread = count_unread(repo, &issue.id);
ListEntry { issue, unread }
})
.collect();
Ok(entries)
}
pub fn list_to_writer(
repo: &Repository,
show_closed: bool,
show_archived: bool,
limit: Option<usize>,
offset: Option<usize>,
sort: SortMode,
writer: &mut dyn std::io::Write,
) -> Result<(), crate::error::Error> {
let entries = list(repo, show_closed, show_archived, limit, offset, sort)?;
if entries.is_empty() {
writeln!(writer, "No issues found.").ok();
return Ok(());
}
for e in &entries {
let i = &e.issue;
let status = i.status.as_str();
let labels = if i.labels.is_empty() {
String::new()
} else {
format!(" [{}]", i.labels.join(", "))
};
let unread = match e.unread {
Some(n) if n > 0 => format!(" ({} new)", n),
_ => String::new(),
};
writeln!(
writer,
"{:.8} {:6} {}{} (by {}){}",
i.id, status, i.title, labels, i.author.name, unread
)
.ok();
}
Ok(())
}
/// Count events after the last-seen mark. Returns None if never viewed.
fn count_unread(repo: &git2::Repository, id: &str) -> Option<usize> {
let seen_ref = format!("refs/collab/local/seen/issues/{}", id);
let seen_oid = repo.refname_to_id(&seen_ref).ok()?;
let ref_name = format!("refs/collab/issues/{}", id);
let tip = repo.refname_to_id(&ref_name).ok()?;
if seen_oid == tip {
return Some(0);
}
let mut revwalk = repo.revwalk().ok()?;
revwalk
.set_sorting(git2::Sort::TOPOLOGICAL)
.ok()?;
revwalk.push(tip).ok()?;
revwalk.hide(seen_oid).ok()?;
Some(revwalk.count())
}
pub fn list_json(repo: &Repository, show_closed: bool, show_archived: bool, sort: SortMode) -> Result<String, crate::error::Error> {
let issues = load_issues(repo, show_archived)?;
let filtered = cli::filter_sort_paginate(issues, show_closed, sort, None, None);
Ok(serde_json::to_string_pretty(&filtered)?)
}
pub fn show_json(repo: &Repository, id_prefix: &str) -> Result<String, crate::error::Error> {
let (ref_name, id) = state::resolve_issue_ref(repo, id_prefix)?;
let issue = IssueState::from_ref(repo, &ref_name, &id)?;
Ok(serde_json::to_string_pretty(&issue)?)
}
pub fn show(repo: &Repository, id_prefix: &str) -> Result<IssueState, crate::error::Error> {
let (ref_name, id) = state::resolve_issue_ref(repo, id_prefix)?;
let issue = IssueState::from_ref(repo, &ref_name, &id)?;
// Mark as read: store current tip as seen
let tip = repo.refname_to_id(&ref_name)?;
let seen_ref = format!("refs/collab/local/seen/issues/{}", id);
repo.reference(&seen_ref, tip, true, "mark seen")?;
Ok(issue)
}
pub fn label(repo: &Repository, id_prefix: &str, label: &str) -> Result<(), crate::error::Error> {
let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
dag::append_action(repo, &ref_name, Action::IssueLabel {
label: label.to_string(),
})?;
Ok(())
}
pub fn unlabel(repo: &Repository, id_prefix: &str, label: &str) -> Result<(), crate::error::Error> {
let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
dag::append_action(repo, &ref_name, Action::IssueUnlabel {
label: label.to_string(),
})?;
Ok(())
}
pub fn assign(
repo: &Repository,
id_prefix: &str,
assignee: &str,
) -> Result<(), crate::error::Error> {
let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
dag::append_action(repo, &ref_name, Action::IssueAssign {
assignee: assignee.to_string(),
})?;
Ok(())
}
pub fn unassign(
repo: &Repository,
id_prefix: &str,
assignee: &str,
) -> Result<(), crate::error::Error> {
let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
dag::append_action(repo, &ref_name, Action::IssueUnassign {
assignee: assignee.to_string(),
})?;
Ok(())
}
pub fn edit(
repo: &Repository,
id_prefix: &str,
title: Option<&str>,
body: Option<&str>,
) -> Result<(), crate::error::Error> {
if title.is_none() && body.is_none() {
return Err(
git2::Error::from_str("at least one of --title or --body must be provided").into(),
);
}
let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
dag::append_action(repo, &ref_name, Action::IssueEdit {
title: title.map(|s| s.to_string()),
body: body.map(|s| s.to_string()),
})?;
Ok(())
}
pub fn comment(repo: &Repository, id_prefix: &str, body: &str) -> Result<(), crate::error::Error> {
let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
dag::append_action(repo, &ref_name, Action::IssueComment {
body: body.to_string(),
})?;
Ok(())
}
pub fn close(
repo: &Repository,
id_prefix: &str,
reason: Option<&str>,
) -> Result<(), crate::error::Error> {
let (ref_name, id) = state::resolve_issue_ref(repo, id_prefix)?;
dag::append_action(repo, &ref_name, Action::IssueClose {
reason: reason.map(|s| s.to_string()),
})?;
// Archive the ref (move to refs/collab/archive/issues/)
if ref_name.starts_with("refs/collab/issues/") {
state::archive_issue_ref(repo, &id)?;
}
Ok(())
}
pub fn delete(repo: &Repository, id_prefix: &str) -> Result<String, crate::error::Error> {
let (ref_name, id) = state::resolve_issue_ref(repo, id_prefix)?;
repo.find_reference(&ref_name)?.delete()?;
Ok(id)
}
pub fn reopen(repo: &Repository, id_prefix: &str) -> Result<(), crate::error::Error> {
let (ref_name, _id) = state::resolve_issue_ref(repo, id_prefix)?;
dag::append_action(repo, &ref_name, Action::IssueReopen)?;
Ok(())
}