use std::fs;
use std::path::PathBuf;
use anyhow::Context;
use worktrunk::copy::{copy_dir_recursive, copy_leaf};
use worktrunk::git::Repository;
use worktrunk::path::format_path_for_display;
use worktrunk::progress::{Progress, format_bytes};
use worktrunk::styling::{
eprintln, format_with_gutter, info_message, println, success_message, verbosity,
};
use super::shared::{list_and_filter_ignored_entries, resolve_copy_ignored_config};
pub fn step_copy_ignored(
from: Option<&str>,
to: Option<&str>,
dry_run: bool,
force: bool,
format: crate::cli::SwitchFormat,
) -> anyhow::Result<()> {
if worktrunk::priority::in_background_hook() {
worktrunk::priority::lower_current_process();
}
let json_mode = format == crate::cli::SwitchFormat::Json;
let repo = Repository::current()?;
let copy_ignored_config = resolve_copy_ignored_config(&repo)?;
let (source_path, source_context) = match from {
Some(branch) => {
let path = repo.worktree_for_branch(branch)?.ok_or_else(|| {
worktrunk::git::GitError::WorktreeNotFound {
branch: branch.to_string(),
}
})?;
(path, branch.to_string())
}
None => {
let path = repo.primary_worktree()?.ok_or_else(|| {
anyhow::anyhow!(
"No primary worktree found (bare repo with no default branch worktree)"
)
})?;
let context = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
(path, context)
}
};
let dest_path = match to {
Some(branch) => repo.worktree_for_branch(branch)?.ok_or_else(|| {
worktrunk::git::GitError::WorktreeNotFound {
branch: branch.to_string(),
}
})?,
None => repo.current_worktree().root()?,
};
if source_path == dest_path {
if json_mode {
let payload = serde_json::json!({
"outcome": "same_worktree",
"from": source_path,
"to": dest_path,
"entries": Vec::<serde_json::Value>::new(),
"files": 0,
"bytes": 0,
});
println!("{}", serde_json::to_string_pretty(&payload)?);
} else {
eprintln!(
"{}",
info_message("Source and destination are the same worktree")
);
}
return Ok(());
}
let worktree_paths: Vec<PathBuf> = repo
.list_worktrees()?
.iter()
.map(|wt| wt.path.clone())
.collect();
let entries_to_copy = list_and_filter_ignored_entries(
&source_path,
&source_context,
&worktree_paths,
©_ignored_config.exclude,
)?;
if entries_to_copy.is_empty() {
if json_mode {
let payload = serde_json::json!({
"outcome": if dry_run { "planned" } else { "copied" },
"dry_run": dry_run,
"from": source_path,
"to": dest_path,
"entries": Vec::<serde_json::Value>::new(),
"files": 0,
"bytes": 0,
});
println!("{}", serde_json::to_string_pretty(&payload)?);
} else {
eprintln!("{}", info_message("No matching files to copy"));
}
return Ok(());
}
let verbose = verbosity();
if dry_run {
if json_mode {
let entries: Vec<_> = entries_to_copy
.iter()
.map(|(src_entry, is_dir)| {
let relative = src_entry
.strip_prefix(&source_path)
.unwrap_or(src_entry.as_path());
serde_json::json!({
"path": relative,
"kind": if *is_dir { "dir" } else { "file" },
})
})
.collect();
let payload = serde_json::json!({
"outcome": "planned",
"dry_run": true,
"from": source_path,
"to": dest_path,
"entries": entries,
});
println!("{}", serde_json::to_string_pretty(&payload)?);
return Ok(());
}
let items: Vec<String> = entries_to_copy
.iter()
.map(|(src_entry, is_dir)| {
let relative = src_entry
.strip_prefix(&source_path)
.unwrap_or(src_entry.as_path());
let entry_type = if *is_dir { "dir" } else { "file" };
format!("{} ({})", format_path_for_display(relative), entry_type)
})
.collect();
let entry_word = if items.len() == 1 { "entry" } else { "entries" };
eprintln!(
"{}",
info_message(format!(
"Would copy {} {}:\n{}",
items.len(),
entry_word,
format_with_gutter(&items.join("\n"), None)
))
);
return Ok(());
}
if verbose >= 1 && !json_mode {
let items: Vec<String> = entries_to_copy
.iter()
.map(|(src_entry, is_dir)| {
let relative = src_entry
.strip_prefix(&source_path)
.unwrap_or(src_entry.as_path());
let entry_type = if *is_dir { "dir" } else { "file" };
format!("{} ({})", format_path_for_display(relative), entry_type)
})
.collect();
let entry_word = if items.len() == 1 { "entry" } else { "entries" };
eprintln!(
"{}",
info_message(format!(
"Copying {} {}:\n{}",
items.len(),
entry_word,
format_with_gutter(&items.join("\n"), None)
))
);
}
let progress = if verbose >= 1 || json_mode {
Progress::disabled()
} else {
Progress::start("Copying")
};
let mut copied_count = 0usize;
let mut copied_bytes = 0u64;
for (src_entry, is_dir) in &entries_to_copy {
let relative = src_entry
.strip_prefix(&source_path)
.expect("git ls-files path under worktree");
let dest_entry = dest_path.join(relative);
if *is_dir {
let (n, b) =
copy_dir_recursive(src_entry, &dest_entry, Some(&dest_path), force, &progress)
.with_context(|| {
format!("copying directory {}", format_path_for_display(relative))
})?;
copied_count += n;
copied_bytes += b;
} else {
if let Some(parent) = dest_entry.parent() {
fs::create_dir_all(parent).with_context(|| {
format!(
"creating directory for {}",
format_path_for_display(relative)
)
})?;
}
if let Some(bytes) = copy_leaf(src_entry, &dest_entry, Some(&dest_path), force)? {
copied_count += 1;
copied_bytes += bytes;
progress.record(bytes);
}
}
}
progress.finish();
if json_mode {
let entries: Vec<_> = entries_to_copy
.iter()
.map(|(src_entry, is_dir)| {
let relative = src_entry
.strip_prefix(&source_path)
.unwrap_or(src_entry.as_path());
serde_json::json!({
"path": relative,
"kind": if *is_dir { "dir" } else { "file" },
})
})
.collect();
let payload = serde_json::json!({
"outcome": "copied",
"dry_run": false,
"from": source_path,
"to": dest_path,
"entries": entries,
"files": copied_count,
"bytes": copied_bytes,
});
println!("{}", serde_json::to_string_pretty(&payload)?);
} else {
let file_word = if copied_count == 1 { "file" } else { "files" };
eprintln!(
"{}",
success_message(format!(
"Copied {copied_count} {file_word} · {}",
format_bytes(copied_bytes)
))
);
}
Ok(())
}