use crate::action::Action;
use crate::subvolume_command::main_checkout_root;
use crate::diagnostic_log::debug;
use crate::parallel_budget::{BudgetRequest, HostResources, worker_budget};
use crate::line_sink::LineSink;
use crate::parallel_runner::{Event, OrderedEvents, OrderedRun, OrderedWork, run_ordered};
use crate::subvolume_error::{Interrupt, MigrateError};
use crate::subvolume_index_files::linked_worktrees_inside;
use crate::subvolume_platform::{LinuxPlatform, Platform};
use crate::worktree_convert_arguments::{CONVERT_HELP, ConvertArguments, RISK_NOTICE};
use crate::worktree_convert::{ConvertOptions, Verdict, convert_worktree};
use std::collections::HashMap;
use std::ffi::OsString;
use std::io::Write;
use std::path::{Path, PathBuf};
const TAG: &str = "convertWorktrees";
pub const REFUSED_EXIT_CODE: i32 = 1;
pub const FAILED_EXIT_CODE: i32 = 2;
fn targets(arguments: &ConvertArguments, cwd: &Path) -> Result<Vec<PathBuf>, String> {
if arguments.all {
let root: PathBuf = main_checkout_root(cwd)?;
let mut inside: Vec<PathBuf> = Vec::new();
for linked in linked_worktrees_inside(root.as_path()) {
inside.push(linked.worktree);
}
return Ok(inside);
}
let mut named: Vec<PathBuf> = Vec::new();
for word in &arguments.paths {
named.push(cwd.join(word));
}
return Ok(named);
}
pub const CONVERSION_MEMORY: u64 = 128 << 20;
pub const CONVERSION_STORAGE_CEILING: usize = 16;
pub fn conversion_workers(lanes: usize, cap: Option<usize>, host: &HostResources) -> usize {
return worker_budget(&BudgetRequest { items: lanes, per_item_memory: CONVERSION_MEMORY, ceiling: Some(CONVERSION_STORAGE_CEILING), cap, host });
}
pub struct ConvertRun<'run> {
pub platform: &'run (dyn Platform + Sync),
pub arguments: &'run ConvertArguments,
pub cwd: &'run Path,
pub interrupt: &'run Interrupt,
pub host: &'run HostResources,
}
pub fn execute(platform: &(dyn Platform + Sync), arguments: &ConvertArguments, cwd: &Path, interrupt: &Interrupt, say: &mut dyn FnMut(&str)) -> i32 {
return execute_run(&ConvertRun { platform, arguments, cwd, interrupt, host: &HostResources::read() }, say);
}
fn lanes_of(list: &[PathBuf]) -> Vec<Vec<usize>> {
let mut lanes: Vec<Vec<usize>> = Vec::new();
let mut lane_of: HashMap<PathBuf, usize> = HashMap::new();
for (index, path) in list.iter().enumerate() {
let real: PathBuf = match std::fs::canonicalize(path) {
Ok(resolved) => resolved,
Err(_unresolved) => path.clone(),
};
match lane_of.get(&real) {
Some(&lane) => lanes[lane].push(index),
None => {
lane_of.insert(real, lanes.len());
lanes.push(vec![index]);
}
}
}
return lanes;
}
struct ConvertEachWorktree<'a> {
platform: &'a (dyn Platform + Sync),
options: ConvertOptions,
interrupt: &'a Interrupt,
}
impl OrderedWork<PathBuf, Result<Verdict, MigrateError>> for ConvertEachWorktree<'_> {
fn run(&self, _index: usize, path: &PathBuf, say: &mut dyn LineSink) -> Result<Verdict, MigrateError> {
return convert_worktree(self.platform, path.as_path(), self.options, self.interrupt, say);
}
}
struct ConversionReport<'a> {
say: &'a mut dyn FnMut(&str),
list: &'a [PathBuf],
dry_run: bool,
refused: usize,
converted: usize,
failed: usize,
}
impl OrderedEvents<Result<Verdict, MigrateError>> for ConversionReport<'_> {
fn on_event(&mut self, event: Event<'_, Result<Verdict, MigrateError>>) {
match event {
Event::Begin(index) => (self.say)(format!("{}", self.list[index].display()).as_str()),
Event::Line(_index, text) => (self.say)(text),
Event::End(_index, Ok(Verdict::Converted)) => {
self.converted += 1;
(self.say)(" converted into a subvolume");
}
Event::End(_index, Ok(Verdict::Resumed)) => {
self.converted += 1;
(self.say)(if self.dry_run { " an unfinished conversion would be completed" } else { " the unfinished conversion was completed" });
}
Event::End(_index, Ok(Verdict::WouldConvert)) => (self.say)(" would convert"),
Event::End(_index, Ok(Verdict::AlreadySubvolume)) => (self.say)(" already a subvolume, skipped"),
Event::End(_index, Ok(Verdict::Abandoned)) => (self.say)(" the unfinished conversion was undone"),
Event::End(_index, Ok(Verdict::Skipped(reasons))) => {
self.refused += 1;
for reason in reasons {
(self.say)(format!(" skipped: {reason}").as_str());
}
}
Event::End(_index, Err(error)) => {
self.failed += 1;
(self.say)(format!(" failed: {error}").as_str());
(self.say)(" The conversion is journaled. Run the same command again to finish it, or add --abandon to undo it before its swap.");
}
}
}
}
pub fn execute_run(run: &ConvertRun<'_>, say: &mut dyn FnMut(&str)) -> i32 {
let ConvertRun { platform, arguments, cwd, interrupt, host } = *run;
let list: Vec<PathBuf> = match targets(arguments, cwd) {
Ok(found) => found,
Err(reason) => {
say(format!("git monochromatic-git-policy-cli convert-worktree-to-subvolume: refused: {reason}").as_str());
say("Nothing was changed.");
return REFUSED_EXIT_CODE;
}
};
let options: ConvertOptions = ConvertOptions { dry_run: arguments.dry_run, abandon: arguments.abandon, command_line_attributes: arguments.command_line_attributes };
if !arguments.dry_run && !arguments.abandon && !list.is_empty() {
say(RISK_NOTICE);
}
let lanes: Vec<Vec<usize>> = lanes_of(list.as_slice());
let workers: usize = conversion_workers(lanes.len(), arguments.jobs, host);
debug(TAG, format!("{} worktree(s) in {} lane(s) on {workers} worker(s) (host {host:?}, cap {:?})", list.len(), lanes.len(), arguments.jobs).as_str());
let mut report: ConversionReport<'_> = ConversionReport { say, list: list.as_slice(), dry_run: arguments.dry_run, refused: 0, converted: 0, failed: 0 };
run_ordered(OrderedRun {
items: list.as_slice(),
lanes: lanes.as_slice(),
workers,
work: &ConvertEachWorktree { platform, options, interrupt },
on_event: &mut report,
});
let (refused, converted, failed): (usize, usize, usize) = (report.refused, report.converted, report.failed);
let failures: String = if failed > 0 { format!(", {failed} failed") } else { String::new() };
(report.say)(format!("{} worktree(s) looked at, {converted} converted or completed, {refused} skipped{failures}.", list.len()).as_str());
if failed > 0 {
return FAILED_EXIT_CODE;
}
return if refused > 0 && !arguments.all && !arguments.dry_run { REFUSED_EXIT_CODE } else { 0 };
}
fn say_on_standard_error(text: &str) {
let mut stderr = std::io::stderr().lock();
let _written = writeln!(stderr, "{text}");
}
pub fn convert_action(arguments: &ConvertArguments, cwd: &Path, environment: &[(OsString, OsString)], real_git: Option<&Path>) -> Action {
if arguments.help {
return Action::Exit { code: 0, stdout: String::from(CONVERT_HELP), stderr: String::new() };
}
let code: i32 = execute(&LinuxPlatform::forwarding_to(environment, real_git), arguments, cwd, &Interrupt::never(), &mut say_on_standard_error);
return Action::Exit { code, stdout: String::new(), stderr: String::new() };
}
#[cfg(test)]
#[path = "worktree_convert_command_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "worktree_convert_parallel_tests.rs"]
mod parallel_tests;