use crate::diff::merge_trees::TreeMergeOutput;
use crate::index::Index;
pub use crate::state::{
operation_alternatives, operation_next_steps, operation_next_steps_empty, status_operation,
InProgressOperation, NextStep, Operation,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConflictKind {
Content,
AddAdd,
ModifyDelete {
deleted_in_current: bool,
},
}
impl ConflictKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Content => "content",
Self::AddAdd => "add_add",
Self::ModifyDelete { .. } => "modify_delete",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Conflict {
pub path: String,
pub kind: ConflictKind,
}
#[must_use]
pub fn conflict_kind_from_stages(stage1: bool, stage2: bool, stage3: bool) -> ConflictKind {
match (stage1, stage2, stage3) {
(false, true, true) => ConflictKind::AddAdd,
(true, true, true) => ConflictKind::Content,
(true, false, true) => ConflictKind::ModifyDelete {
deleted_in_current: true,
},
(true, true, false) => ConflictKind::ModifyDelete {
deleted_in_current: false,
},
_ => ConflictKind::Content,
}
}
#[must_use]
pub fn conflicts_from_index(index: &Index) -> Vec<Conflict> {
let paths = conflict_path_bytes_from_index(index);
paths
.into_iter()
.map(|path_bytes| {
let stage1 = index.get(&path_bytes, 1).is_some();
let stage2 = index.get(&path_bytes, 2).is_some();
let stage3 = index.get(&path_bytes, 3).is_some();
Conflict {
path: String::from_utf8_lossy(&path_bytes).into_owned(),
kind: conflict_kind_from_stages(stage1, stage2, stage3),
}
})
.collect()
}
#[must_use]
pub fn conflicts_from_tree_merge(merged: &TreeMergeOutput) -> Vec<Conflict> {
let paths = tree_merge_conflict_path_strings(merged);
paths
.into_iter()
.map(|path| {
let path_bytes = path.as_bytes();
let stage1 = merged.index.get(path_bytes, 1).is_some();
let stage2 = merged.index.get(path_bytes, 2).is_some();
let stage3 = merged.index.get(path_bytes, 3).is_some();
Conflict {
path,
kind: conflict_kind_from_stages(stage1, stage2, stage3),
}
})
.collect()
}
#[must_use]
pub fn tree_merge_has_conflicts(merged: &TreeMergeOutput) -> bool {
!merged.conflict_content.is_empty() || merged.index.entries().iter().any(|e| e.stage() != 0)
}
#[must_use]
pub fn next_step_grit_commands(steps: &[NextStep]) -> Vec<String> {
steps.iter().filter_map(NextStep::grit_command).collect()
}
fn tree_merge_conflict_path_strings(merged: &TreeMergeOutput) -> Vec<String> {
let mut paths: Vec<String> = merged
.conflict_content
.keys()
.map(|k| String::from_utf8_lossy(k).into_owned())
.collect();
paths.extend(
merged
.index
.entries()
.iter()
.filter(|e| e.stage() != 0)
.map(|e| String::from_utf8_lossy(&e.path).into_owned()),
);
paths.sort();
paths.dedup();
paths
}
fn conflict_path_bytes_from_index(index: &Index) -> Vec<Vec<u8>> {
use std::collections::BTreeSet;
let mut paths = BTreeSet::new();
for entry in index.entries() {
let stage = entry.stage();
if (1..=3).contains(&stage) {
paths.insert(entry.path.clone());
}
}
paths.into_iter().collect()
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::index::{Index, IndexEntry};
use crate::merge_trees::TreeMergeOutput;
use std::collections::{BTreeMap, HashSet};
fn index_entry(path: &str, stage: u8) -> IndexEntry {
let mut entry = IndexEntry {
ctime_sec: 0,
ctime_nsec: 0,
mtime_sec: 0,
mtime_nsec: 0,
dev: 0,
ino: 0,
mode: crate::index::MODE_REGULAR,
uid: 0,
gid: 0,
size: 0,
oid: crate::diff::zero_oid(),
flags: 0,
flags_extended: None,
path: path.as_bytes().to_vec(),
base_index_pos: 0,
};
entry.set_stage(stage);
entry
}
#[test]
fn conflict_kind_from_stages_content() {
assert_eq!(
conflict_kind_from_stages(true, true, true),
ConflictKind::Content
);
}
#[test]
fn conflict_kind_from_stages_add_add() {
assert_eq!(
conflict_kind_from_stages(false, true, true),
ConflictKind::AddAdd
);
}
#[test]
fn conflict_kind_from_stages_modify_delete_deleted_in_current() {
assert_eq!(
conflict_kind_from_stages(true, false, true),
ConflictKind::ModifyDelete {
deleted_in_current: true
}
);
}
#[test]
fn conflict_kind_from_stages_modify_delete_deleted_incoming() {
assert_eq!(
conflict_kind_from_stages(true, true, false),
ConflictKind::ModifyDelete {
deleted_in_current: false
}
);
}
#[test]
fn conflict_kind_as_str_is_stable_snake_case() {
assert_eq!(ConflictKind::Content.as_str(), "content");
assert_eq!(ConflictKind::AddAdd.as_str(), "add_add");
assert_eq!(
ConflictKind::ModifyDelete {
deleted_in_current: true
}
.as_str(),
"modify_delete"
);
}
#[test]
fn conflicts_from_tree_merge_unions_markers_and_unmerged_index() {
let mut index = Index::new();
index.add_or_replace(index_entry("b.txt", 1));
index.add_or_replace(index_entry("b.txt", 2));
let mut conflict_content = BTreeMap::new();
conflict_content.insert(b"a.txt".to_vec(), crate::diff::zero_oid());
let merged = TreeMergeOutput {
index,
conflict_content,
result_tree: crate::diff::zero_oid(),
touched_paths: HashSet::new(),
};
let conflicts = conflicts_from_tree_merge(&merged);
assert_eq!(conflicts.len(), 2);
assert_eq!(conflicts[0].path, "a.txt");
assert_eq!(conflicts[0].kind, ConflictKind::Content);
assert_eq!(conflicts[1].path, "b.txt");
assert_eq!(
conflicts[1].kind,
ConflictKind::ModifyDelete {
deleted_in_current: false
}
);
}
#[test]
fn next_step_grit_command_for_each_operation() {
use crate::state::InProgressOperation;
let cases: &[(NextStep, &str)] = &[
(
NextStep::Abort(InProgressOperation::Merge),
"grit merge --abort",
),
(
NextStep::Continue(InProgressOperation::Merge),
"grit merge --continue",
),
(
NextStep::Abort(InProgressOperation::CherryPick),
"grit pick --abort",
),
(
NextStep::Continue(InProgressOperation::CherryPick),
"grit pick --continue",
),
(
NextStep::Skip(InProgressOperation::CherryPick),
"grit pick --skip",
),
(
NextStep::Abort(InProgressOperation::Revert),
"grit revert --abort",
),
(
NextStep::Continue(InProgressOperation::Revert),
"grit revert --continue",
),
(
NextStep::Skip(InProgressOperation::Revert),
"grit revert --skip",
),
];
for (step, expected) in cases {
assert_eq!(step.grit_command().as_deref(), Some(*expected), "{step:?}");
}
}
}