use std::ffi::OsString;
use crate::subvolume_arguments::{MigrateArguments, parse_migrate_arguments};
use crate::worktree_convert_arguments::{ConvertArguments, parse_convert_arguments};
pub const MANAGEMENT_USAGE: &str = "\
Usage: git monochromatic-git-policy-cli check (--all | -- <pathspec>...) [--policy <id>]...
git monochromatic-git-policy-cli fix (--all | -- <pathspec>...) [--policy <id>]...
git monochromatic-git-policy-cli doctor
git monochromatic-git-policy-cli service (status | settle-earlier-batches)
git monochromatic-git-policy-cli migrate-to-subvolume [--dry-run | --rollback | --remove-old]
git monochromatic-git-policy-cli convert-worktree-to-subvolume [--dry-run | --abandon] (--all | <worktree>...)
git monochromatic-git-policy-cli --help
";
pub const MANAGEMENT_HELP: &str = "\
Usage: git monochromatic-git-policy-cli <command> [options]
Run repository policies directly.
Commands:
check Check policies over an explicit scope.
fix Apply policy fixes over an explicit scope.
service status: whether the commit service answers (it sets itself up when a commit needs it).
settle-earlier-batches: finish the batches an earlier release's commit service left open for this worktree (doctor
says how many). Run it only when no wrapper of the earlier release is in use in this worktree at that moment; it
refuses while an earlier socket listens or an earlier helper runs, and says what to do.
doctor List traditional hooks (unsupported). Exit 0 when none or only git-lfs shims are found,
1 when any other hook is found, 2 outside a repository.
migrate-to-subvolume
Convert the main checkout into a btrfs subvolume (see `git monochromatic-git-policy-cli migrate-to-subvolume --help`).
convert-worktree-to-subvolume
Convert linked worktrees that are ordinary directories into subvolumes (see its `--help`).
Scope (exactly one):
--all The whole repository.
-- <pathspec>... The named pathspecs.
Options:
--policy <id> Run only this policy; may be repeated.
--help Show this help.
Policies are configured as data in monochromatic-git-policy-cli.config.jsonc at the repository top level.
The trust, untrust and status commands are retired: configuration is no longer code,
so there is nothing to approve.
";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RetiredCommand {
Trust,
Untrust,
Status,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ManagementAction {
Help,
Retired {
command: RetiredCommand,
help: bool,
},
Doctor,
MigrateToSubvolume(Result<MigrateArguments, String>),
ConvertWorktree(Result<ConvertArguments, String>),
ServiceStatus,
SettleEarlierBatches,
Direct {
fix: bool,
all: bool,
policies: Vec<String>,
pathspecs: Vec<OsString>,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ManagementRefusal {
Usage,
PathspecsBeforeSeparator {
fix: bool,
},
ScopeRequired {
fix: bool,
},
}
fn is_option_token(token: &OsString) -> bool {
let bytes: &[u8] = token.as_encoded_bytes();
return bytes.starts_with(b"-") && bytes != b"-";
}
fn parse_retired(
command: RetiredCommand,
rest: &[OsString],
) -> Result<ManagementAction, ManagementRefusal> {
let mut help: bool = false;
let mut terminated: bool = false;
for token in rest {
if terminated {
return Err(ManagementRefusal::Usage);
}
if token == "--" {
terminated = true;
} else if command == RetiredCommand::Trust && (token == "--help" || token == "-h") {
help = true;
} else if command == RetiredCommand::Trust && token == "--yes" {
continue;
} else {
return Err(ManagementRefusal::Usage);
}
}
return Ok(ManagementAction::Retired { command, help });
}
fn record_policy(policies: &mut Vec<String>, value: &[u8]) -> Result<(), ManagementRefusal> {
let Ok(id) = std::str::from_utf8(value) else {
return Err(ManagementRefusal::Usage);
};
let owned: String = String::from(id);
if !policies.contains(&owned) {
policies.push(owned);
}
return Ok(());
}
fn parse_direct(fix: bool, rest: &[OsString]) -> Result<ManagementAction, ManagementRefusal> {
let mut all: bool = false;
let mut policies: Vec<String> = Vec::<String>::new();
let mut pathspecs: Vec<OsString> = Vec::<OsString>::new();
let mut terminated: bool = false;
let mut pathspec_before_separator: bool = false;
let mut is_policy_value: bool = false;
for token in rest {
if is_policy_value {
is_policy_value = false;
record_policy(&mut policies, token.as_encoded_bytes())?;
continue;
}
if terminated {
pathspecs.push(token.clone());
continue;
}
if token == "--" {
terminated = true;
continue;
}
if !is_option_token(token) {
pathspec_before_separator = true;
continue;
}
if token == "--all" {
all = true;
continue;
}
if token == "--policy" {
is_policy_value = true;
continue;
}
let Some(attached) = token.as_encoded_bytes().strip_prefix(b"--policy=") else {
return Err(ManagementRefusal::Usage);
};
record_policy(&mut policies, attached)?;
}
if is_policy_value {
return Err(ManagementRefusal::Usage);
}
if pathspec_before_separator {
return Err(ManagementRefusal::PathspecsBeforeSeparator { fix });
}
if all == !pathspecs.is_empty() {
return Err(ManagementRefusal::ScopeRequired { fix });
}
return Ok(ManagementAction::Direct {
fix,
all,
policies,
pathspecs,
});
}
pub fn parse_management_arguments(
arguments: &[OsString],
) -> Result<ManagementAction, ManagementRefusal> {
let Some(name) = arguments.first() else {
return Err(ManagementRefusal::Usage);
};
let rest: &[OsString] = &arguments[1..];
if name == "--help" || name == "-h" {
if !rest.is_empty() {
return Err(ManagementRefusal::Usage);
}
return Ok(ManagementAction::Help);
}
if name == "trust" {
return parse_retired(RetiredCommand::Trust, rest);
}
if name == "untrust" {
return parse_retired(RetiredCommand::Untrust, rest);
}
if name == "status" {
return parse_retired(RetiredCommand::Status, rest);
}
if name == "doctor" {
if !rest.is_empty() {
return Err(ManagementRefusal::Usage);
}
return Ok(ManagementAction::Doctor);
}
if name == "migrate-to-subvolume" {
return Ok(ManagementAction::MigrateToSubvolume(parse_migrate_arguments(rest)));
}
if name == "convert-worktree-to-subvolume" {
return Ok(ManagementAction::ConvertWorktree(parse_convert_arguments(rest)));
}
if name == "service" {
return match rest {
[word] if word == "status" => Ok(ManagementAction::ServiceStatus),
[word] if word == "settle-earlier-batches" => Ok(ManagementAction::SettleEarlierBatches),
_ => Err(ManagementRefusal::Usage),
};
}
if name == "check" {
return parse_direct(false, rest);
}
if name == "fix" {
return parse_direct(true, rest);
}
return Err(ManagementRefusal::Usage);
}
#[cfg(test)]
#[path = "management_arguments_tests.rs"]
mod tests;