use crate::command_clean::{CleanRegion, clean_changes_worktree, parse_clean_region};
use crate::command_discard::{
Discard, abort_discard, checkout_discard, force_discard, read_tree_discard, restore_discard,
restores_index_only, switch_discard,
};
use crate::command_options::OptionError;
use crate::command_reset::{ResetMode, ResetRegion, parse_reset_region};
use crate::command_stash::{parse_stash_region, stash_changes_state};
use crate::effective_target::EffectiveTarget;
use crate::global_arguments::command_tokens;
use crate::rule_worktree_dirty::DirtyScope;
pub use crate::rule_linked_worktree_messages::{
dirty_worktree_message, main_worktree_message, outside_worktree_message,
};
use std::ffi::OsString;
pub const LINKED_WORKTREE_REQUIRED_CODE: &str = "linked-worktree-required";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GuardedCommand {
Stash,
Clean,
ResetHard,
ResetMixed,
RestoreStaged,
RestoreWorktree,
CheckoutPaths,
CheckoutMove,
SwitchForce,
SwitchMove,
Remove,
Move,
CheckoutIndex,
ReadTree,
AbortMerge,
AbortRebase,
AbortPick,
AbortAm,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LinkedWorktreeDecision {
Pass,
NeedsEffectiveTarget(GuardedCommand),
WhenDirty(GuardedCommand),
}
pub fn dirty_scope(command: GuardedCommand) -> DirtyScope {
if command == GuardedCommand::Clean {
return DirtyScope::TrackedAndUntracked;
}
return DirtyScope::Tracked;
}
fn decided(command: GuardedCommand, reading: Discard) -> LinkedWorktreeDecision {
match reading {
Discard::Harmless => return LinkedWorktreeDecision::Pass,
Discard::MovesHead => return LinkedWorktreeDecision::WhenDirty(command),
Discard::Overwrites => return LinkedWorktreeDecision::NeedsEffectiveTarget(command),
}
}
fn guarded_decision(
word: &[u8],
region: &[OsString],
) -> Result<LinkedWorktreeDecision, OptionError> {
if word == b"stash" {
let reads_only: bool = parse_stash_region(region, &[])
.is_ok_and(|facts| { return !stash_changes_state(facts.subcommand); });
if reads_only {
return Ok(LinkedWorktreeDecision::Pass);
}
return Ok(LinkedWorktreeDecision::NeedsEffectiveTarget(GuardedCommand::Stash));
}
if word == b"clean" {
let facts: CleanRegion = parse_clean_region(region, &[])?;
if clean_changes_worktree(&facts) {
return Ok(LinkedWorktreeDecision::NeedsEffectiveTarget(GuardedCommand::Clean));
}
return Ok(LinkedWorktreeDecision::Pass);
}
if word == b"reset" {
let facts: ResetRegion = parse_reset_region(region, &[])?;
return Ok(match facts.mode {
Some(ResetMode::Soft) => LinkedWorktreeDecision::Pass,
Some(ResetMode::Hard | ResetMode::Merge | ResetMode::Keep) => {
LinkedWorktreeDecision::NeedsEffectiveTarget(GuardedCommand::ResetHard)
}
Some(ResetMode::Mixed) | None => {
LinkedWorktreeDecision::NeedsEffectiveTarget(GuardedCommand::ResetMixed)
}
});
}
if word == b"checkout" {
return Ok(match checkout_discard(region)? {
Discard::Overwrites => decided(GuardedCommand::CheckoutPaths, Discard::Overwrites),
reading => decided(GuardedCommand::CheckoutMove, reading),
});
}
if word == b"switch" {
return Ok(match switch_discard(region)? {
Discard::Overwrites => decided(GuardedCommand::SwitchForce, Discard::Overwrites),
reading => decided(GuardedCommand::SwitchMove, reading),
});
}
if word == b"restore" {
let command: GuardedCommand = if restores_index_only(region) {
GuardedCommand::RestoreStaged
} else {
GuardedCommand::RestoreWorktree
};
return Ok(decided(command, restore_discard(region)));
}
if word == b"rm" {
return Ok(decided(GuardedCommand::Remove, force_discard(region)));
}
if word == b"mv" {
return Ok(decided(GuardedCommand::Move, force_discard(region)));
}
if word == b"checkout-index" {
return Ok(decided(GuardedCommand::CheckoutIndex, force_discard(region)));
}
if word == b"read-tree" {
return Ok(decided(GuardedCommand::ReadTree, read_tree_discard(region)));
}
for (aborting, command) in [
(&b"merge"[..], GuardedCommand::AbortMerge),
(b"rebase", GuardedCommand::AbortRebase),
(b"cherry-pick", GuardedCommand::AbortPick),
(b"revert", GuardedCommand::AbortPick),
(b"am", GuardedCommand::AbortAm),
] {
if word == aborting {
return Ok(decided(command, abort_discard(region)));
}
}
return Ok(LinkedWorktreeDecision::Pass);
}
pub fn decide_linked_worktree(arguments: &[OsString]) -> LinkedWorktreeDecision {
let Some((word, region)) = command_tokens(arguments) else {
return LinkedWorktreeDecision::Pass;
};
return guarded_decision(word.as_encoded_bytes(), region).unwrap_or(LinkedWorktreeDecision::Pass);
}
pub fn resolve_linked_worktree(command: GuardedCommand, target: EffectiveTarget) -> Option<String> {
match target {
EffectiveTarget::OutsideWorktree => return Some(outside_worktree_message(command)),
EffectiveTarget::MainWorktree => return Some(main_worktree_message(command)),
EffectiveTarget::LinkedWorktree | EffectiveTarget::Allowlisted => return None,
}
}
#[cfg(test)]
#[path = "rule_linked_worktree_tests.rs"]
mod tests;