git_policy_cli/
subvolume_command.rs1use super::action::{Action, ENGINE_FAILURE_EXIT_CODE};
17use super::subvolume_arguments::{MIGRATE_HELP, MigrateArguments, MigrateMode};
18use super::subvolume_error::{Interrupt, MigrateError};
19use super::subvolume_migrate::{MigrateOptions, Verdict, migrate};
20use super::subvolume_platform::{LinuxPlatform, Platform};
21use super::subvolume_preflight::{Refusal, layout_of};
22use super::subvolume_rollback::{remove_old_tree, rollback};
23use super::subvolume_run::Run;
24use std::io::Write;
25use std::path::{Path, PathBuf};
26
27pub const REFUSED_EXIT_CODE: i32 = 1;
29
30pub fn main_checkout_root(start: &Path) -> Result<PathBuf, String> {
43 let real: PathBuf = std::fs::canonicalize(start).map_err(|error: std::io::Error| { return format!("cannot resolve {}: {error}", start.display()); })?;
44 let mut directory: PathBuf = real.clone();
45 loop {
46 let dot_git: PathBuf = directory.join(".git");
47 if let Ok(metadata) = std::fs::symlink_metadata(&dot_git) {
48 if metadata.is_dir() {
49 return Ok(directory);
50 }
51 return common_root_of_gitfile(dot_git.as_path());
52 }
53 let Some(parent) = directory.parent() else {
54 return Err(format!("{} is not inside a Git working tree (no .git above it)", real.display()));
55 };
56 directory = parent.to_path_buf();
57 }
58}
59
60fn common_root_of_gitfile(gitfile: &Path) -> Result<PathBuf, String> {
62 let text: String = std::fs::read_to_string(gitfile).map_err(|error: std::io::Error| { return format!("cannot read {}: {error}", gitfile.display()); })?;
63 let Some(named) = text.trim().strip_prefix("gitdir:") else {
64 return Err(format!("{} is not a gitfile (no `gitdir:` line)", gitfile.display()));
65 };
66 let base: &Path = gitfile.parent().unwrap_or(Path::new("/"));
67 let admin: PathBuf = base.join(named.trim());
68 let common_text: String = std::fs::read_to_string(admin.join("commondir"))
69 .map_err(|_| { return format!("{} is not a linked worktree's gitfile ({} has no commondir), so this is not a main checkout or a linked worktree of one", gitfile.display(), admin.display()); })?;
70 let common: PathBuf = std::fs::canonicalize(admin.join(common_text.trim())).map_err(|error: std::io::Error| { return format!("cannot resolve the common directory: {error}"); })?;
71 if common.file_name().is_none_or(|name: &std::ffi::OsStr| { return name != ".git"; }) {
72 return Err(format!("the common directory {} is not named .git, so the repository has no main checkout directory to convert", common.display()));
73 }
74 return common.parent().map(Path::to_path_buf).ok_or_else(|| { return String::from("the common directory has no parent"); });
75}
76
77fn say_refusals(say: &mut dyn FnMut(&str), refusals: &[Refusal]) {
79 for refusal in refusals {
80 say(format!("git cli-git migrate-to-subvolume: refused: {}", refusal.cause).as_str());
81 say(format!(" What to do: {}", refusal.remedy).as_str());
82 }
83 say("Nothing was changed.");
84}
85
86pub fn execute(platform: &dyn Platform, arguments: &MigrateArguments, root: &Path, interrupt: &Interrupt, say: &mut dyn FnMut(&str)) -> i32 {
97 let outcome: Result<i32, MigrateError> = run_mode(platform, arguments, root, interrupt, &mut *say);
98 match outcome {
99 Ok(code) => return code,
100 Err(error) => {
101 let journal: String = layout_of(root).map(|layout| { return layout.journal_path.display().to_string(); }).unwrap_or_default();
102 say(format!("git cli-git migrate-to-subvolume: failed: {error}").as_str());
103 say(format!("The journal {journal} records the steps that finished. Run the command again to resume from there, or add --rollback to restore the original directory.").as_str());
104 return ENGINE_FAILURE_EXIT_CODE;
105 }
106 }
107}
108
109fn run_mode(platform: &dyn Platform, arguments: &MigrateArguments, root: &Path, interrupt: &Interrupt, say: &mut dyn FnMut(&str)) -> Result<i32, MigrateError> {
111 if arguments.mode == MigrateMode::Migrate {
112 let options: MigrateOptions = MigrateOptions { dry_run: arguments.dry_run, nested_worktrees: arguments.nested_worktrees, refresh_indexes: arguments.refresh_indexes };
113 let verdict: Verdict = migrate(platform, root, options, interrupt, &mut *say)?;
114 if let Verdict::Refused(refusals) = verdict {
115 say_refusals(say, refusals.as_slice());
116 return Ok(REFUSED_EXIT_CODE);
117 }
118 return Ok(0);
119 }
120 let Some(layout) = layout_of(root) else {
121 return Err(MigrateError::Journal(format!("{} has no parent directory", root.display())));
122 };
123 let mut run: Run<'_> = Run::open(platform, &layout, interrupt, say)?;
124 if arguments.dry_run {
125 let what: &str = if arguments.mode == MigrateMode::Rollback { "restore the original directory" } else { "delete the original tree" };
126 run.line(format!("dry run: the command would {what} of {}; nothing was changed.", layout.root.display()).as_str());
127 return Ok(0);
128 }
129 let refused: Option<Vec<Refusal>> = if arguments.mode == MigrateMode::Rollback { rollback(&mut run, arguments.discard_unreadable)? } else { remove_old_tree(&mut run, arguments.discard_unreadable)? };
130 if let Some(refusals) = refused {
131 say_refusals(run.say, refusals.as_slice());
132 return Ok(REFUSED_EXIT_CODE);
133 }
134 return Ok(0);
135}
136
137pub fn migrate_action(arguments: &MigrateArguments, cwd: &Path) -> Action {
147 if arguments.help {
148 return Action::Exit { code: 0, stdout: String::from(MIGRATE_HELP), stderr: String::new() };
149 }
150 let root: PathBuf = match main_checkout_root(cwd) {
151 Ok(found) => found,
152 Err(message) => {
153 return Action::Exit {
154 code: REFUSED_EXIT_CODE,
155 stdout: String::new(),
156 stderr: format!("git cli-git migrate-to-subvolume: refused: {message}\n What to do: run the command inside the repository whose main checkout you want to convert.\nNothing was changed.\n"),
157 };
158 }
159 };
160 let mut say = |text: &str| {
161 let mut stderr = std::io::stderr().lock();
162 let _written = writeln!(stderr, "{text}");
163 };
164 let code: i32 = execute(&LinuxPlatform, arguments, root.as_path(), &Interrupt::never(), &mut say);
165 return Action::Exit { code, stdout: String::new(), stderr: String::new() };
166}
167
168#[cfg(test)]
170#[path = "subvolume_command_tests.rs"]
171mod tests;