use anyhow::{Context, Result};
use serde_json;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::warn;
use super::{WorktreeManager, WorktreeState, WorktreeStatus};
use crate::subprocess::ProcessCommandBuilder;
pub(crate) fn filter_sessions_by_status(
states: Vec<WorktreeState>,
target_status: WorktreeStatus,
) -> Vec<WorktreeState> {
states
.into_iter()
.filter(|state| state.status == target_status)
.collect()
}
pub(crate) fn load_state_from_file(path: &Path) -> Option<WorktreeState> {
if path.extension().and_then(|s| s.to_str()) != Some("json") {
return None;
}
fs::read_to_string(path)
.ok()
.and_then(|content| serde_json::from_str::<WorktreeState>(&content).ok())
}
pub(crate) fn collect_all_states(metadata_dir: &Path) -> Result<Vec<WorktreeState>> {
if !metadata_dir.exists() {
return Ok(Vec::new());
}
let mut states = Vec::new();
for entry in fs::read_dir(metadata_dir)? {
let path = entry?.path();
if let Some(state) = load_state_from_file(&path) {
states.push(state);
}
}
Ok(states)
}
impl WorktreeManager {
pub fn get_session_state(&self, name: &str) -> Result<WorktreeState> {
let state_file = self.base_dir.join(".metadata").join(format!("{name}.json"));
let state_json = fs::read_to_string(&state_file)?;
let state: WorktreeState = serde_json::from_str(&state_json)?;
Ok(state)
}
pub fn list_interrupted_sessions(&self) -> Result<Vec<WorktreeState>> {
let metadata_dir = self.base_dir.join(".metadata");
let all_states = collect_all_states(&metadata_dir)?;
Ok(filter_sessions_by_status(
all_states,
WorktreeStatus::Interrupted,
))
}
pub fn load_session_state(&self, session_id: &str) -> Result<WorktreeState> {
self.get_session_state(session_id)
}
}
impl WorktreeManager {
pub(crate) async fn get_parent_branch(&self, branch_name: &str) -> Result<String> {
let command = ProcessCommandBuilder::new("git")
.current_dir(&self.repo_path)
.args(["config", "--get", &format!("branch.{}.merge", branch_name)])
.build();
let output = self.subprocess.runner().run(command).await?;
if output.status.success() && !output.stdout.is_empty() {
let parent = output.stdout.trim();
if let Some(name) = parent.strip_prefix("refs/heads/") {
return Ok(name.to_string());
}
}
Ok("main".to_string())
}
pub(crate) async fn get_current_branch(&self) -> Result<String> {
let command = ProcessCommandBuilder::new("git")
.current_dir(&self.repo_path)
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.build();
let output = self.subprocess.runner().run(command).await?;
if !output.status.success() {
anyhow::bail!("Failed to get current branch");
}
Ok(output.stdout.trim().to_string())
}
pub async fn get_merge_target(&self, session_name: &str) -> Result<String> {
let state = self.get_session_state(session_name)?;
if state.original_branch.is_empty() || state.original_branch == "HEAD" {
if !state.original_branch.is_empty() {
warn!("Detached HEAD state detected, using default branch");
}
return self.determine_default_branch().await;
}
if !self.check_branch_exists(&state.original_branch).await? {
warn!(
"Original branch '{}' no longer exists, using default branch",
state.original_branch
);
return self.determine_default_branch().await;
}
Ok(state.original_branch.clone())
}
pub(crate) async fn check_branch_exists(&self, branch: &str) -> Result<bool> {
let command = Self::build_branch_check_command(&self.repo_path, branch);
Ok(self
.subprocess
.runner()
.run(command)
.await
.map(|o| o.status.success())
.unwrap_or(false))
}
pub(crate) async fn get_commit_count_between_branches(
&self,
target_branch: &str,
worktree_branch: &str,
) -> Result<String> {
let command =
Self::build_commit_diff_command(&self.repo_path, target_branch, worktree_branch);
let output = self
.subprocess
.runner()
.run(command)
.await
.context("Failed to check for new commits")?;
if output.status.success() {
Ok(output.stdout.trim().to_string())
} else {
Err(anyhow::anyhow!("Failed to get commit count"))
}
}
pub(crate) async fn get_merged_branches(&self, target_branch: &str) -> Result<String> {
let check_path = self
.get_git_root_path()
.await
.unwrap_or_else(|_| self.repo_path.clone());
let command = Self::build_merge_check_command(&check_path, target_branch);
let output = self
.subprocess
.runner()
.run(command)
.await
.context("Failed to check merged branches")?;
if output.status.success() {
Ok(output.stdout)
} else {
Err(anyhow::anyhow!("Failed to check merged branches"))
}
}
pub(crate) async fn get_git_root_path(&self) -> Result<PathBuf> {
let command = ProcessCommandBuilder::new("git")
.args(["rev-parse", "--show-toplevel"])
.build();
let output = self
.subprocess
.runner()
.run(command)
.await
.context("Failed to get git root path")?;
if output.status.success() {
Ok(PathBuf::from(output.stdout.trim()))
} else {
Err(anyhow::anyhow!("Failed to get git root path"))
}
}
pub async fn get_worktree_for_branch(&self, branch: &str) -> Result<Option<PathBuf>> {
let sessions = self.list_sessions().await?;
Ok(sessions
.into_iter()
.find(|s| s.branch == branch)
.map(|s| s.path))
}
pub(crate) async fn determine_default_branch(&self) -> Result<String> {
let main_exists = self.check_branch_exists("main").await?;
Ok(Self::select_default_branch(main_exists))
}
fn select_default_branch(main_exists: bool) -> String {
if main_exists {
"main".to_string()
} else {
"master".to_string()
}
}
}
use super::CleanupConfig;
impl WorktreeManager {
pub fn get_cleanup_config() -> CleanupConfig {
CleanupConfig {
auto_cleanup: std::env::var("PRODIGY_AUTO_CLEANUP")
.map(|v| v.to_lowercase() == "true")
.unwrap_or(true),
confirm_before_cleanup: std::env::var("PRODIGY_CONFIRM_CLEANUP")
.map(|v| v.to_lowercase() == "true")
.unwrap_or(std::env::var("PRODIGY_AUTOMATION").is_err()),
retention_days: std::env::var("PRODIGY_RETENTION_DAYS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(7),
dry_run: std::env::var("PRODIGY_DRY_RUN")
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false),
}
}
}