use std::path::{Path, PathBuf};
use argonauts_core::events::RunEventKind;
use argonauts_core::events::run::RunOutcome;
use argonauts_core::log::COPIES_REMOVED;
use argonauts_core::{Note, NoteLevel, RunId, unborn};
use crate::engine::host::Host;
use crate::git::Git;
use crate::merge::branch::{PREFIX, run_worktree};
use crate::merge::{kept, your_folder};
const CHECKS: &str = "-checks";
pub fn ends_for_good(outcome: RunOutcome) -> bool {
outcome != RunOutcome::Stopped
}
fn keeps_its_branches(outcome: RunOutcome) -> bool {
matches!(
outcome,
RunOutcome::BranchLeft | RunOutcome::PullRequest | RunOutcome::Failed
)
}
pub async fn remove_at_end(host: &Host, outcome: RunOutcome) {
if !ends_for_good(outcome) {
return;
}
your_folder::say_if_it_has_git_now(host);
let kept = kept::at_end(host, outcome).await;
if host.settings().runs.keep_copies {
if let Some(kept) = &kept {
kept.leave_in_copy(host).await;
}
return;
}
let repo = host.repo();
let git = host.git();
let merge_copy = run_worktree(host);
let removed = remove_copies(git, &repo.root, &repo.worktrees, &merge_copy).await;
if !keeps_its_branches(outcome) {
remove_branches(git, &repo.root, host.run_id()).await;
} else if let Some(kept) = &kept {
kept.leave(host).await;
}
if let Some(removed) = removed.named() {
let note = Note {
level: NoteLevel::Info,
text: format!("{COPIES_REMOVED}{removed}"),
};
let _ = host.record(None, None, RunEventKind::Note(note));
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Removed {
pub base: bool,
pub tasks: usize,
pub merge: bool,
}
impl Removed {
pub fn named(&self) -> Option<String> {
let mut parts = Vec::new();
if self.base {
parts.push("base".to_owned());
}
match self.tasks {
0 => {}
1 => parts.push("1 task".to_owned()),
tasks => parts.push(format!("{tasks} tasks")),
}
if self.merge {
parts.push("merge".to_owned());
}
(!parts.is_empty()).then(|| parts.join(", "))
}
pub fn count(&self) -> usize {
usize::from(self.base) + self.tasks + usize::from(self.merge)
}
}
pub async fn remove_copies(git: &Git, root: &Path, worktrees: &Path, merge_copy: &Path) -> Removed {
let mut removed = Removed::default();
for copy in copies_in(worktrees) {
let gone = match git.remove_worktree(root, ©).await {
Ok(()) => true,
Err(_) if !copy.join(".git").exists() => std::fs::remove_dir_all(©).is_ok(),
Err(err) => {
tracing::warn!("removing {}: {err}", copy.display());
false
}
};
if !gone {
continue;
}
let name = copy.file_name().map(|name| name.to_string_lossy());
if copy == merge_copy {
removed.merge = true;
} else if name.as_deref() == Some("base") {
removed.base = true;
} else if !name.is_some_and(|name| name.ends_with(CHECKS)) {
removed.tasks += 1;
}
}
if let Err(err) = git.prune_worktrees(root).await {
tracing::warn!("pruning worktrees: {err}");
}
let _ = std::fs::remove_dir(worktrees);
removed
}
fn copies_in(worktrees: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(worktrees) else {
return Vec::new();
};
let mut copies: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.is_dir())
.collect();
copies.sort();
copies
}
pub async fn remove_branches(git: &Git, root: &Path, run: &RunId) -> usize {
let prefix = format!("{PREFIX}{run}/");
let mut removed = 0;
match git.branches_under(root, &prefix).await {
Ok(branches) => {
for branch in branches {
match git.delete_branch(root, &branch).await {
Ok(()) => removed += 1,
Err(err) => tracing::warn!("deleting {branch}: {err}"),
}
}
}
Err(err) => tracing::warn!("listing the run's branches: {err}"),
}
for name in [unborn::base_ref(run), kept::kept_ref(run)] {
if let Err(err) = git.delete_ref(root, &name).await {
tracing::warn!("deleting {name}: {err}");
}
}
removed
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn what_went_is_named_in_the_order_a_run_makes_it() {
let all = Removed {
base: true,
tasks: 2,
merge: true,
};
assert_eq!(all.named().as_deref(), Some("base, 2 tasks, merge"));
assert_eq!(all.count(), 4);
let one = Removed {
tasks: 1,
..Removed::default()
};
assert_eq!(one.named().as_deref(), Some("1 task"));
assert_eq!(Removed::default().named(), None);
}
#[test]
fn only_a_run_that_can_open_again_keeps_its_copies() {
for outcome in [
RunOutcome::Merged,
RunOutcome::PullRequest,
RunOutcome::BranchLeft,
RunOutcome::Abandoned,
RunOutcome::Failed,
] {
assert!(ends_for_good(outcome), "{outcome:?}");
}
assert!(!ends_for_good(RunOutcome::Stopped));
assert!(!keeps_its_branches(RunOutcome::Merged));
assert!(!keeps_its_branches(RunOutcome::Abandoned));
assert!(keeps_its_branches(RunOutcome::BranchLeft));
assert!(keeps_its_branches(RunOutcome::PullRequest));
assert!(keeps_its_branches(RunOutcome::Failed));
}
}