use crate::management_direct_commands::{direct_config_failure, plan_direct, refusal};
use crate::management_service_repository_selection::{Selection, status_git_dir};
use crate::action::{Action, ENGINE_FAILURE_EXIT_CODE, failure, with_leading_notice};
use crate::child_environment::{child_environment_overlay, environment_value};
use crate::doctor::doctor_action_with_section;
use crate::service_route::service_status;
use crate::management_service_settle_earlier_batches::{earlier_batches_note, plan_settle_earlier_batches, state_dir_of};
use crate::management_arguments::{
MANAGEMENT_HELP, ManagementAction, RetiredCommand,
parse_management_arguments,
};
use crate::management_selection::selected_policies;
use crate::policy_registry::PolicyId;
use crate::real_git::{ResolutionInputs, ResolvedGit, resolve_real_git};
use crate::transaction_git::GitContext;
use crate::subvolume_arguments::MigrateArguments;
use crate::worktree_convert_arguments::ConvertArguments;
#[cfg(target_os = "linux")]
use crate::subvolume_command::migrate_action;
use crate::wrapper_controls::Controls;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
fn retired_name(command: RetiredCommand) -> &'static str {
match command {
RetiredCommand::Trust => return "trust",
RetiredCommand::Untrust => return "untrust",
RetiredCommand::Status => return "status",
}
}
pub fn retired_explanation(command: RetiredCommand) -> String {
return format!(
"git monochromatic-git-policy-cli {} is retired. monochromatic-git-policy-cli now reads monochromatic-git-policy-cli.config.jsonc as data and runs no \
repository-supplied code, so there is no code execution to approve, revoke or report. \
Nothing was changed; existing trust records are left in place and are no longer read.\n",
retired_name(command)
);
}
fn scope_pathspecs(all: bool, pathspecs: Vec<OsString>) -> Vec<OsString> {
if all {
return vec![OsString::from(":/")];
}
return pathspecs;
}
fn plan_service(global_arguments: &[OsString], environment: &[(OsString, OsString)], inputs: &ResolutionInputs) -> Action {
let (real_git, notice): (Option<PathBuf>, Option<String>) = optional_real_git(inputs);
let action: Action = plan_service_with(global_arguments, environment, real_git.as_deref());
return with_leading_notice(action, notice.as_deref());
}
fn plan_settle(global_arguments: &[OsString], environment: &[(OsString, OsString)], inputs: &ResolutionInputs) -> Action {
let (real_git, notice): (Option<PathBuf>, Option<String>) = optional_real_git(inputs);
let action: Action = plan_settle_earlier_batches(global_arguments, environment, real_git.as_deref());
return with_leading_notice(action, notice.as_deref());
}
fn optional_real_git(inputs: &ResolutionInputs) -> (Option<PathBuf>, Option<String>) {
match resolve_real_git(inputs) {
Ok(found) => return (Some(found.path), found.notice),
Err(_) => return (None, None),
}
}
fn plan_service_with(global_arguments: &[OsString], environment: &[(OsString, OsString)], real_git: Option<&Path>) -> Action {
let runtime: Option<PathBuf> = match environment_value(environment, "XDG_RUNTIME_DIR") {
Some(text) if !text.is_empty() => Some(PathBuf::from(text)),
_ => None,
};
let git_dir: Option<PathBuf> = match status_git_dir(global_arguments, environment, real_git) {
Selection::Found(found) => Some(found),
Selection::Outside => None,
Selection::Unresolved(reason) => {
return Action::Exit {
code: 1,
stdout: String::new(),
stderr: format!("The commit service cannot be checked: {reason}, so it is not known which repository this invocation selects, and no service was counted.\n"),
};
}
};
let report = service_status(runtime.as_deref(), git_dir.as_deref());
return Action::Exit { code: report.code, stdout: report.stdout, stderr: report.stderr };
}
fn plan_migrate(parsed: Result<MigrateArguments, String>) -> Action {
let arguments: MigrateArguments = match parsed {
Ok(found) => found,
Err(reason) => {
return Action::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stdout: String::new(),
stderr: format!("git monochromatic-git-policy-cli migrate-to-subvolume: {reason}\n"),
};
}
};
let cwd: PathBuf = match std::env::current_dir() {
Ok(found) => found,
Err(error) => return failure(format!("cannot determine the current directory: {error}").as_str()),
};
return run_migrate(&arguments, cwd.as_path());
}
fn plan_convert(parsed: Result<ConvertArguments, String>, global_arguments: &[OsString], environment: &[(OsString, OsString)], inputs: &ResolutionInputs) -> Action {
let arguments: ConvertArguments = match parsed {
Ok(found) => ConvertArguments { command_line_attributes: crate::worktree_convert_arguments::selects_attribute_source(global_arguments), ..found },
Err(reason) => {
return Action::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stdout: String::new(),
stderr: format!("git monochromatic-git-policy-cli convert-worktree-to-subvolume: {reason}\n"),
};
}
};
let cwd: PathBuf = match std::env::current_dir() {
Ok(found) => found,
Err(error) => return failure(format!("cannot determine the current directory: {error}").as_str()),
};
let (real_git, notice): (Option<PathBuf>, Option<String>) = optional_real_git(inputs);
let action: Action = run_convert(&arguments, cwd.as_path(), environment, real_git.as_deref());
return with_leading_notice(action, notice.as_deref());
}
#[cfg(target_os = "linux")]
fn run_convert(arguments: &ConvertArguments, cwd: &Path, environment: &[(OsString, OsString)], real_git: Option<&Path>) -> Action {
return crate::worktree_convert_command::convert_action(arguments, cwd, environment, real_git);
}
#[cfg(not(target_os = "linux"))]
fn run_convert(_arguments: &ConvertArguments, _cwd: &Path, _environment: &[(OsString, OsString)], _real_git: Option<&Path>) -> Action {
return failure("git monochromatic-git-policy-cli convert-worktree-to-subvolume works on Linux with btrfs only");
}
#[cfg(target_os = "linux")]
pub fn host_filesystem_section(common_directory: &Path) -> String {
return crate::doctor_filesystem::host_section(common_directory);
}
#[cfg(not(target_os = "linux"))]
pub fn host_filesystem_section(_common_directory: &Path) -> String {
return String::new();
}
#[cfg(target_os = "linux")]
fn run_migrate(arguments: &MigrateArguments, cwd: &Path) -> Action {
return migrate_action(arguments, cwd);
}
#[cfg(not(target_os = "linux"))]
fn run_migrate(_arguments: &MigrateArguments, _cwd: &Path) -> Action {
return failure("git monochromatic-git-policy-cli migrate-to-subvolume works on Linux with btrfs only");
}
fn plan_doctor(
git_global_arguments: &[OsString],
environment: &[(OsString, OsString)],
inputs: &ResolutionInputs,
filesystem_section: &dyn Fn(&Path) -> String,
) -> Action {
let resolved: ResolvedGit = match resolve_real_git(inputs) {
Ok(found) => found,
Err(error) => return failure(error.to_string().as_str()),
};
let real_git: PathBuf = resolved.path;
let cwd: PathBuf = match std::env::current_dir() {
Ok(found) => found,
Err(error) => return failure(format!("cannot determine the current directory: {error}").as_str()),
};
let overlay: Vec<(OsString, OsString)> = child_environment_overlay(environment, real_git.as_path());
let context: GitContext = GitContext {
real_git,
overlay,
global_prefix: git_global_arguments.to_vec(),
common_dir: None,
};
let action: Action = doctor_action_with_section(&context, cwd.as_path(), filesystem_section);
let noted: Action = with_earlier_batches_note(action, git_global_arguments, environment, context.real_git.as_path());
return with_leading_notice(noted, resolved.notice.as_deref());
}
fn with_earlier_batches_note(action: Action, global_arguments: &[OsString], environment: &[(OsString, OsString)], real_git: &Path) -> Action {
let Action::Exit { code, stdout, stderr } = action else {
return action;
};
if code == ENGINE_FAILURE_EXIT_CODE {
return Action::Exit { code, stdout, stderr };
}
let selection: Selection = status_git_dir(global_arguments, environment, Some(real_git));
let note: String = earlier_batches_note(&selection, state_dir_of(environment).as_deref());
return Action::Exit { code, stdout, stderr: format!("{stderr}{note}") };
}
pub fn plan_management(
arguments: &[OsString],
git_global_arguments: &[OsString],
controls: &Controls,
environment: &[(OsString, OsString)],
inputs: &ResolutionInputs,
) -> Action {
return plan_management_with_section(arguments, git_global_arguments, controls, environment, inputs, &no_filesystem_section);
}
fn no_filesystem_section(_cwd: &Path) -> String {
return String::new();
}
pub fn plan_management_with_section(
arguments: &[OsString],
git_global_arguments: &[OsString],
controls: &Controls,
environment: &[(OsString, OsString)],
inputs: &ResolutionInputs,
filesystem_section: &dyn Fn(&Path) -> String,
) -> Action {
let parsed: ManagementAction = match parse_management_arguments(arguments) {
Ok(action) => action,
Err(reason) => return refusal(reason),
};
let (fix, policies, scope): (bool, Vec<String>, Vec<OsString>) = match parsed {
ManagementAction::Help => {
return Action::Exit {
code: 0,
stdout: String::from(MANAGEMENT_HELP),
stderr: String::new(),
};
}
ManagementAction::Retired { command, help } => {
if help {
return Action::Exit {
code: 0,
stdout: retired_explanation(command),
stderr: String::new(),
};
}
return Action::Exit {
code: 0,
stdout: String::new(),
stderr: retired_explanation(command),
};
}
ManagementAction::Doctor => return plan_doctor(git_global_arguments, environment, inputs, filesystem_section),
ManagementAction::ServiceStatus => return plan_service(git_global_arguments, environment, inputs),
ManagementAction::SettleEarlierBatches => return plan_settle(git_global_arguments, environment, inputs),
ManagementAction::MigrateToSubvolume(migrate_parse) => return plan_migrate(migrate_parse),
ManagementAction::ConvertWorktree(convert_parse) => return plan_convert(convert_parse, git_global_arguments, environment, inputs),
ManagementAction::Direct {
fix,
all,
policies,
pathspecs,
} => (fix, policies, scope_pathspecs(all, pathspecs)),
};
let selected: Vec<PolicyId> = match selected_policies(policies.as_slice()) {
Ok(ids) => ids,
Err(error) => return direct_config_failure(&error),
};
let resolved: ResolvedGit = match resolve_real_git(inputs) {
Ok(found) => found,
Err(error) => return failure(error.to_string().as_str()),
};
let action: Action = plan_direct(
fix,
selected,
scope,
controls,
environment,
git_global_arguments,
resolved.path,
);
return with_leading_notice(action, resolved.notice.as_deref());
}
#[cfg(test)]
#[path = "management_tests.rs"]
mod tests;