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 cli-git check (--all | -- <pathspec>...) [--policy <id>]...
git cli-git fix (--all | -- <pathspec>...) [--policy <id>]...
git cli-git doctor
git cli-git migrate-to-subvolume [--dry-run | --rollback | --remove-old]
git cli-git convert-worktree-to-subvolume [--dry-run | --abandon] (--all | <worktree>...)
git cli-git --help
";
pub const MANAGEMENT_HELP: &str = "\
Usage: git cli-git <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).
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 cli-git 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 cli-git.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,
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),
_ => 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;