use anyhow::{Context, Result};
use chrono::Utc;
use std::path::{Path, PathBuf};
use super::lifecycle::{ParsecState, ShipResult, Workspace, WorkspaceStatus};
use crate::config::ParsecConfig;
use crate::git;
pub struct WorktreeManager {
repo_root: PathBuf,
config: ParsecConfig,
}
impl WorktreeManager {
pub fn new(repo: &Path, config: &ParsecConfig) -> Result<Self> {
let repo_root = git::get_repo_root(repo)
.with_context(|| format!("failed to locate git repository root from {:?}", repo))?;
Ok(Self {
repo_root,
config: config.clone(),
})
}
pub fn repo_root(&self) -> &Path {
&self.repo_root
}
pub fn create(
&self,
ticket: &str,
base: Option<&str>,
ticket_title: Option<String>,
) -> Result<Workspace> {
let base_branch = match base {
Some(b) => b.to_owned(),
None => git::get_default_branch(&self.repo_root)
.context("failed to detect default branch")?,
};
let branch = format!("{}{}", self.config.workspace.branch_prefix, ticket);
let worktree_path = match self.config.workspace.layout {
crate::config::WorktreeLayout::Sibling => {
let repo_name = self
.repo_root
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "repo".to_string());
self.repo_root
.parent()
.unwrap_or(&self.repo_root)
.join(format!("{}.{}", repo_name, ticket))
}
crate::config::WorktreeLayout::Internal => self
.repo_root
.join(&self.config.workspace.base_dir)
.join(ticket),
};
git::fetch_if_remote(&self.repo_root)?;
git::worktree_add(&self.repo_root, &worktree_path, &branch, &base_branch).with_context(
|| {
format!(
"failed to create worktree for ticket '{}' at {:?}",
ticket, worktree_path
)
},
)?;
let workspace = Workspace {
ticket: ticket.to_owned(),
path: worktree_path.clone(),
branch,
base_branch,
created_at: Utc::now(),
ticket_title,
status: WorkspaceStatus::Active,
};
let mut state =
ParsecState::load(&self.repo_root).context("failed to load parsec state")?;
state.add_workspace(workspace.clone());
state
.save(&self.repo_root)
.context("failed to save parsec state")?;
for hook_cmd in &self.config.hooks.post_create {
eprintln!("Running post-create hook: {}", hook_cmd);
let status = std::process::Command::new("sh")
.args(["-c", hook_cmd])
.current_dir(&worktree_path)
.status();
match status {
Ok(s) if s.success() => {}
Ok(s) => eprintln!("warning: hook '{}' exited with {}", hook_cmd, s),
Err(e) => eprintln!("warning: failed to run hook '{}': {}", hook_cmd, e),
}
}
Ok(workspace)
}
pub fn list(&self) -> Result<Vec<Workspace>> {
let state = ParsecState::load(&self.repo_root).context("failed to load parsec state")?;
let mut workspaces: Vec<Workspace> = state.workspaces.into_values().collect();
workspaces.sort_by_key(|w| w.created_at);
Ok(workspaces)
}
pub fn get(&self, ticket: &str) -> Result<Workspace> {
let state = ParsecState::load(&self.repo_root).context("failed to load parsec state")?;
state
.get_workspace(ticket)
.cloned()
.ok_or_else(|| anyhow::anyhow!("no workspace found for ticket '{}'", ticket))
}
pub fn ship(&self, ticket: &str) -> Result<ShipResult> {
let mut state =
ParsecState::load(&self.repo_root).context("failed to load parsec state")?;
let workspace = state
.get_workspace(ticket)
.cloned()
.ok_or_else(|| anyhow::anyhow!("no workspace found for ticket '{}'", ticket))?;
git::push_branch(&workspace.path, &workspace.branch)
.with_context(|| format!("failed to push branch '{}'", workspace.branch))?;
let cleaned_up = if self.config.ship.auto_cleanup {
match git::worktree_remove(&self.repo_root, &workspace.path) {
Ok(()) => {
let _ = git::delete_branch(&self.repo_root, &workspace.branch);
true
}
Err(e) => {
eprintln!("warning: failed to remove worktree: {e}");
false
}
}
} else {
false
};
if cleaned_up {
state.remove_workspace(ticket);
} else if let Some(ws) = state.workspaces.get_mut(ticket) {
ws.status = WorkspaceStatus::Shipped;
}
state
.save(&self.repo_root)
.context("failed to save parsec state after ship")?;
Ok(ShipResult {
ticket: ticket.to_owned(),
branch: workspace.branch,
base_branch: workspace.base_branch,
ticket_title: workspace.ticket_title,
pr_url: None, cleaned_up,
})
}
pub fn clean(&self, all: bool, dry_run: bool) -> Result<Vec<Workspace>> {
let mut state =
ParsecState::load(&self.repo_root).context("failed to load parsec state")?;
let candidates: Vec<Workspace> = state
.workspaces
.values()
.filter(|ws| {
if all {
return true;
}
git::is_branch_merged(&self.repo_root, &ws.branch, &ws.base_branch).unwrap_or(false)
})
.cloned()
.collect();
if !dry_run {
for ws in &candidates {
match git::worktree_remove(&self.repo_root, &ws.path) {
Ok(()) => {
let _ = git::delete_branch(&self.repo_root, &ws.branch);
}
Err(e) => {
eprintln!(
"warning: failed to remove worktree for '{}': {e}",
ws.ticket
);
}
}
state.remove_workspace(&ws.ticket);
}
state
.save(&self.repo_root)
.context("failed to save parsec state after clean")?;
}
Ok(candidates)
}
}