grite 0.5.3

Git-backed issue tracker with CRDT merging, designed for AI coding agents
//! Lock management commands

use libgrite_core::GriteError;
use libgrite_git::LockManager;
use serde::Serialize;

use crate::cli::{Cli, LockCommand};
use crate::context::GriteContext;
use crate::output::output_success;

#[derive(Serialize)]
struct LockAcquireOutput {
    resource: String,
    owner: String,
    nonce: String,
    expires_unix_ms: u64,
    ttl_seconds: u64,
}

#[derive(Serialize)]
struct LockReleaseOutput {
    resource: String,
    released: bool,
}

#[derive(Serialize)]
struct LockRenewOutput {
    resource: String,
    owner: String,
    expires_unix_ms: u64,
    ttl_seconds: u64,
}

#[derive(Serialize)]
struct LockStatusOutput {
    locks: Vec<LockInfo>,
    total: usize,
}

#[derive(Serialize)]
struct LockInfo {
    resource: String,
    owner: String,
    expires_unix_ms: u64,
    time_remaining_seconds: u64,
    expired: bool,
}

#[derive(Serialize)]
struct LockGcOutput {
    removed: usize,
    kept: usize,
}

pub fn run(cli: &Cli, cmd: LockCommand) -> Result<(), GriteError> {
    match cmd {
        LockCommand::Acquire { resource, ttl } => run_acquire(cli, resource, ttl),
        LockCommand::Release { resource } => run_release(cli, resource),
        LockCommand::Renew { resource, ttl } => run_renew(cli, resource, ttl),
        LockCommand::Status => run_status(cli),
        LockCommand::Gc => run_gc(cli),
    }
}

fn run_acquire(cli: &Cli, resource: String, ttl_seconds: u64) -> Result<(), GriteError> {
    let ctx = GriteContext::resolve(cli)?;
    let git_dir = ctx.repo_root().join(".git");
    let manager = LockManager::open(&git_dir)?;

    let ttl_ms = ttl_seconds * 1000;
    let lock = manager
        .acquire(&resource, &ctx.actor_id, Some(ttl_ms))
        .map_err(|e| match e {
            libgrite_git::GitError::LockConflict {
                resource,
                owner,
                expires_in_ms,
            } => GriteError::Conflict(format!(
                "Lock on {} is held by {} (expires in {}s)",
                resource,
                owner,
                expires_in_ms / 1000
            )),
            _ => GriteError::Internal(e.to_string()),
        })?;

    output_success(
        cli,
        LockAcquireOutput {
            resource: lock.resource,
            owner: lock.owner,
            nonce: lock.nonce,
            expires_unix_ms: lock.expires_unix_ms,
            ttl_seconds,
        },
    );

    Ok(())
}

fn run_release(cli: &Cli, resource: String) -> Result<(), GriteError> {
    let ctx = GriteContext::resolve(cli)?;
    let git_dir = ctx.repo_root().join(".git");
    let manager = LockManager::open(&git_dir)?;

    manager
        .release(&resource, &ctx.actor_id)
        .map_err(|e| match e {
            libgrite_git::GitError::LockNotOwned { resource, owner } => GriteError::Conflict(
                format!("Cannot release lock on {} - owned by {}", resource, owner),
            ),
            _ => GriteError::Internal(e.to_string()),
        })?;

    output_success(
        cli,
        LockReleaseOutput {
            resource,
            released: true,
        },
    );

    Ok(())
}

fn run_renew(cli: &Cli, resource: String, ttl_seconds: u64) -> Result<(), GriteError> {
    let ctx = GriteContext::resolve(cli)?;
    let git_dir = ctx.repo_root().join(".git");
    let manager = LockManager::open(&git_dir)?;

    let ttl_ms = ttl_seconds * 1000;
    let lock = manager
        .renew(&resource, &ctx.actor_id, Some(ttl_ms))
        .map_err(|e| match e {
            libgrite_git::GitError::LockNotOwned { resource, owner } => GriteError::Conflict(
                format!("Cannot renew lock on {} - owned by {}", resource, owner),
            ),
            _ => GriteError::Internal(e.to_string()),
        })?;

    output_success(
        cli,
        LockRenewOutput {
            resource: lock.resource,
            owner: lock.owner,
            expires_unix_ms: lock.expires_unix_ms,
            ttl_seconds,
        },
    );

    Ok(())
}

fn run_status(cli: &Cli) -> Result<(), GriteError> {
    let ctx = GriteContext::resolve(cli)?;
    let git_dir = ctx.repo_root().join(".git");
    let manager = LockManager::open(&git_dir)?;

    let locks = manager.list_locks()?;

    let lock_infos: Vec<LockInfo> = locks
        .iter()
        .map(|lock| LockInfo {
            resource: lock.resource.clone(),
            owner: lock.owner.clone(),
            expires_unix_ms: lock.expires_unix_ms,
            time_remaining_seconds: lock.time_remaining_ms() / 1000,
            expired: lock.is_expired(),
        })
        .collect();

    let total = lock_infos.len();

    output_success(
        cli,
        LockStatusOutput {
            locks: lock_infos,
            total,
        },
    );

    Ok(())
}

fn run_gc(cli: &Cli) -> Result<(), GriteError> {
    let ctx = GriteContext::resolve(cli)?;
    let git_dir = ctx.repo_root().join(".git");
    let manager = LockManager::open(&git_dir)?;

    let stats = manager.gc()?;

    output_success(
        cli,
        LockGcOutput {
            removed: stats.removed,
            kept: stats.kept,
        },
    );

    Ok(())
}