use crate::cli::{
AssigneeCommand, AttachmentCommand, Cli, IssueCommand, LabelCommand, LinkCommand,
};
use crate::context::GriteContext;
use crate::event_helper::insert_and_append;
use crate::output::{format_issue_table, output_success, IssueRow};
use libgrite_core::{
hash::compute_event_id,
lock::LockCheckResult,
store::IssueFilter,
types::event::{Event, EventKind, IssueState},
types::ids::{generate_issue_id, hex_to_id, id_to_hex},
types::issue::IssueSummary,
GriteError,
};
use libgrite_git;
use serde::Serialize;
fn check_issue_lock(cli: &Cli, ctx: &GriteContext, issue_id_hex: &str) -> Result<(), GriteError> {
let resource = format!("issue:{}", issue_id_hex);
match ctx.check_lock(&resource)? {
LockCheckResult::Clear => Ok(()),
LockCheckResult::Warning(conflicts) => {
if !cli.quiet {
for lock in &conflicts {
eprintln!(
"Warning: {} is locked by {} (expires in {}s)",
lock.resource,
lock.owner,
lock.time_remaining_ms() / 1000
);
}
}
Ok(())
}
LockCheckResult::Blocked(_) => Err(GriteError::Conflict(
"Repository is locked by another process".to_string(),
)),
}
}
fn check_repo_lock(cli: &Cli, ctx: &GriteContext) -> Result<(), GriteError> {
match ctx.check_lock("repo:global")? {
LockCheckResult::Clear => Ok(()),
LockCheckResult::Warning(conflicts) => {
if !cli.quiet {
for lock in &conflicts {
eprintln!(
"Warning: {} is locked by {} (expires in {}s)",
lock.resource,
lock.owner,
lock.time_remaining_ms() / 1000
);
}
}
Ok(())
}
LockCheckResult::Blocked(_) => Err(GriteError::Conflict(
"Repository is locked by another process".to_string(),
)),
}
}
struct LockGuard<'a> {
ctx: &'a GriteContext,
resource: String,
acquired: bool,
}
impl<'a> LockGuard<'a> {
fn acquire(
ctx: &'a GriteContext,
issue_id_hex: &str,
should_lock: bool,
) -> Result<Self, GriteError> {
let resource = format!("issue:{}", issue_id_hex);
if should_lock {
let lock_manager = ctx.open_lock_manager()?;
lock_manager
.acquire(&resource, &ctx.actor_id, None)
.map_err(|e| match e {
libgrite_git::GitError::LockConflict {
resource,
owner,
expires_in_ms,
} => GriteError::Conflict(format!(
"Cannot acquire lock on {} - held by {} (expires in {}s)",
resource,
owner,
expires_in_ms / 1000
)),
_ => GriteError::Internal(e.to_string()),
})?;
Ok(Self {
ctx,
resource,
acquired: true,
})
} else {
Ok(Self {
ctx,
resource,
acquired: false,
})
}
}
}
impl<'a> Drop for LockGuard<'a> {
fn drop(&mut self) {
if self.acquired {
if let Ok(lock_manager) = self.ctx.open_lock_manager() {
let _ = lock_manager.release(&self.resource, &self.ctx.actor_id);
}
}
}
}
#[derive(Serialize)]
struct IssueCreateOutput {
issue_id: String,
event_id: String,
wal_head: Option<String>,
}
#[derive(Serialize)]
struct IssueListOutput {
issues: Vec<IssueSummaryJson>,
total: usize,
}
#[derive(Serialize)]
struct IssueSummaryJson {
issue_id: String,
title: String,
state: String,
labels: Vec<String>,
assignees: Vec<String>,
created_ts: u64,
updated_ts: u64,
comment_count: usize,
}
impl From<&IssueSummary> for IssueSummaryJson {
fn from(s: &IssueSummary) -> Self {
Self {
issue_id: id_to_hex(&s.issue_id),
title: s.title.clone(),
state: format!("{:?}", s.state).to_lowercase(),
labels: s.labels.clone(),
assignees: s.assignees.clone(),
created_ts: s.created_ts,
updated_ts: s.updated_ts,
comment_count: s.comment_count,
}
}
}
#[derive(Serialize)]
struct IssueShowOutput {
issue: IssueSummaryJson,
events: Vec<EventJson>,
}
#[derive(Serialize)]
struct EventJson {
event_id: String,
issue_id: String,
actor: String,
ts_unix_ms: u64,
parent: Option<String>,
kind: serde_json::Value,
}
#[derive(Serialize)]
struct IssueUpdateOutput {
issue_id: String,
event_id: String,
wal_head: Option<String>,
}
#[derive(Serialize)]
struct IssueStateOutput {
issue_id: String,
event_id: String,
state: String,
wal_head: Option<String>,
}
pub fn run(cli: &Cli, cmd: IssueCommand) -> Result<(), GriteError> {
match cmd {
IssueCommand::Create { title, body, label } => run_create(cli, title, body, label),
IssueCommand::List { state, label } => run_list(cli, state, label),
IssueCommand::Show { id } => run_show(cli, id),
IssueCommand::Update {
id,
title,
body,
lock,
} => run_update(cli, id, title, body, lock),
IssueCommand::Comment { id, body, lock } => run_comment(cli, id, body, lock),
IssueCommand::Close { id, lock } => run_close(cli, id, lock),
IssueCommand::Reopen { id, lock } => run_reopen(cli, id, lock),
IssueCommand::Label { cmd } => run_label(cli, cmd),
IssueCommand::Assignee { cmd } => run_assignee(cli, cmd),
IssueCommand::Link { cmd } => run_link(cli, cmd),
IssueCommand::Attachment { cmd } => run_attachment(cli, cmd),
IssueCommand::Dep { cmd } => super::dep::run(cli, cmd),
}
}
fn current_ts() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
fn run_create(
cli: &Cli,
title: String,
body: String,
labels: Vec<String>,
) -> Result<(), GriteError> {
let ctx = GriteContext::resolve(cli)?;
check_repo_lock(cli, &ctx)?;
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = generate_issue_id();
let ts = current_ts();
let kind = EventKind::IssueCreated {
title,
body,
labels,
};
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueCreateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
wal_head: result.wal_head,
},
);
Ok(())
}
fn run_list(cli: &Cli, state: Option<String>, label: Option<String>) -> Result<(), GriteError> {
let ctx = GriteContext::resolve(cli)?;
let store = ctx.open_store()?;
let state_filter = state.map(|s| match s.to_lowercase().as_str() {
"open" => IssueState::Open,
"closed" => IssueState::Closed,
_ => IssueState::Open,
});
let filter = IssueFilter {
state: state_filter,
label,
};
let issues = store.list_issues(&filter)?;
let total = issues.len();
let issue_jsons: Vec<IssueSummaryJson> = issues.iter().map(IssueSummaryJson::from).collect();
if cli.json {
output_success(
cli,
IssueListOutput {
issues: issue_jsons,
total,
},
);
} else if !cli.quiet {
let rows: Vec<IssueRow> = issues
.iter()
.map(|i| IssueRow {
id: id_to_hex(&i.issue_id),
state: format!("{:?}", i.state).to_lowercase(),
title: i.title.clone(),
created_ts: i.created_ts,
})
.collect();
println!("{}", format_issue_table(&rows));
}
Ok(())
}
fn run_show(cli: &Cli, id: String) -> Result<(), GriteError> {
let ctx = GriteContext::resolve(cli)?;
let store = ctx.open_store()?;
let issue_id = store.resolve_issue_id(&id)?;
let proj = store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let events = store.get_issue_events(&issue_id)?;
let event_jsons: Vec<EventJson> = events
.iter()
.map(|e| EventJson {
event_id: id_to_hex(&e.event_id),
issue_id: id_to_hex(&e.issue_id),
actor: id_to_hex(&e.actor),
ts_unix_ms: e.ts_unix_ms,
parent: e.parent.as_ref().map(id_to_hex),
kind: serde_json::to_value(&e.kind).unwrap_or(serde_json::Value::Null),
})
.collect();
let summary = IssueSummary::from(&proj);
output_success(
cli,
IssueShowOutput {
issue: IssueSummaryJson::from(&summary),
events: event_jsons,
},
);
Ok(())
}
fn run_update(
cli: &Cli,
id: String,
title: Option<String>,
body: Option<String>,
lock: bool,
) -> Result<(), GriteError> {
if title.is_none() && body.is_none() {
return Err(GriteError::InvalidArgs(
"At least one of --title or --body must be provided".to_string(),
));
}
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let ts = current_ts();
let kind = EventKind::IssueUpdated { title, body };
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueUpdateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
wal_head: result.wal_head,
},
);
Ok(())
}
fn run_comment(cli: &Cli, id: String, body: String, lock: bool) -> Result<(), GriteError> {
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let ts = current_ts();
let kind = EventKind::CommentAdded { body };
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueUpdateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
wal_head: result.wal_head,
},
);
Ok(())
}
fn run_close(cli: &Cli, id: String, lock: bool) -> Result<(), GriteError> {
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let ts = current_ts();
let kind = EventKind::StateChanged {
state: IssueState::Closed,
};
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueStateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
state: "closed".to_string(),
wal_head: result.wal_head,
},
);
Ok(())
}
fn run_reopen(cli: &Cli, id: String, lock: bool) -> Result<(), GriteError> {
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let ts = current_ts();
let kind = EventKind::StateChanged {
state: IssueState::Open,
};
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueStateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
state: "open".to_string(),
wal_head: result.wal_head,
},
);
Ok(())
}
fn run_label(cli: &Cli, cmd: LabelCommand) -> Result<(), GriteError> {
match cmd {
LabelCommand::Add { id, label, lock } => {
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let ts = current_ts();
let kind = EventKind::LabelAdded { label };
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueUpdateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
wal_head: result.wal_head,
},
);
}
LabelCommand::Remove { id, label, lock } => {
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let ts = current_ts();
let kind = EventKind::LabelRemoved { label };
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueUpdateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
wal_head: result.wal_head,
},
);
}
}
Ok(())
}
fn run_assignee(cli: &Cli, cmd: AssigneeCommand) -> Result<(), GriteError> {
match cmd {
AssigneeCommand::Add { id, user, lock } => {
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let ts = current_ts();
let kind = EventKind::AssigneeAdded { user };
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueUpdateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
wal_head: result.wal_head,
},
);
}
AssigneeCommand::Remove { id, user, lock } => {
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let ts = current_ts();
let kind = EventKind::AssigneeRemoved { user };
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueUpdateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
wal_head: result.wal_head,
},
);
}
}
Ok(())
}
fn run_link(cli: &Cli, cmd: LinkCommand) -> Result<(), GriteError> {
match cmd {
LinkCommand::Add {
id,
url,
note,
lock,
} => {
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let ts = current_ts();
let kind = EventKind::LinkAdded { url, note };
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueUpdateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
wal_head: result.wal_head,
},
);
}
}
Ok(())
}
fn run_attachment(cli: &Cli, cmd: AttachmentCommand) -> Result<(), GriteError> {
match cmd {
AttachmentCommand::Add {
id,
name,
sha256,
mime,
lock,
} => {
let ctx = GriteContext::resolve(cli)?;
let _lock_guard = LockGuard::acquire(&ctx, &id, lock)?;
if !lock {
check_issue_lock(cli, &ctx, &id)?;
}
let store = ctx.open_store()?;
let wal = ctx.open_wal()?;
let actor = ctx.actor_config.actor_id_bytes()?;
let issue_id = store.resolve_issue_id(&id)?;
store
.get_issue(&issue_id)?
.ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", id)))?;
let sha256_bytes: [u8; 32] = hex_to_id(&sha256)?;
let ts = current_ts();
let kind = EventKind::AttachmentAdded {
name,
sha256: sha256_bytes,
mime,
};
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
let event = ctx.sign_event(event);
let result = insert_and_append(&store, &wal, &actor, &event)?;
output_success(
cli,
IssueUpdateOutput {
issue_id: id_to_hex(&issue_id),
event_id: id_to_hex(&event_id),
wal_head: result.wal_head,
},
);
}
}
Ok(())
}