todo-tree 0.5.2

A CLI tool to find and display TODO-style comments in your codebase
Documentation
use crate::cli::{WorkflowArgs, WorkflowCommands, WorkflowInitArgs};
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};

const DEFAULT_WORKFLOW_PATH: &str = ".github/workflows/todo-tree.yml";
const ACTION_VERSION: &str = "v1.0.3";

pub fn run(args: WorkflowArgs) -> Result<()> {
    match args.command {
        WorkflowCommands::Init(args) => init(args),
    }
}

fn init(args: WorkflowInitArgs) -> Result<()> {
    let action = args.action.unwrap_or_else(default_action_ref);
    let path = args
        .path
        .unwrap_or_else(|| PathBuf::from(DEFAULT_WORKFLOW_PATH));

    validate_action_ref(&action)?;
    write_workflow_template(&path, args.force, &action)?;

    println!("Created workflow file: {}", path.display());
    println!("The workflow will run on pull requests using {action}.");

    Ok(())
}

fn default_action_ref() -> String {
    format!("alexandretrotel/todo-tree-action@{ACTION_VERSION}")
}

fn validate_action_ref(action: &str) -> Result<()> {
    let Some((repo, reference)) = action.split_once('@') else {
        anyhow::bail!(
            "Invalid action reference {:?}. Expected format: owner/repo@ref",
            action
        );
    };

    let mut repo_parts = repo.split('/');
    let owner = repo_parts.next().unwrap_or_default();
    let name = repo_parts.next().unwrap_or_default();

    if owner.is_empty()
        || name.is_empty()
        || reference.is_empty()
        || repo_parts.next().is_some()
        || action.contains('\n')
        || action.contains('\r')
        || action.contains(' ')
        || action.contains('\t')
        || reference.contains('@')
    {
        anyhow::bail!(
            "Invalid action reference {:?}. Expected format: owner/repo@ref",
            action
        );
    }

    Ok(())
}

fn workflow_template(action: &str) -> String {
    format!(
        r#"name: todo-tree

on:
  pull_request:

permissions:
  contents: read
  pull-requests: write

jobs:
  todo-tree:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Scan TODOs
        uses: {action}
        with:
          github-token: ${{{{ secrets.GITHUB_TOKEN }}}}
          changed-only: true
          new-only: true
"#
    )
}

fn write_workflow_template(path: &Path, force: bool, action: &str) -> Result<()> {
    if path.exists() && !force {
        anyhow::bail!(
            "Workflow file {} already exists. Use --force to overwrite.",
            path.display()
        );
    }

    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
    }

    std::fs::write(path, workflow_template(action))
        .with_context(|| format!("Failed to write workflow file: {}", path.display()))?;

    Ok(())
}