grite 0.3.0

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)
        .map_err(|e| GriteError::Internal(e.to_string()))?;

    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)
        .map_err(|e| GriteError::Internal(e.to_string()))?;

    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)
        .map_err(|e| GriteError::Internal(e.to_string()))?;

    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)
        .map_err(|e| GriteError::Internal(e.to_string()))?;

    let locks = manager.list_locks()
        .map_err(|e| GriteError::Internal(e.to_string()))?;

    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)
        .map_err(|e| GriteError::Internal(e.to_string()))?;

    let stats = manager.gc()
        .map_err(|e| GriteError::Internal(e.to_string()))?;

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

    Ok(())
}