use crate::command_conflict_side::{SideRequest, conflict_side_request};
use crate::conflict_side_decision::magic_on_conflicted;
use crate::conflict_side_modes::{MODE_OPTIONS, MODE_VARIABLES, magic_note, mode_note, modes_in_effect};
use crate::conflict_sides::{
FailureKind, Operation, SAVED_DIRECTORY, SaveFailure, SavedCopy, SideDecision, clean_paths_note, conflicted_paths, decide_sides, operation_in_progress,
save_copies, saved_notice,
};
use crate::diagnostic_log::debug;
use crate::discarded_prune::{Limits, Problem, directory_name_of, prune};
use crate::global_arguments::command_tokens;
use crate::private_storage::read_regular_file;
use crate::repo_read_index::{IndexEntry, parse_index};
use crate::repo_read_layout::{RepoLayout, read_layout};
use crate::repo_read_layout_without::read_layout_without;
use crate::transaction_git::GitContext;
use std::ffi::OsString;
const TAG: &str = "judgeConflictSides";
const INDEX_LIMIT_BYTES: u64 = 1 << 30;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SideVerdict {
NotApplicable,
Allowed {
notice: String,
copies: Vec<SavedCopy>,
},
Refused(String),
CopyFailed(String),
}
fn copy_failed_text(failure: &SaveFailure) -> String {
return match failure.kind {
FailureKind::Io => format!(
"cli-git: this command would replace a conflicted file, and your copy of it could not be saved first ({}: {}), so it is \
rejected and nothing was changed. Free the space or fix the permissions of the Git directory and run it again, or \
save the file yourself and pass `--no-enforce-worktree` directly after the subcommand.",
failure.subject, failure.reason
),
FailureKind::Ancestor => format!(
"cli-git: this command would replace a conflicted file, but `{}` in front of it {}. Git would replace `{}` with a real \
directory to write the file, and nothing of it would be saved first, so the command is rejected and nothing was \
changed. Move `{}` out of the way or save it yourself and run the command again, or pass `--no-enforce-worktree` \
directly after the subcommand to go ahead without a copy.",
failure.subject, failure.reason, failure.subject, failure.subject
),
FailureKind::Changed => format!(
"cli-git: the conflicted file `{}` {}, so the copy that was being made may be stale or cut short and was not kept. The \
command is rejected and nothing was changed. Run it again once nothing is writing to the file (an editor or a build may \
be), or save the file yourself and pass `--no-enforce-worktree` directly after the subcommand.",
failure.subject, failure.reason
),
};
}
fn index_declined() -> String {
return String::from(
" The index could not be read in process (it is split, sparse, unreadable, or chosen by `GIT_INDEX_FILE`), so no \
file can be shown to be conflicted.",
);
}
fn read_conflicts(layout: &RepoLayout) -> Result<std::collections::BTreeSet<Vec<u8>>, String> {
if std::env::var_os("GIT_INDEX_FILE").is_some() {
return Err(index_declined());
}
let Ok(bytes) = read_regular_file(layout.git_dir.join("index").as_path(), INDEX_LIMIT_BYTES) else {
return Err(index_declined());
};
let entries: Vec<IndexEntry> = parse_index(bytes.as_slice(), layout.object_format).ok_or_else(index_declined)?;
return Ok(conflicted_paths(entries.as_slice()));
}
fn prune_after_save(layout: &RepoLayout, copies: &[SavedCopy], milliseconds: i64, limits: Limits) -> String {
let saved_root: std::path::PathBuf = layout.common_dir.join(SAVED_DIRECTORY);
let mut keep: Option<String> = None;
for copy in copies {
if let Some(place) = copy.saved_to.as_deref() {
keep = directory_name_of(saved_root.as_path(), place);
if keep.is_some() {
break;
}
}
}
if keep.is_none() {
return String::new();
}
let outcome = prune(layout.common_dir.as_path(), milliseconds, limits, keep.as_deref());
return outcome.problems.iter().map(Problem::message).collect();
}
fn verdict_for_declined_layout(context: &GitContext, request: &SideRequest) -> SideVerdict {
let modes: Vec<String> = modes_in_effect(context.global_prefix.as_slice(), context.overlay.as_slice());
if modes.is_empty() {
return SideVerdict::NotApplicable;
}
let Some(layout) = read_layout_without(context.global_prefix.as_slice(), context.overlay.as_slice(), &MODE_OPTIONS, &MODE_VARIABLES) else {
debug(TAG, &format!("not applicable: the layout reader declines this invocation even without {modes:?}"));
return SideVerdict::NotApplicable;
};
let Some(operation): Option<Operation> = operation_in_progress(layout.git_dir.as_path()) else {
return SideVerdict::NotApplicable;
};
let Ok(conflicted) = read_conflicts(&layout) else {
return SideVerdict::NotApplicable;
};
if let SideDecision::Allow(_) = decide_sides(request.paths.as_slice(), layout.prefix.as_slice(), layout.worktree_root.as_path(), &conflicted) {
debug(TAG, &format!("refused during {operation:?}: only {modes:?} stands between this line and the carve-out"));
return SideVerdict::Refused(mode_note(modes.as_slice(), operation));
}
return SideVerdict::NotApplicable;
}
pub fn judge_conflict_sides(context: &GitContext, arguments: &[OsString], milliseconds: i64, limits: Limits) -> SideVerdict {
let Some((word, region)) = command_tokens(arguments) else {
return SideVerdict::NotApplicable;
};
let Some(request): Option<SideRequest> = conflict_side_request(word.as_encoded_bytes(), region) else {
return SideVerdict::NotApplicable;
};
let Some(layout) = read_layout(context.global_prefix.as_slice(), context.overlay.as_slice()) else {
debug(TAG, "the layout reader declined this invocation (a global option other than -C, a GIT_ variable it does not tolerate, or a layout it does not read), so the usual rejection stands");
return verdict_for_declined_layout(context, &request);
};
let Some(operation): Option<Operation> = operation_in_progress(layout.git_dir.as_path()) else {
return SideVerdict::NotApplicable;
};
let conflicted = match read_conflicts(&layout) {
Ok(found) => found,
Err(note) => {
debug(TAG, &format!("refused during {operation:?}: the conflicted paths cannot be read in process:{note}"));
return SideVerdict::Refused(note);
}
};
match decide_sides(request.paths.as_slice(), layout.prefix.as_slice(), layout.worktree_root.as_path(), &conflicted) {
SideDecision::Refuse(others) => {
debug(TAG, &format!("refused during {operation:?}: {others:?} are not conflicted files among {} conflicted", conflicted.len()));
let magic = magic_on_conflicted(others.as_slice(), layout.prefix.as_slice(), layout.worktree_root.as_path(), &conflicted);
if let Some(names) = magic {
debug(TAG, &format!("the only reason is pathspec magic on conflicted files: {names:?}"));
return SideVerdict::Refused(magic_note(names.as_slice(), operation));
}
return SideVerdict::Refused(clean_paths_note(others.as_slice(), operation));
}
SideDecision::Allow(paths) => {
match save_copies(layout.worktree_root.as_path(), layout.common_dir.as_path(), paths.as_slice(), milliseconds) {
Ok(copies) => {
let unpruned: String = prune_after_save(&layout, copies.as_slice(), milliseconds, limits);
return SideVerdict::Allowed { notice: format!("{}{unpruned}", saved_notice(copies.as_slice())), copies };
}
Err(failure) => {
debug(TAG, &format!("copy failed during {operation:?}, command rejected: {failure:?}"));
return SideVerdict::CopyFailed(copy_failed_text(&failure));
}
}
}
}
}
#[cfg(test)]
#[path = "conflict_side_guard_tests.rs"]
mod tests;