shipflow 0.1.4

A minimalist, git-aware CLI for tracking what you ship
Documentation
use std::io::IsTerminal;

use crate::error::{Result, ShipflowError};
use crate::git::{self, CommitSummary};
use crate::storage::{TaskStore, resolve_storage};
use crate::task::LinkedCommit;
use crate::utils::{self, is_color_enabled, read_line, with_color};

pub fn run(query: String, commit: Option<String>, no_link: bool, global: bool) -> Result<()> {
    let cwd = std::env::current_dir()?;
    let ctx = resolve_storage(global, &cwd)?;
    let git_ctx = git::detect(&cwd);
    let mut store = TaskStore::open(&ctx)?;

    let linked = resolve_linked_commit(&git_ctx, commit, no_link)?;

    let done = store.mark_done(&query, linked)?;
    let color = with_color(is_color_enabled());

    let mut msg = format!(
        "Done {}{}",
        color.id(&done.id[..8.min(done.id.len())]),
        color.highlight(&done.title)
    );

    if let Some(c) = &done.linked_commit {
        msg.push_str(&format!("\n  linked: {} {}", c.sha, c.message));
    }

    utils::print_success(&msg)?;
    Ok(())
}

fn resolve_linked_commit(
    git_ctx: &git::GitContext,
    commit_flag: Option<String>,
    no_link: bool,
) -> Result<Option<LinkedCommit>> {
    if no_link {
        return Ok(None);
    }

    let Some(repo_root) = git_ctx.repo_root.as_ref() else {
        return Ok(None);
    };

    if let Some(sha) = commit_flag {
        if !git::validate_commit(repo_root, &sha)? {
            return Err(ShipflowError::InvalidCommit(sha));
        }
        return git::linked_commit_from_sha(repo_root, &sha, git_ctx.branch.as_deref());
    }

    if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
        return Ok(None);
    }

    let commits = git::recent_commits(repo_root, 5)?;
    if commits.is_empty() {
        return Ok(None);
    }

    prompt_commit_selection(repo_root, &commits, git_ctx.branch.as_deref())
}

fn prompt_commit_selection(
    repo_root: &std::path::Path,
    commits: &[CommitSummary],
    branch: Option<&str>,
) -> Result<Option<LinkedCommit>> {
    let color = with_color(is_color_enabled());
    println!("{}", color.info("Recent commits:"));
    println!("{}", color.dim("  0) Skip"));

    for (i, c) in commits.iter().enumerate() {
        println!("  {}) {} {}", i + 1, c.sha, c.message);
    }

    let input = read_line("Link commit [0-5]: ")?;
    if input.is_empty() || input == "0" {
        return Ok(None);
    }

    let choice: usize = input
        .parse()
        .map_err(|_| ShipflowError::InvalidCommit(input.clone()))?;

    if choice == 0 || choice > commits.len() {
        return Ok(None);
    }

    let selected = &commits[choice - 1];
    git::linked_commit_from_sha(repo_root, &selected.sha, branch)
}