tandem-server 0.4.23

HTTP server for Tandem engine APIs
Documentation
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ManagedWorktreeRecord {
    pub key: String,
    pub repo_root: String,
    pub path: String,
    pub branch: String,
    pub base: String,
    pub managed: bool,
    pub task_id: Option<String>,
    pub owner_run_id: Option<String>,
    pub lease_id: Option<String>,
    pub cleanup_branch: bool,
    pub created_at_ms: u64,
    pub updated_at_ms: u64,
}

#[derive(Debug, Clone)]
pub struct ManagedWorktreeEnsureInput {
    pub repo_root: String,
    pub task_id: Option<String>,
    pub owner_run_id: Option<String>,
    pub lease_id: Option<String>,
    pub branch_hint: Option<String>,
    pub base: String,
    pub cleanup_branch: bool,
}

#[derive(Debug, Clone)]
pub struct ManagedWorktreeEnsureResult {
    pub record: ManagedWorktreeRecord,
    pub reused: bool,
}

fn slug_part(raw: Option<&str>) -> Option<String> {
    let cleaned = raw
        .unwrap_or_default()
        .trim()
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() {
                ch.to_ascii_lowercase()
            } else {
                '-'
            }
        })
        .collect::<String>();
    let collapsed = cleaned
        .split('-')
        .filter(|part| !part.is_empty())
        .collect::<Vec<_>>()
        .join("-");
    if collapsed.is_empty() {
        None
    } else {
        Some(collapsed)
    }
}

pub fn managed_worktree_slug(
    task_id: Option<&str>,
    owner_run_id: Option<&str>,
    lease_id: Option<&str>,
    branch_hint: Option<&str>,
) -> String {
    let mut parts = Vec::new();
    if let Some(task_id) = slug_part(task_id) {
        parts.push(task_id);
    }
    if let Some(owner_run_id) = slug_part(owner_run_id) {
        parts.push(owner_run_id);
    }
    if let Some(lease_id) = slug_part(lease_id) {
        parts.push(lease_id);
    }
    if parts.is_empty() {
        parts.push(
            slug_part(branch_hint)
                .filter(|value| !value.is_empty())
                .unwrap_or_else(|| "worktree".to_string()),
        );
    }
    parts.join("-")
}

pub fn managed_worktree_key(
    repo_root: &str,
    task_id: Option<&str>,
    owner_run_id: Option<&str>,
    lease_id: Option<&str>,
    path: &str,
    branch: &str,
) -> String {
    let task_id = task_id.unwrap_or("");
    let owner_run_id = owner_run_id.unwrap_or("");
    let lease_id = lease_id.unwrap_or("");
    format!("{repo_root}::{task_id}::{owner_run_id}::{lease_id}::{path}::{branch}")
}

pub fn managed_worktree_root(repo_root: &str) -> PathBuf {
    PathBuf::from(repo_root).join(".tandem").join("worktrees")
}

pub fn managed_worktree_path(repo_root: &str, slug: &str) -> PathBuf {
    managed_worktree_root(repo_root).join(slug)
}

pub fn is_within_managed_worktree_root(repo_root: &str, path: &Path) -> bool {
    path.starts_with(managed_worktree_root(repo_root))
}

pub fn resolve_git_repo_root(candidate: &str) -> Option<String> {
    let output = std::process::Command::new("git")
        .args(["-C", candidate, "rev-parse", "--show-toplevel"])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let resolved = String::from_utf8_lossy(&output.stdout).trim().to_string();
    crate::normalize_absolute_workspace_root(&resolved).ok()
}

pub async fn ensure_managed_worktree(
    state: &crate::AppState,
    input: ManagedWorktreeEnsureInput,
) -> anyhow::Result<ManagedWorktreeEnsureResult> {
    let slug = managed_worktree_slug(
        input.task_id.as_deref(),
        input.owner_run_id.as_deref(),
        input.lease_id.as_deref(),
        input.branch_hint.as_deref(),
    );
    let path = managed_worktree_path(&input.repo_root, &slug);
    let branch = format!("tandem/{slug}");
    let path_string = path.to_string_lossy().to_string();
    let key = managed_worktree_key(
        &input.repo_root,
        input.task_id.as_deref(),
        input.owner_run_id.as_deref(),
        input.lease_id.as_deref(),
        &path_string,
        &branch,
    );
    if let Some(existing) = state.managed_worktrees.read().await.get(&key).cloned() {
        if worktree_is_registered(&input.repo_root, &existing.path)? {
            return Ok(ManagedWorktreeEnsureResult {
                record: existing,
                reused: true,
            });
        }
    }
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    if path.exists() && !worktree_is_registered(&input.repo_root, &path_string)? {
        anyhow::bail!("managed worktree path conflict: {path_string}");
    }
    let now = crate::now_ms();
    if worktree_is_registered(&input.repo_root, &path_string)? {
        let record = ManagedWorktreeRecord {
            key: key.clone(),
            repo_root: input.repo_root.clone(),
            path: path_string,
            branch,
            base: input.base,
            managed: true,
            task_id: input.task_id,
            owner_run_id: input.owner_run_id,
            lease_id: input.lease_id,
            cleanup_branch: input.cleanup_branch,
            created_at_ms: now,
            updated_at_ms: now,
        };
        state
            .managed_worktrees
            .write()
            .await
            .insert(key, record.clone());
        return Ok(ManagedWorktreeEnsureResult {
            record,
            reused: true,
        });
    }
    let output = std::process::Command::new("git")
        .args([
            "-C",
            &input.repo_root,
            "worktree",
            "add",
            "-b",
            &branch,
            &path.to_string_lossy(),
            &input.base,
        ])
        .output()?;
    if !output.status.success() {
        anyhow::bail!(
            "git worktree add failed: {}",
            String::from_utf8_lossy(&output.stderr).trim()
        );
    }
    let record = ManagedWorktreeRecord {
        key: key.clone(),
        repo_root: input.repo_root,
        path: path.to_string_lossy().to_string(),
        branch,
        base: input.base,
        managed: true,
        task_id: input.task_id,
        owner_run_id: input.owner_run_id,
        lease_id: input.lease_id,
        cleanup_branch: input.cleanup_branch,
        created_at_ms: now,
        updated_at_ms: now,
    };
    state
        .managed_worktrees
        .write()
        .await
        .insert(key, record.clone());
    Ok(ManagedWorktreeEnsureResult {
        record,
        reused: false,
    })
}

pub async fn delete_managed_worktree(
    state: &crate::AppState,
    record: &ManagedWorktreeRecord,
) -> anyhow::Result<()> {
    let output = std::process::Command::new("git")
        .args([
            "-C",
            &record.repo_root,
            "worktree",
            "remove",
            "--force",
            &record.path,
        ])
        .output()?;
    if !output.status.success() {
        anyhow::bail!(
            "git worktree remove failed: {}",
            String::from_utf8_lossy(&output.stderr).trim()
        );
    }
    if record.cleanup_branch {
        let _ = std::process::Command::new("git")
            .args(["-C", &record.repo_root, "branch", "-D", &record.branch])
            .output();
    }
    state
        .managed_worktrees
        .write()
        .await
        .retain(|_, row| !(row.repo_root == record.repo_root && row.path == record.path));
    Ok(())
}

fn worktree_is_registered(repo_root: &str, path: &str) -> anyhow::Result<bool> {
    let output = std::process::Command::new("git")
        .args(["-C", repo_root, "worktree", "list", "--porcelain"])
        .output()?;
    if !output.status.success() {
        return Ok(false);
    }
    let needle = PathBuf::from(path);
    for line in String::from_utf8_lossy(&output.stdout).lines() {
        if let Some(value) = line.strip_prefix("worktree ") {
            if PathBuf::from(value) == needle {
                return Ok(true);
            }
        }
    }
    Ok(false)
}