use sley_core::{BString, GitError, ObjectFormat, ObjectId, Result};
use sley_index::{is_gitlink, is_symlink_mode};
use sley_object::{Commit, EncodedObject, ObjectType, Tree, TreeEntries, TreeEntry};
use sley_odb::{FileObjectDatabase, ObjectReader, ObjectWriter};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::sync::Arc;
use crate::blob_merge::{
merge_blobs, ConflictStyle, MergeBlobOptions, MergeBlobResult, MergeFavor,
};
use crate::line_diff::WsIgnore;
use crate::name_status::{
diff_name_status_maps_with_renames, flatten_tree, is_type_change, DiffNameStatusOptions,
MergeEntryMap, NameStatus, NameStatusEntry, TrackedEntry, DEFAULT_RENAME_THRESHOLD,
};
pub struct MergeTreesOptions<'a> {
pub ours_label: &'a str,
pub theirs_label: &'a str,
pub ancestor_label: &'a str,
pub favor: MergeFavor,
pub path_favor: Option<&'a dyn Fn(&[u8]) -> MergeFavor>,
pub path_marker_size: Option<&'a dyn Fn(&[u8]) -> usize>,
pub detect_renames: bool,
pub rename_threshold: u8,
pub rename_limit: usize,
pub directory_renames: DirectoryRenames,
pub style: ConflictStyle,
pub ws_ignore: WsIgnore,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum DirectoryRenames {
#[default]
False,
True,
Conflict,
}
impl Default for MergeTreesOptions<'_> {
fn default() -> Self {
Self {
ours_label: "ours",
theirs_label: "theirs",
ancestor_label: "merged common ancestors",
favor: MergeFavor::None,
path_favor: None,
path_marker_size: None,
detect_renames: false,
rename_threshold: DEFAULT_RENAME_THRESHOLD,
rename_limit: 0,
directory_renames: DirectoryRenames::False,
style: ConflictStyle::Merge,
ws_ignore: WsIgnore::EMPTY,
}
}
}
fn merge_favor_for_path(options: &MergeTreesOptions<'_>, path: &[u8]) -> MergeFavor {
if options.favor != MergeFavor::None {
return options.favor;
}
options
.path_favor
.map(|resolver| resolver(path))
.unwrap_or(MergeFavor::None)
}
fn merge_marker_size_for_path(options: &MergeTreesOptions<'_>, path: &[u8]) -> usize {
options
.path_marker_size
.map(|resolver| resolver(path))
.unwrap_or(7)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MergeConflictKind {
Content { add_add: bool },
ModifyDelete {
deleted_in: String,
modified_in: String,
},
RenameContent {
old_path: Vec<u8>,
},
RenameRenameTwoToOne {
ours_path: Vec<u8>,
theirs_path: Vec<u8>,
},
RenameRenameOneToTwo {
old_path: Vec<u8>,
ours_path: Vec<u8>,
theirs_path: Vec<u8>,
ours_label: String,
theirs_label: String,
},
RenameRenameOneToTwoStage,
DirRenameSplit {
source_dir: Vec<u8>,
},
RenameDelete {
old_path: Vec<u8>,
renamed_in: String,
deleted_in: String,
},
FileDirectory {
original_path: Vec<u8>,
moved_from: String,
},
DirRenameLocation {
old_path: Vec<u8>,
renamed_from: Option<Vec<u8>>,
added_in: String,
dir_renamed_in: String,
back_to_self: bool,
},
DirRenameImplicitCollision {
sources: Vec<Vec<u8>>,
},
DistinctTypes {
original_path: Vec<u8>,
ours_renamed: Option<Vec<u8>>,
theirs_renamed: Option<Vec<u8>>,
},
DistinctTypesStage,
}
#[derive(Debug, Clone)]
pub struct MergedPath {
pub path: Vec<u8>,
pub stages: MergeStages,
pub result: Option<(u32, ObjectId)>,
pub worktree: Option<(u32, Vec<u8>)>,
pub conflict: Option<MergeConflictKind>,
pub auto_merged: bool,
}
impl MergedPath {
pub fn is_clean(&self) -> bool {
self.conflict.is_none()
}
}
#[derive(Debug, Clone, Default)]
pub struct MergeStages {
pub base: Option<(u32, ObjectId)>,
pub ours: Option<(u32, ObjectId)>,
pub theirs: Option<(u32, ObjectId)>,
}
#[derive(Debug, Clone)]
pub struct MergeTreesResult {
pub tree: ObjectId,
pub paths: Vec<MergedPath>,
pub clean: bool,
pub cleanup_paths: Vec<Vec<u8>>,
pub info_messages: Vec<MergeInfoMessage>,
}
impl MergeTreesResult {
pub fn conflicts(&self) -> impl Iterator<Item = &MergedPath> {
self.paths.iter().filter(|entry| entry.conflict.is_some())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MergeInfoMessage {
DirRenameSkippedDueToRerename {
old_dir: Vec<u8>,
path: Vec<u8>,
new_dir: Vec<u8>,
},
DirRenameApplied {
old_path: Vec<u8>,
new_path: Vec<u8>,
renamed_from: Option<Vec<u8>>,
added_in: String,
dir_renamed_in: String,
},
DirRenameLocationConflict {
old_path: Vec<u8>,
new_path: Vec<u8>,
renamed_from: Option<Vec<u8>>,
added_in: String,
dir_renamed_in: String,
},
RenameDeleteConflict {
old_path: Vec<u8>,
new_path: Vec<u8>,
renamed_in: String,
deleted_in: String,
},
}
pub fn is_mergeable_file_mode(mode: u32) -> bool {
mode == 0o100644 || mode == 0o100755
}
pub fn merge_trees(
db: &FileObjectDatabase,
format: ObjectFormat,
base: Option<&ObjectId>,
ours: &ObjectId,
theirs: &ObjectId,
options: &MergeTreesOptions<'_>,
) -> Result<MergeTreesResult> {
let base_map = match base {
Some(tree) => flatten_tree(db, format, tree)?,
None => MergeEntryMap::new(),
};
let ours_map = flatten_tree(db, format, ours)?;
let theirs_map = flatten_tree(db, format, theirs)?;
merge_entry_maps(db, format, &base_map, &ours_map, &theirs_map, options)
}
pub fn merge_entry_maps(
db: &FileObjectDatabase,
format: ObjectFormat,
base_map: &MergeEntryMap,
ours_map: &MergeEntryMap,
theirs_map: &MergeEntryMap,
options: &MergeTreesOptions<'_>,
) -> Result<MergeTreesResult> {
let (mut renames, side_renames) = if options.detect_renames {
let (renames, ours_side, theirs_side) =
detect_merge_renames(db, format, base_map, ours_map, theirs_map, options)?;
(renames, Some((ours_side, theirs_side)))
} else {
(MergeRenames::default(), None)
};
let (mut eff_base, mut eff_ours, mut eff_theirs) =
apply_merge_renames(base_map, ours_map, theirs_map, &renames);
let mut dir_rename_dirty = false;
let mut rehomed_paths: BTreeMap<Vec<u8>, RehomeSides> = BTreeMap::new();
let mut dir_rename_two_to_one: Vec<DirRenameTwoToOne> = Vec::new();
let mut dir_rename_collisions: Vec<DirRenameCollision> = Vec::new();
let mut dir_rename_splits: BTreeSet<Vec<u8>> = BTreeSet::new();
let mut dir_rename_back_to_self: BTreeSet<Vec<u8>> = BTreeSet::new();
let mut info_messages = Vec::new();
let mut cleanup_paths: BTreeSet<Vec<u8>> = renames
.dest_to_source
.values()
.map(|rename| rename.source.clone())
.collect();
if options.directory_renames != DirectoryRenames::False
&& let Some((ours_side, theirs_side)) = &side_renames
{
let dir_renames = compute_directory_renames(ours_map, theirs_map, ours_side, theirs_side);
let outcome = apply_directory_renames(
base_map,
&eff_base,
&eff_ours,
&eff_theirs,
ours_side,
theirs_side,
&dir_renames,
&renames.dest_to_source,
);
eff_base = outcome.base;
eff_ours = outcome.ours;
eff_theirs = outcome.theirs;
rehomed_paths = outcome.rehomed;
dir_rename_collisions = outcome.collisions;
dir_rename_splits = outcome.splits;
dir_rename_back_to_self = outcome.back_to_self;
info_messages = outcome.info_messages;
dir_rename_dirty = outcome.dirty;
remap_rename_destinations(&mut renames, &rehomed_paths);
drop_collapsed_rename_rename_conflicts(&mut renames);
dir_rename_two_to_one = collect_dir_rename_two_to_one(&renames, &rehomed_paths);
}
for info in rehomed_paths
.values()
.flat_map(|sides| [&sides.ours, &sides.theirs])
.flatten()
{
cleanup_paths.insert(info.old_path.clone());
}
if options.directory_renames == DirectoryRenames::True {
for (dest, sides) in &rehomed_paths {
for info in [&sides.ours, &sides.theirs].into_iter().flatten() {
let (added_in, dir_renamed_in) = if info.added_on_ours {
(
options.ours_label.to_string(),
options.theirs_label.to_string(),
)
} else {
(
options.theirs_label.to_string(),
options.ours_label.to_string(),
)
};
info_messages.push(MergeInfoMessage::DirRenameApplied {
old_path: info.old_path.clone(),
new_path: dest.clone(),
renamed_from: info.renamed_from.clone(),
added_in,
dir_renamed_in,
});
}
}
}
let dir_rename_conflict_paths: BTreeMap<Vec<u8>, RehomeSides> =
if options.directory_renames == DirectoryRenames::Conflict {
rehomed_paths.clone()
} else {
BTreeMap::new()
};
let mut all_paths = BTreeSet::new();
all_paths.extend(eff_base.keys().cloned());
all_paths.extend(eff_ours.keys().cloned());
all_paths.extend(eff_theirs.keys().cloned());
let mut paths: Vec<MergedPath> = Vec::new();
let mut leaves: MergeEntryMap = BTreeMap::new();
let mut clean = true;
for path in all_paths {
let base = eff_base.get(&path).cloned();
let ours = eff_ours.get(&path).cloned();
let theirs = eff_theirs.get(&path).cloned();
let rename = renames.dest_to_source.get(&path);
let old_path = rename.map(|r| r.source.clone());
let favor = merge_favor_for_path(options, &path);
if ours == theirs {
if let Some(entry) = ours {
leaves.insert(path.clone(), entry);
}
paths.push(clean_path(path, ours));
continue;
}
if ours == base {
if let Some(entry) = &theirs {
leaves.insert(path.clone(), *entry);
}
paths.push(clean_path(path, theirs));
continue;
}
if theirs == base {
if let Some(entry) = &ours {
leaves.insert(path.clone(), *entry);
}
paths.push(clean_path(path, ours));
continue;
}
let content_mergeable = matches!(&ours, Some((mode, _)) if is_mergeable_file_mode(*mode))
&& matches!(&theirs, Some((mode, _)) if is_mergeable_file_mode(*mode))
&& match &base {
Some((mode, _)) => is_mergeable_file_mode(*mode),
None => true,
};
if let (true, Some((ours_mode, ours_oid)), Some((theirs_mode, theirs_oid))) =
(content_mergeable, &ours, &theirs)
{
let add_add = base.is_none();
let base_bytes = match &base {
Some((_, oid)) => merge_blob_bytes(db, oid)?,
None => Vec::new(),
};
let ours_bytes = merge_blob_bytes(db, ours_oid)?;
let theirs_bytes = merge_blob_bytes(db, theirs_oid)?;
let rehome = rehomed_paths.get(&path);
let (base_label_owned, ours_label, theirs_label) = match rename {
Some(MergeRename { source, side }) => {
let (ours_path, theirs_path) = match side {
RenameSide::Theirs => (source.as_slice(), path.as_slice()),
RenameSide::Ours => (path.as_slice(), source.as_slice()),
};
(
qualify_label(options.ancestor_label, source.as_slice()),
qualify_label(options.ours_label, ours_path),
qualify_label(options.theirs_label, theirs_path),
)
}
None => {
let ours_path = rehome
.and_then(|info| info.ours.as_ref())
.map_or(path.as_slice(), |info| info.old_path.as_slice());
let theirs_path = rehome
.and_then(|info| info.theirs.as_ref())
.map_or(path.as_slice(), |info| info.old_path.as_slice());
if ours_path != path.as_slice() || theirs_path != path.as_slice() {
(
qualify_label(options.ancestor_label, path.as_slice()),
qualify_label(options.ours_label, ours_path),
qualify_label(options.theirs_label, theirs_path),
)
} else {
(
options.ancestor_label.to_string(),
options.ours_label.to_string(),
options.theirs_label.to_string(),
)
}
}
};
let result = merge_blobs(
&base_bytes,
&ours_bytes,
&theirs_bytes,
&MergeBlobOptions {
ours_label: &ours_label,
theirs_label: &theirs_label,
base_label: &base_label_owned,
style: options.style,
favor,
ws_ignore: options.ws_ignore,
marker_size: merge_marker_size_for_path(options, &path),
},
);
let base_mode = base.as_ref().map(|(mode, _)| *mode);
let (resolved_mode, mode_conflict) =
merge_file_modes(base_mode, *ours_mode, *theirs_mode);
if !result.conflicted && !mode_conflict {
let oid = db.write_object(EncodedObject::new(ObjectType::Blob, result.content))?;
leaves.insert(path.clone(), (resolved_mode, oid));
paths.push(clean_path_auto(path, Some((resolved_mode, oid)), true));
} else if favor != MergeFavor::None && !mode_conflict {
let chosen = if favor == MergeFavor::Ours {
ours
} else {
theirs
};
if let Some(entry) = chosen {
leaves.insert(path.clone(), entry);
}
paths.push(clean_path_auto(path, chosen, true));
} else {
clean = false;
let oid =
db.write_object(EncodedObject::new(ObjectType::Blob, result.content.clone()))?;
leaves.insert(path.clone(), (resolved_mode, oid));
let worktree_mode = if *ours_mode == *theirs_mode {
*ours_mode
} else {
0o100644
};
let conflict = if let Some(old) = &old_path {
MergeConflictKind::RenameContent {
old_path: old.clone(),
}
} else if add_add {
match rehome.and_then(|info| Some((info.ours.as_ref()?, info.theirs.as_ref()?)))
{
Some((ours_info, theirs_info)) => MergeConflictKind::RenameRenameTwoToOne {
ours_path: ours_info.old_path.clone(),
theirs_path: theirs_info.old_path.clone(),
},
None => MergeConflictKind::Content { add_add },
}
} else {
MergeConflictKind::Content { add_add }
};
paths.push(MergedPath {
path: path.clone(),
stages: stages_for(&base, &ours, &theirs),
result: Some((resolved_mode, oid)),
worktree: Some((worktree_mode, result.content)),
conflict: Some(conflict),
auto_merged: true,
});
}
} else if base.is_some() && (ours.is_none() || theirs.is_none()) {
clean = false;
let (deleted_in, modified_in, surviving) = if ours.is_none() {
(
options.ours_label.to_string(),
options.theirs_label.to_string(),
theirs,
)
} else {
(
options.theirs_label.to_string(),
options.ours_label.to_string(),
ours,
)
};
let worktree = match &surviving {
Some((mode, oid)) => Some((*mode, merge_worktree_bytes(db, *mode, oid)?)),
None => None,
};
if let Some(entry) = surviving {
leaves.insert(path.clone(), entry);
}
paths.push(MergedPath {
path: path.clone(),
stages: stages_for(&base, &ours, &theirs),
result: surviving,
worktree,
conflict: Some(MergeConflictKind::ModifyDelete {
deleted_in,
modified_in,
}),
auto_merged: false,
});
} else if let (Some(&(ours_mode, ours_oid)), Some(&(theirs_mode, theirs_oid))) =
(ours.as_ref(), theirs.as_ref())
&& sley_index::is_symlink_mode(ours_mode)
&& sley_index::is_symlink_mode(theirs_mode)
{
match favor {
MergeFavor::Ours => {
leaves.insert(path.clone(), (ours_mode, ours_oid));
paths.push(clean_path_auto(
path.clone(),
Some((ours_mode, ours_oid)),
false,
));
}
MergeFavor::Theirs => {
leaves.insert(path.clone(), (theirs_mode, theirs_oid));
paths.push(clean_path_auto(
path.clone(),
Some((theirs_mode, theirs_oid)),
false,
));
}
MergeFavor::None | MergeFavor::Union => {
clean = false;
leaves.insert(path.clone(), (ours_mode, ours_oid));
let worktree =
Some((ours_mode, merge_worktree_bytes(db, ours_mode, &ours_oid)?));
paths.push(MergedPath {
path: path.clone(),
stages: stages_for(&base, &ours, &theirs),
result: Some((ours_mode, ours_oid)),
worktree,
conflict: Some(MergeConflictKind::Content {
add_add: base.is_none(),
}),
auto_merged: false,
});
}
}
} else if let (Some((ours_mode, ours_oid)), Some((theirs_mode, theirs_oid))) =
(ours, theirs)
&& is_type_change(ours_mode, theirs_mode)
{
clean = false;
let (rename_ours, rename_theirs) = if is_mergeable_file_mode(ours_mode) {
(true, false)
} else if is_mergeable_file_mode(theirs_mode) {
(false, true)
} else {
(true, true)
};
let ours_base = base.filter(|(mode, _)| !is_type_change(*mode, ours_mode));
let theirs_base = base.filter(|(mode, _)| !is_type_change(*mode, theirs_mode));
let ours_path = if rename_ours {
unique_df_path(&path, options.ours_label, &leaves, &paths)
} else {
path.clone()
};
leaves.insert(ours_path.clone(), (ours_mode, ours_oid));
let theirs_path = if rename_theirs {
unique_df_path(&path, options.theirs_label, &leaves, &paths)
} else {
path.clone()
};
leaves.insert(theirs_path.clone(), (theirs_mode, theirs_oid));
let ours_carries_message = !rename_ours || rename_theirs;
let distinct = MergeConflictKind::DistinctTypes {
original_path: path.clone(),
ours_renamed: rename_ours.then(|| ours_path.clone()),
theirs_renamed: rename_theirs.then(|| theirs_path.clone()),
};
let ours_worktree = Some((ours_mode, merge_worktree_bytes(db, ours_mode, &ours_oid)?));
paths.push(MergedPath {
path: ours_path,
stages: MergeStages {
base: ours_base,
ours: Some((ours_mode, ours_oid)),
theirs: None,
},
result: Some((ours_mode, ours_oid)),
worktree: ours_worktree,
conflict: Some(if ours_carries_message {
distinct.clone()
} else {
MergeConflictKind::DistinctTypesStage
}),
auto_merged: false,
});
let theirs_worktree = Some((
theirs_mode,
merge_worktree_bytes(db, theirs_mode, &theirs_oid)?,
));
paths.push(MergedPath {
path: theirs_path,
stages: MergeStages {
base: theirs_base,
ours: None,
theirs: Some((theirs_mode, theirs_oid)),
},
result: Some((theirs_mode, theirs_oid)),
worktree: theirs_worktree,
conflict: Some(if ours_carries_message {
MergeConflictKind::DistinctTypesStage
} else {
distinct
}),
auto_merged: false,
});
} else {
clean = false;
let add_add = base.is_none();
let surviving = ours.or(theirs);
let worktree = match &surviving {
Some((mode, oid)) => Some((*mode, merge_worktree_bytes(db, *mode, oid)?)),
None => None,
};
if let Some(entry) = surviving {
leaves.insert(path.clone(), entry);
}
paths.push(MergedPath {
path: path.clone(),
stages: stages_for(&base, &ours, &theirs),
result: surviving,
worktree,
conflict: Some(MergeConflictKind::Content { add_add }),
auto_merged: false,
});
}
}
if !renames.rename_rename_one_to_two.is_empty() {
apply_rename_rename_one_to_two_conflicts(
db,
base_map,
&eff_ours,
&eff_theirs,
&renames.rename_rename_one_to_two,
&mut paths,
&mut leaves,
options,
)?;
clean = false;
}
if !dir_rename_two_to_one.is_empty() {
apply_dir_rename_two_to_one_conflicts(
db,
&eff_ours,
&eff_theirs,
&dir_rename_two_to_one,
&mut paths,
&mut leaves,
options,
)?;
clean = false;
}
if !renames.rename_rename_two_to_one.is_empty() || !renames.rename_adds.is_empty() {
apply_rename_two_to_one_and_add_conflicts(
db,
base_map,
ours_map,
theirs_map,
&renames,
&mut paths,
&mut leaves,
options,
)?;
clean = false;
}
if !renames.rename_deletes.is_empty() {
for (dest, rd) in &renames.rename_deletes {
let Some(slot) = paths.iter_mut().find(|p| &p.path == dest) else {
continue;
};
if slot.conflict.is_some() {
continue;
}
let base_entry = base_map.get(&rd.source).copied();
let renamed_entry = slot.result;
let (ours_stage, theirs_stage) = match rd.side {
RenameSide::Ours => (renamed_entry, None),
RenameSide::Theirs => (None, renamed_entry),
};
let (renamed_in, deleted_in) = match rd.side {
RenameSide::Ours => (
options.ours_label.to_string(),
options.theirs_label.to_string(),
),
RenameSide::Theirs => (
options.theirs_label.to_string(),
options.ours_label.to_string(),
),
};
let worktree = match &renamed_entry {
Some((mode, oid)) => Some((*mode, merge_worktree_bytes(db, *mode, oid)?)),
None => None,
};
slot.stages = MergeStages {
base: base_entry,
ours: ours_stage,
theirs: theirs_stage,
};
slot.worktree = worktree;
slot.conflict = Some(MergeConflictKind::RenameDelete {
old_path: rd.source.clone(),
renamed_in,
deleted_in,
});
clean = false;
}
}
if dir_rename_dirty {
clean = false;
}
for collision in &dir_rename_collisions {
clean = false;
if let Some(slot) = paths.iter_mut().find(|p| p.path == collision.dest)
&& slot.conflict.is_none()
{
slot.conflict = Some(MergeConflictKind::DirRenameImplicitCollision {
sources: collision.sources.clone(),
});
} else if !paths.iter().any(|p| p.path == collision.dest) {
paths.push(MergedPath {
path: collision.dest.clone(),
stages: MergeStages::default(),
result: None,
worktree: None,
conflict: Some(MergeConflictKind::DirRenameImplicitCollision {
sources: collision.sources.clone(),
}),
auto_merged: false,
});
}
}
for source_dir in &dir_rename_splits {
clean = false;
paths.push(MergedPath {
path: source_dir.clone(),
stages: MergeStages::default(),
result: None,
worktree: None,
conflict: Some(MergeConflictKind::DirRenameSplit {
source_dir: source_dir.clone(),
}),
auto_merged: false,
});
}
if !dir_rename_conflict_paths.is_empty() {
clean = false;
for (dest, infos) in &dir_rename_conflict_paths {
for info in [&infos.ours, &infos.theirs].into_iter().flatten() {
let (added_in, dir_renamed_in) = if info.added_on_ours {
(
options.ours_label.to_string(),
options.theirs_label.to_string(),
)
} else {
(
options.theirs_label.to_string(),
options.ours_label.to_string(),
)
};
let back_to_self = dir_rename_back_to_self.contains(dest);
if let Some(slot) = paths.iter_mut().find(|p| &p.path == dest)
&& slot.conflict.is_none()
{
if back_to_self {
slot.stages = MergeStages {
base: eff_base.get(dest).copied(),
ours: eff_ours.get(dest).copied(),
theirs: eff_theirs.get(dest).copied(),
};
slot.worktree = match &slot.result {
Some((mode, oid)) => {
Some((*mode, merge_worktree_bytes(db, *mode, oid)?))
}
None => slot.worktree.clone(),
};
}
slot.conflict = Some(MergeConflictKind::DirRenameLocation {
old_path: info.old_path.clone(),
renamed_from: info.renamed_from.clone(),
added_in,
dir_renamed_in,
back_to_self,
});
} else {
info_messages.push(MergeInfoMessage::DirRenameLocationConflict {
old_path: info.old_path.clone(),
new_path: dest.clone(),
renamed_from: info.renamed_from.clone(),
added_in,
dir_renamed_in,
});
}
}
}
}
resolve_directory_file_conflicts(
db,
&mut paths,
&mut leaves,
&mut clean,
&eff_ours,
&eff_theirs,
options,
&mut info_messages,
)?;
let tree = write_merged_tree(db, &leaves)?;
cleanup_paths.retain(|path| !leaves.contains_key(path));
Ok(MergeTreesResult {
tree,
paths,
clean,
cleanup_paths: cleanup_paths.into_iter().collect(),
info_messages,
})
}
fn flatten_branch_label(branch: &str) -> String {
branch.replace('/', "_")
}
fn unique_df_path(
path: &[u8],
branch: &str,
leaves: &MergeEntryMap,
paths: &[MergedPath],
) -> Vec<u8> {
let mut base = path.to_vec();
base.push(b'~');
base.extend_from_slice(flatten_branch_label(branch).as_bytes());
let taken = |candidate: &[u8]| {
leaves.contains_key(candidate) || paths.iter().any(|p| p.path == candidate)
};
if !taken(&base) {
return base;
}
let mut suffix = 0usize;
loop {
let mut candidate = base.clone();
candidate.push(b'_');
candidate.extend_from_slice(suffix.to_string().as_bytes());
if !taken(&candidate) {
return candidate;
}
suffix += 1;
}
}
#[allow(clippy::too_many_arguments)]
fn resolve_directory_file_conflicts(
db: &FileObjectDatabase,
paths: &mut Vec<MergedPath>,
leaves: &mut MergeEntryMap,
clean: &mut bool,
eff_ours: &MergeEntryMap,
eff_theirs: &MergeEntryMap,
options: &MergeTreesOptions<'_>,
info_messages: &mut Vec<MergeInfoMessage>,
) -> Result<()> {
let mut directory_prefixes: BTreeSet<Vec<u8>> = BTreeSet::new();
for key in leaves.keys() {
let mut idx = 0;
while let Some(pos) = key[idx..].iter().position(|b| *b == b'/') {
let end = idx + pos;
directory_prefixes.insert(key[..end].to_vec());
idx = end + 1;
}
}
if directory_prefixes.is_empty() {
return Ok(());
}
let colliding: Vec<Vec<u8>> = leaves
.keys()
.filter(|key| directory_prefixes.contains(*key))
.cloned()
.collect();
for original in colliding {
let Some(entry) = leaves.remove(&original) else {
continue;
};
let moved_bytes = merge_worktree_bytes(db, entry.0, &entry.1)?;
let ours_has_file = eff_ours.contains_key(&original);
let theirs_has_file = eff_theirs.contains_key(&original);
let from_ours = ours_has_file || !theirs_has_file;
let branch = if from_ours {
options.ours_label
} else {
options.theirs_label
};
let new_path = unique_df_path(&original, branch, leaves, paths);
leaves.insert(new_path.clone(), entry);
*clean = false;
if let Some(slot) = paths.iter_mut().find(|p| p.path == original) {
if let Some(MergeConflictKind::RenameDelete {
old_path,
renamed_in,
deleted_in,
}) = &slot.conflict
{
info_messages.push(MergeInfoMessage::RenameDeleteConflict {
old_path: old_path.clone(),
new_path: original.clone(),
renamed_in: renamed_in.clone(),
deleted_in: deleted_in.clone(),
});
}
slot.path = new_path.clone();
slot.result = Some(entry);
if slot.stages.base.is_none()
&& slot.stages.ours.is_none()
&& slot.stages.theirs.is_none()
{
slot.stages = MergeStages {
base: None,
ours: if from_ours { Some(entry) } else { None },
theirs: if from_ours { None } else { Some(entry) },
};
}
slot.worktree = Some((entry.0, moved_bytes));
slot.conflict = Some(MergeConflictKind::FileDirectory {
original_path: original.clone(),
moved_from: branch.to_string(),
});
} else {
paths.push(MergedPath {
path: new_path.clone(),
stages: MergeStages {
base: None,
ours: if from_ours { Some(entry) } else { None },
theirs: if from_ours { None } else { Some(entry) },
},
result: Some(entry),
worktree: Some((entry.0, moved_bytes)),
conflict: Some(MergeConflictKind::FileDirectory {
original_path: original.clone(),
moved_from: branch.to_string(),
}),
auto_merged: false,
});
}
}
paths.sort_by(|a, b| a.path.cmp(&b.path));
Ok(())
}
fn clean_path(path: Vec<u8>, result: Option<(u32, ObjectId)>) -> MergedPath {
clean_path_auto(path, result, false)
}
fn clean_path_auto(
path: Vec<u8>,
result: Option<(u32, ObjectId)>,
auto_merged: bool,
) -> MergedPath {
MergedPath {
path,
stages: MergeStages::default(),
result,
worktree: None,
conflict: None,
auto_merged,
}
}
fn stages_for(
base: &Option<(u32, ObjectId)>,
ours: &Option<(u32, ObjectId)>,
theirs: &Option<(u32, ObjectId)>,
) -> MergeStages {
MergeStages {
base: *base,
ours: *ours,
theirs: *theirs,
}
}
fn merge_blob_bytes(reader: &impl ObjectReader, oid: &ObjectId) -> Result<Vec<u8>> {
let object = reader.read_object(oid)?;
if object.object_type != ObjectType::Blob {
return Err(GitError::InvalidObject(format!(
"expected blob {}, found {}",
oid,
object.object_type.as_str()
)));
}
Ok(object.body.clone())
}
fn merge_worktree_bytes(reader: &impl ObjectReader, mode: u32, oid: &ObjectId) -> Result<Vec<u8>> {
if sley_index::is_gitlink(mode) {
Ok(Vec::new())
} else {
merge_blob_bytes(reader, oid)
}
}
fn merge_file_modes(base: Option<u32>, ours: u32, theirs: u32) -> (u32, bool) {
if ours == theirs {
return (ours, false);
}
match base {
Some(base) if ours == base => (theirs, false),
Some(base) if theirs == base => (ours, false),
_ => (ours, true),
}
}
fn write_merged_tree(db: &FileObjectDatabase, leaves: &MergeEntryMap) -> Result<ObjectId> {
let mut root = MergeTreeNode::default();
for (path, (mode, oid)) in leaves {
root.insert(path, *mode, *oid);
}
root.write(db)
}
#[derive(Default)]
struct MergeTreeNode {
blobs: BTreeMap<Vec<u8>, (u32, ObjectId)>,
subtrees: BTreeMap<Vec<u8>, MergeTreeNode>,
}
impl MergeTreeNode {
fn insert(&mut self, path: &[u8], mode: u32, oid: ObjectId) {
match path.iter().position(|byte| *byte == b'/') {
Some(slash) => {
let component = path[..slash].to_vec();
let rest = &path[slash + 1..];
self.subtrees
.entry(component)
.or_default()
.insert(rest, mode, oid);
}
None => {
self.blobs.insert(path.to_vec(), (mode, oid));
}
}
}
fn write(&self, db: &FileObjectDatabase) -> Result<ObjectId> {
let mut entries: Vec<TreeEntry> = Vec::new();
for (name, (mode, oid)) in &self.blobs {
entries.push(TreeEntry {
mode: *mode,
name: BString::from(name.clone()),
oid: *oid,
});
}
for (name, subtree) in &self.subtrees {
let oid = subtree.write(db)?;
entries.push(TreeEntry {
mode: 0o040000,
name: BString::from(name.clone()),
oid,
});
}
entries.sort_by_key(merge_tree_sort_key);
let tree = Tree { entries };
db.write_object(EncodedObject::new(ObjectType::Tree, tree.write()))
}
}
fn merge_tree_sort_key(entry: &TreeEntry) -> Vec<u8> {
let mut key = entry.name.as_bytes().to_vec();
if entry.mode == 0o040000 {
key.push(b'/');
}
key
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum RenameSide {
Ours,
Theirs,
}
#[derive(Clone)]
struct MergeRename {
source: Vec<u8>,
side: RenameSide,
}
#[derive(Clone)]
struct RenameDelete {
source: Vec<u8>,
side: RenameSide,
}
#[derive(Default)]
struct MergeRenames {
dest_to_source: BTreeMap<Vec<u8>, MergeRename>,
rename_deletes: BTreeMap<Vec<u8>, RenameDelete>,
rename_rename_one_to_two: BTreeMap<Vec<u8>, RenameRenameOneToTwo>,
rename_rename_two_to_one: BTreeMap<Vec<u8>, RenameRenameTwoToOne>,
rename_adds: BTreeMap<Vec<u8>, RenameAdd>,
}
#[derive(Clone)]
struct RenameRenameOneToTwo {
ours_dest: Vec<u8>,
theirs_dest: Vec<u8>,
}
#[derive(Clone)]
struct RenameRenameTwoToOne {
ours_source: Vec<u8>,
theirs_source: Vec<u8>,
}
#[derive(Clone)]
struct RenameAdd {
source: Vec<u8>,
side: RenameSide,
}
struct SideRenames {
pairs: Vec<(Vec<u8>, Vec<u8>)>,
}
fn detect_merge_renames(
db: &FileObjectDatabase,
format: ObjectFormat,
base_map: &MergeEntryMap,
ours_map: &MergeEntryMap,
theirs_map: &MergeEntryMap,
options: &MergeTreesOptions<'_>,
) -> Result<(MergeRenames, SideRenames, SideRenames)> {
let mut renames = MergeRenames::default();
let ours_side = collect_side_renames(
db,
format,
base_map,
ours_map,
theirs_map,
RenameSide::Ours,
options.rename_threshold,
options.rename_limit,
&mut renames,
)?;
let theirs_side = collect_side_renames(
db,
format,
base_map,
theirs_map,
ours_map,
RenameSide::Theirs,
options.rename_threshold,
options.rename_limit,
&mut renames,
)?;
collect_rename_rename_one_to_two(&mut renames, &ours_side, &theirs_side);
collect_rename_rename_two_to_one_and_adds(
&mut renames,
&ours_side,
&theirs_side,
base_map,
ours_map,
theirs_map,
);
Ok((renames, ours_side, theirs_side))
}
fn collect_rename_rename_two_to_one_and_adds(
renames: &mut MergeRenames,
ours_side: &SideRenames,
theirs_side: &SideRenames,
base_map: &MergeEntryMap,
ours_map: &MergeEntryMap,
theirs_map: &MergeEntryMap,
) {
let ours_by_dest: BTreeMap<&[u8], &[u8]> = ours_side
.pairs
.iter()
.map(|(old, new)| (new.as_slice(), old.as_slice()))
.collect();
let theirs_by_dest: BTreeMap<&[u8], &[u8]> = theirs_side
.pairs
.iter()
.map(|(old, new)| (new.as_slice(), old.as_slice()))
.collect();
for (dest, ours_src) in &ours_by_dest {
let Some(theirs_src) = theirs_by_dest.get(dest) else {
continue;
};
if ours_src == theirs_src {
continue;
}
if renames.rename_rename_one_to_two.contains_key(*dest) {
continue;
}
renames.rename_rename_two_to_one.insert(
dest.to_vec(),
RenameRenameTwoToOne {
ours_source: ours_src.to_vec(),
theirs_source: theirs_src.to_vec(),
},
);
}
for (dest, ours_src) in &ours_by_dest {
if renames.rename_rename_two_to_one.contains_key(*dest)
|| renames.rename_rename_one_to_two.contains_key(*dest)
{
continue;
}
if theirs_map.contains_key(*dest)
&& !base_map.contains_key(*dest)
&& !theirs_by_dest.contains_key(dest)
{
renames.rename_adds.insert(
dest.to_vec(),
RenameAdd {
source: ours_src.to_vec(),
side: RenameSide::Ours,
},
);
}
}
for (dest, theirs_src) in &theirs_by_dest {
if renames.rename_rename_two_to_one.contains_key(*dest)
|| renames.rename_rename_one_to_two.contains_key(*dest)
|| renames.rename_adds.contains_key(*dest)
{
continue;
}
if ours_map.contains_key(*dest)
&& !base_map.contains_key(*dest)
&& !ours_by_dest.contains_key(dest)
{
renames.rename_adds.insert(
dest.to_vec(),
RenameAdd {
source: theirs_src.to_vec(),
side: RenameSide::Theirs,
},
);
}
}
}
fn collect_rename_rename_one_to_two(
renames: &mut MergeRenames,
ours_side: &SideRenames,
theirs_side: &SideRenames,
) {
let ours_by_source: BTreeMap<&[u8], &[u8]> = ours_side
.pairs
.iter()
.map(|(old, new)| (old.as_slice(), new.as_slice()))
.collect();
for (old, theirs_new) in &theirs_side.pairs {
let Some(ours_new) = ours_by_source.get(old.as_slice()) else {
continue;
};
if *ours_new == theirs_new.as_slice() {
continue;
}
renames.rename_deletes.remove(*ours_new);
renames.rename_deletes.remove(theirs_new);
renames.dest_to_source.remove(*ours_new);
renames.dest_to_source.remove(theirs_new);
renames.rename_rename_one_to_two.insert(
old.clone(),
RenameRenameOneToTwo {
ours_dest: (*ours_new).to_vec(),
theirs_dest: theirs_new.clone(),
},
);
}
}
#[allow(clippy::too_many_arguments)]
fn collect_side_renames(
db: &FileObjectDatabase,
format: ObjectFormat,
base_map: &MergeEntryMap,
side_map: &MergeEntryMap,
other_map: &MergeEntryMap,
side: RenameSide,
threshold: u8,
rename_limit: usize,
renames: &mut MergeRenames,
) -> Result<SideRenames> {
let base_tree = entry_map_as_tracked(base_map);
let side_tree = entry_map_as_tracked(side_map);
let options = DiffNameStatusOptions {
detect_renames: true,
detect_copies: false,
find_copies_harder: false,
rename_empty: false,
detect_inexact: true,
rename_threshold: threshold,
copy_threshold: threshold,
rename_limit,
..Default::default()
};
let changes = diff_name_status_maps_with_renames(
&base_tree,
&side_tree,
base_tree.keys().chain(side_tree.keys()),
options,
|oid| merge_blob_bytes(db, oid).ok().map(|b| Arc::from(b.into_boxed_slice())),
)?;
let mut pairs = Vec::new();
for change in changes {
let NameStatus::Renamed(_) = change.status else {
continue;
};
let Some(old_path) = change.old_path.as_ref() else {
continue;
};
let old = old_path.as_bytes().to_vec();
let new = change.path.as_bytes().to_vec();
pairs.push((old.clone(), new.clone()));
if !other_map.contains_key(&old) {
if base_map.contains_key(&old) && !other_map.contains_key(&new) {
renames
.rename_deletes
.entry(new.clone())
.or_insert(RenameDelete {
source: old.clone(),
side,
});
}
continue;
}
if other_map.contains_key(&new) {
continue;
}
renames
.dest_to_source
.entry(new)
.or_insert(MergeRename { source: old, side });
}
let _ = format;
Ok(SideRenames { pairs })
}
fn apply_merge_renames(
base_map: &MergeEntryMap,
ours_map: &MergeEntryMap,
theirs_map: &MergeEntryMap,
renames: &MergeRenames,
) -> (MergeEntryMap, MergeEntryMap, MergeEntryMap) {
if renames.dest_to_source.is_empty() {
return (base_map.clone(), ours_map.clone(), theirs_map.clone());
}
let mut base = base_map.clone();
let mut ours = ours_map.clone();
let mut theirs = theirs_map.clone();
for (new, rename) in &renames.dest_to_source {
let old = &rename.source;
if let Some(entry) = base.remove(old) {
base.entry(new.clone()).or_insert(entry);
}
for side in [&mut ours, &mut theirs] {
if let Some(entry) = side.remove(old) {
side.entry(new.clone()).or_insert(entry);
}
}
}
(base, ours, theirs)
}
fn parent_dir(path: &[u8]) -> Option<&[u8]> {
path.iter().rposition(|b| *b == b'/').map(|i| &path[..i])
}
fn apply_dir_rename(old_dir: &[u8], new_dir: &[u8], path: &[u8]) -> Vec<u8> {
let rest_start = if new_dir.is_empty() {
old_dir.len() + 1
} else {
old_dir.len()
};
let mut out = new_dir.to_vec();
out.extend_from_slice(&path[rest_start..]);
out
}
fn check_dir_renamed<'a>(
path: &[u8],
dir_renames: &'a BTreeMap<Vec<u8>, Vec<u8>>,
) -> Option<(&'a [u8], &'a [u8])> {
let mut cur = parent_dir(path);
while let Some(dir) = cur {
if let Some((old_dir, new_dir)) = dir_renames.get_key_value(dir) {
return Some((old_dir.as_slice(), new_dir.as_slice()));
}
cur = parent_dir(dir);
}
None
}
struct DirectoryRenameMaps {
ours: BTreeMap<Vec<u8>, Vec<u8>>,
theirs: BTreeMap<Vec<u8>, Vec<u8>>,
ours_split: BTreeSet<Vec<u8>>,
theirs_split: BTreeSet<Vec<u8>>,
}
fn compute_directory_renames(
ours_map: &MergeEntryMap,
theirs_map: &MergeEntryMap,
ours_side: &SideRenames,
theirs_side: &SideRenames,
) -> DirectoryRenameMaps {
let ours = compute_side_dir_renames(&ours_side.pairs, ours_map);
let theirs = compute_side_dir_renames(&theirs_side.pairs, theirs_map);
let mut ours_map_out = ours.renames;
let mut theirs_map_out = theirs.renames;
let dup: Vec<Vec<u8>> = ours_map_out
.keys()
.filter(|k| theirs_map_out.contains_key(*k))
.cloned()
.collect();
for k in dup {
ours_map_out.remove(&k);
theirs_map_out.remove(&k);
}
DirectoryRenameMaps {
ours: ours_map_out,
theirs: theirs_map_out,
ours_split: ours.split,
theirs_split: theirs.split,
}
}
struct SideDirRenames {
renames: BTreeMap<Vec<u8>, Vec<u8>>,
split: BTreeSet<Vec<u8>>,
}
fn compute_side_dir_renames(
pairs: &[(Vec<u8>, Vec<u8>)],
side_map: &MergeEntryMap,
) -> SideDirRenames {
let mut counts: BTreeMap<Vec<u8>, BTreeMap<Vec<u8>, usize>> = BTreeMap::new();
for (old, new) in pairs {
update_dir_rename_counts(&mut counts, old, new);
}
let mut renames = BTreeMap::new();
let mut split = BTreeSet::new();
for (old_dir, targets) in counts {
let mut max = 0usize;
let mut bad_max = 0usize;
let mut best: Option<Vec<u8>> = None;
for (target, count) in &targets {
if *count == max {
bad_max = max;
} else if *count > max {
max = *count;
best = Some(target.clone());
}
}
if max == 0 {
continue;
}
if bad_max == max {
split.insert(old_dir);
continue;
}
if let Some(best) = best
&& directory_fully_removed(&old_dir, side_map)
{
renames.insert(old_dir, best);
}
}
SideDirRenames { renames, split }
}
fn update_dir_rename_counts(
counts: &mut BTreeMap<Vec<u8>, BTreeMap<Vec<u8>, usize>>,
old: &[u8],
new: &[u8],
) {
let mut old_dir = old.to_vec();
let mut new_dir = new.to_vec();
let mut first = true;
loop {
let old_has = dir_munge(&mut old_dir);
let new_has = dir_munge(&mut new_dir);
if !first {
let old_sub = trailing_component(old, &old_dir);
let new_sub = trailing_component(new, &new_dir);
if old_sub != new_sub {
break;
}
}
if old_dir == new_dir {
break;
}
*counts
.entry(old_dir.clone())
.or_default()
.entry(new_dir.clone())
.or_default() += 1;
first = false;
if old_dir.is_empty() || new_dir.is_empty() {
break;
}
if !old_has || !new_has {
break;
}
}
}
fn dir_munge(buf: &mut Vec<u8>) -> bool {
match buf.iter().rposition(|b| *b == b'/') {
Some(i) => {
buf.truncate(i);
true
}
None => {
buf.clear();
false
}
}
}
fn trailing_component<'a>(full: &'a [u8], dir: &[u8]) -> &'a [u8] {
if dir.is_empty() {
full
} else {
&full[dir.len() + 1..]
}
}
fn directory_fully_removed(dir: &[u8], side_map: &MergeEntryMap) -> bool {
let mut prefix = dir.to_vec();
prefix.push(b'/');
for path in side_map.keys() {
if path.starts_with(&prefix) {
return false;
}
}
true
}
struct DirRenameMove {
from: Vec<u8>,
to: Vec<u8>,
renamed_from: Option<Vec<u8>>,
}
struct DirRenameTwoToOne {
dest: Vec<u8>,
ours_source: Vec<u8>,
theirs_source: Vec<u8>,
ours_label_path: Vec<u8>,
theirs_label_path: Vec<u8>,
}
#[derive(Clone)]
struct RehomeInfo {
old_path: Vec<u8>,
renamed_from: Option<Vec<u8>>,
added_on_ours: bool,
}
#[derive(Clone, Default)]
struct RehomeSides {
ours: Option<RehomeInfo>,
theirs: Option<RehomeInfo>,
}
struct DirRenameCollision {
dest: Vec<u8>,
sources: Vec<Vec<u8>>,
}
struct DirRenameOutcome {
base: MergeEntryMap,
ours: MergeEntryMap,
theirs: MergeEntryMap,
rehomed: BTreeMap<Vec<u8>, RehomeSides>,
collisions: Vec<DirRenameCollision>,
splits: BTreeSet<Vec<u8>>,
back_to_self: BTreeSet<Vec<u8>>,
dirty: bool,
info_messages: Vec<MergeInfoMessage>,
}
#[allow(clippy::too_many_arguments)]
fn apply_directory_renames(
base_map: &MergeEntryMap,
eff_base: &MergeEntryMap,
eff_ours: &MergeEntryMap,
eff_theirs: &MergeEntryMap,
ours_side: &SideRenames,
theirs_side: &SideRenames,
dir_renames: &DirectoryRenameMaps,
file_rename_dests: &BTreeMap<Vec<u8>, MergeRename>,
) -> DirRenameOutcome {
let mut base = eff_base.clone();
let mut ours = eff_ours.clone();
let mut theirs = eff_theirs.clone();
let mut rehomed = BTreeMap::new();
let mut collisions = Vec::new();
let mut splits = BTreeSet::new();
let mut back_to_self = BTreeSet::new();
let mut info_messages = Vec::new();
let mut dirty = false;
let ours_excl = exclusion_dirs(&dir_renames.ours);
let theirs_excl = exclusion_dirs(&dir_renames.theirs);
let ours_moves = plan_rehome(
base_map,
&ours,
ours_side,
&dir_renames.theirs,
&ours_excl,
&dir_renames.theirs_split,
&mut collisions,
&mut splits,
&mut info_messages,
&mut dirty,
);
let theirs_moves = plan_rehome(
base_map,
&theirs,
theirs_side,
&dir_renames.ours,
&theirs_excl,
&dir_renames.ours_split,
&mut collisions,
&mut splits,
&mut info_messages,
&mut dirty,
);
apply_rehome_moves(
base_map,
file_rename_dests,
&mut base,
&mut ours,
&mut theirs,
ours_moves,
true,
&mut rehomed,
&mut collisions,
&mut back_to_self,
&mut dirty,
);
apply_rehome_moves(
base_map,
file_rename_dests,
&mut base,
&mut ours,
&mut theirs,
theirs_moves,
false,
&mut rehomed,
&mut collisions,
&mut back_to_self,
&mut dirty,
);
DirRenameOutcome {
base,
ours,
theirs,
rehomed,
collisions,
splits,
back_to_self,
dirty,
info_messages,
}
}
fn exclusion_dirs(side_dir_renames: &BTreeMap<Vec<u8>, Vec<u8>>) -> BTreeSet<Vec<u8>> {
side_dir_renames.keys().cloned().collect()
}
#[allow(clippy::too_many_arguments)]
fn plan_rehome(
base_map: &MergeEntryMap,
side: &MergeEntryMap,
side_renames: &SideRenames,
renamer_dirs: &BTreeMap<Vec<u8>, Vec<u8>>,
exclusions: &BTreeSet<Vec<u8>>,
split_dirs: &BTreeSet<Vec<u8>>,
collisions: &mut Vec<DirRenameCollision>,
splits: &mut BTreeSet<Vec<u8>>,
info_messages: &mut Vec<MergeInfoMessage>,
dirty: &mut bool,
) -> Vec<DirRenameMove> {
if renamer_dirs.is_empty() && split_dirs.is_empty() {
return Vec::new();
}
let side_rename_src: BTreeMap<&[u8], &[u8]> = side_renames
.pairs
.iter()
.map(|(o, n)| (n.as_slice(), o.as_slice()))
.collect();
let candidates: Vec<Vec<u8>> = side
.keys()
.filter(|p| !base_map.contains_key(*p) || side_rename_src.contains_key(p.as_slice()))
.cloned()
.collect();
let mut planned: BTreeMap<Vec<u8>, Vec<DirRenameMove>> = BTreeMap::new();
for path in candidates {
if let Some(split_dir) = check_dir_split(&path, split_dirs) {
splits.insert(split_dir.to_vec());
*dirty = true;
continue;
}
let Some((old_dir, new_dir)) = check_dir_renamed(&path, renamer_dirs) else {
continue;
};
let new_dir_is_exclusion = exclusions.contains(new_dir);
let new_dir_inside_exclusion = exclusions
.iter()
.any(|dir| directory_contains_proper(dir, new_dir));
if new_dir_is_exclusion
|| (new_dir_inside_exclusion
&& !side_has_pure_add_under_dir(side, base_map, &side_rename_src, old_dir))
{
info_messages.push(MergeInfoMessage::DirRenameSkippedDueToRerename {
old_dir: old_dir.to_vec(),
path: path.clone(),
new_dir: new_dir.to_vec(),
});
continue;
}
let dest = apply_dir_rename(old_dir, new_dir, &path);
if dest == path {
continue;
}
let renamed_from = side_rename_src.get(path.as_slice()).map(|s| s.to_vec());
planned
.entry(dest.clone())
.or_default()
.push(DirRenameMove {
from: path,
to: dest,
renamed_from,
});
}
let mut moves = Vec::new();
for (dest, group) in planned {
if group.len() > 1 {
*dirty = true;
collisions.push(DirRenameCollision {
dest,
sources: group.into_iter().map(|m| m.from).collect(),
});
continue;
}
moves.push(group.into_iter().next().expect("non-empty"));
}
moves
}
fn check_dir_split<'a>(path: &[u8], split_dirs: &'a BTreeSet<Vec<u8>>) -> Option<&'a [u8]> {
let mut dir = parent_dir(path)?;
loop {
if let Some(split_dir) = split_dirs.get(dir) {
return Some(split_dir);
}
dir = parent_dir(dir)?;
}
}
fn directory_contains_proper(parent: &[u8], child: &[u8]) -> bool {
!parent.is_empty()
&& child.len() > parent.len()
&& child.starts_with(parent)
&& child[parent.len()] == b'/'
}
fn side_has_pure_add_under_dir(
side: &MergeEntryMap,
base_map: &MergeEntryMap,
side_rename_src: &BTreeMap<&[u8], &[u8]>,
dir: &[u8],
) -> bool {
side.keys().any(|path| {
path_is_under_dir(path, dir)
&& !base_map.contains_key(path)
&& !side_rename_src.contains_key(path.as_slice())
})
}
fn path_is_under_dir(path: &[u8], dir: &[u8]) -> bool {
!dir.is_empty() && path.len() > dir.len() && path.starts_with(dir) && path[dir.len()] == b'/'
}
#[allow(clippy::too_many_arguments)]
fn apply_rehome_moves(
original_base: &MergeEntryMap,
file_rename_dests: &BTreeMap<Vec<u8>, MergeRename>,
base: &mut MergeEntryMap,
ours: &mut MergeEntryMap,
theirs: &mut MergeEntryMap,
moves: Vec<DirRenameMove>,
side_is_ours: bool,
rehomed: &mut BTreeMap<Vec<u8>, RehomeSides>,
collisions: &mut Vec<DirRenameCollision>,
back_to_self: &mut BTreeSet<Vec<u8>>,
dirty: &mut bool,
) {
for mv in moves {
let occupied_on_this_side = if side_is_ours {
ours.contains_key(&mv.to) || map_has_directory_at(ours, &mv.to)
} else {
theirs.contains_key(&mv.to) || map_has_directory_at(theirs, &mv.to)
};
let occupied_by_cross_rename =
file_rename_dests
.get(&mv.to)
.is_some_and(|rename| match (side_is_ours, rename.side) {
(true, RenameSide::Theirs) | (false, RenameSide::Ours) => true,
(true, RenameSide::Ours) | (false, RenameSide::Theirs) => false,
});
let base_entry_at_dest = original_base.get(&mv.to).copied();
let base_entry_at_source = original_base.get(&mv.from).copied();
let other_side_entry_at_dest = if side_is_ours {
theirs.get(&mv.to).copied()
} else {
ours.get(&mv.to).copied()
};
let other_side_entry_at_source = if side_is_ours {
theirs.get(&mv.from).copied()
} else {
ours.get(&mv.from).copied()
};
let base_entry_for_shifted_source = base_entry_at_source.or(base_entry_at_dest);
let rename_back_to_modified_source = mv
.renamed_from
.as_ref()
.is_some_and(|source| source == &mv.to)
&& base_entry_at_dest.is_some()
&& (other_side_entry_at_dest.is_some_and(|entry| Some(entry) != base_entry_at_dest)
|| other_side_entry_at_source
.is_some_and(|entry| Some(entry) != base_entry_for_shifted_source));
if ((base_entry_at_dest.is_some() && !rename_back_to_modified_source)
|| (occupied_on_this_side && !occupied_by_cross_rename))
&& mv.to != mv.from
{
*dirty = true;
collisions.push(DirRenameCollision {
dest: mv.to.clone(),
sources: vec![mv.from.clone()],
});
continue;
}
let mut moved = false;
if occupied_by_cross_rename {
base.remove(&mv.from);
if side_is_ours {
if let Some(entry) = ours.remove(&mv.from) {
ours.insert(mv.to.clone(), entry);
moved = true;
}
theirs.remove(&mv.from);
} else {
ours.remove(&mv.from);
if let Some(entry) = theirs.remove(&mv.from) {
theirs.insert(mv.to.clone(), entry);
moved = true;
}
}
} else {
for m in [&mut *base, &mut *ours, &mut *theirs] {
if let Some(entry) = m.remove(&mv.from) {
m.insert(mv.to.clone(), entry);
moved = true;
}
}
}
if moved {
if rename_back_to_modified_source {
back_to_self.insert(mv.to.clone());
}
let info = RehomeInfo {
old_path: mv.from.clone(),
renamed_from: mv.renamed_from.clone(),
added_on_ours: side_is_ours,
};
let entry = rehomed.entry(mv.to.clone()).or_default();
if side_is_ours {
entry.ours = Some(info);
} else {
entry.theirs = Some(info);
}
}
}
}
fn collect_dir_rename_two_to_one(
renames: &MergeRenames,
rehomed: &BTreeMap<Vec<u8>, RehomeSides>,
) -> Vec<DirRenameTwoToOne> {
let mut conflicts = Vec::new();
for (dest, sides) in rehomed {
let Some(file_rename) = renames.dest_to_source.get(dest) else {
continue;
};
match file_rename.side {
RenameSide::Ours => {
let Some(info) = sides.theirs.as_ref() else {
continue;
};
let Some(theirs_source) = info.renamed_from.as_ref() else {
continue;
};
conflicts.push(DirRenameTwoToOne {
dest: dest.clone(),
ours_source: file_rename.source.clone(),
theirs_source: theirs_source.clone(),
ours_label_path: dest.clone(),
theirs_label_path: info.old_path.clone(),
});
}
RenameSide::Theirs => {
let Some(info) = sides.ours.as_ref() else {
continue;
};
let Some(ours_source) = info.renamed_from.as_ref() else {
continue;
};
conflicts.push(DirRenameTwoToOne {
dest: dest.clone(),
ours_source: ours_source.clone(),
theirs_source: file_rename.source.clone(),
ours_label_path: info.old_path.clone(),
theirs_label_path: dest.clone(),
});
}
}
}
conflicts
}
fn map_has_directory_at(map: &MergeEntryMap, path: &[u8]) -> bool {
let mut prefix = path.to_vec();
prefix.push(b'/');
map.keys().any(|candidate| candidate.starts_with(&prefix))
}
fn remap_rename_destinations(renames: &mut MergeRenames, rehomed: &BTreeMap<Vec<u8>, RehomeSides>) {
if rehomed.is_empty() {
return;
}
let mut remapped_deletes = BTreeMap::new();
for (dest, rd) in std::mem::take(&mut renames.rename_deletes) {
let new_dest = rehomed
.iter()
.find_map(|(new_dest, sides)| {
let moved = sides
.ours
.as_ref()
.is_some_and(|info| info.old_path == dest)
|| sides
.theirs
.as_ref()
.is_some_and(|info| info.old_path == dest);
moved.then(|| new_dest.clone())
})
.unwrap_or(dest);
remapped_deletes.insert(new_dest, rd);
}
renames.rename_deletes = remapped_deletes;
for rename in renames.rename_rename_one_to_two.values_mut() {
for (dest, sides) in rehomed {
if sides
.ours
.as_ref()
.is_some_and(|info| info.old_path == rename.ours_dest)
{
rename.ours_dest = dest.clone();
}
if sides
.theirs
.as_ref()
.is_some_and(|info| info.old_path == rename.theirs_dest)
{
rename.theirs_dest = dest.clone();
}
}
}
}
fn drop_collapsed_rename_rename_conflicts(renames: &mut MergeRenames) {
renames
.rename_rename_one_to_two
.retain(|_, rename| rename.ours_dest != rename.theirs_dest);
}
fn apply_dir_rename_two_to_one_conflicts(
db: &FileObjectDatabase,
eff_ours: &MergeEntryMap,
eff_theirs: &MergeEntryMap,
conflicts: &[DirRenameTwoToOne],
paths: &mut [MergedPath],
leaves: &mut MergeEntryMap,
options: &MergeTreesOptions<'_>,
) -> Result<()> {
for conflict in conflicts {
let Some(slot) = paths.iter_mut().find(|path| path.path == conflict.dest) else {
continue;
};
let ours_entry = eff_ours.get(&conflict.dest).copied();
let theirs_entry = eff_theirs.get(&conflict.dest).copied();
let (Some((ours_mode, ours_oid)), Some((theirs_mode, theirs_oid))) =
(ours_entry, theirs_entry)
else {
continue;
};
let ours_bytes = merge_blob_bytes(db, &ours_oid)?;
let theirs_bytes = merge_blob_bytes(db, &theirs_oid)?;
let (resolved_mode, mode_conflict) = merge_file_modes(None, ours_mode, theirs_mode);
let favor = merge_favor_for_path(options, &conflict.dest);
let result = if is_mergeable_file_mode(ours_mode) && is_mergeable_file_mode(theirs_mode) {
merge_blobs(
&[],
&ours_bytes,
&theirs_bytes,
&MergeBlobOptions {
ours_label: &qualify_label(options.ours_label, &conflict.ours_label_path),
theirs_label: &qualify_label(options.theirs_label, &conflict.theirs_label_path),
base_label: options.ancestor_label,
style: options.style,
favor,
ws_ignore: options.ws_ignore,
marker_size: merge_marker_size_for_path(options, &conflict.dest),
},
)
} else {
MergeBlobResult {
content: ours_bytes.clone(),
conflicted: true,
}
};
let oid = db.write_object(EncodedObject::new(ObjectType::Blob, result.content.clone()))?;
leaves.insert(conflict.dest.clone(), (resolved_mode, oid));
slot.stages = MergeStages {
base: None,
ours: ours_entry,
theirs: theirs_entry,
};
slot.result = Some((resolved_mode, oid));
slot.worktree = Some((
if ours_mode == theirs_mode {
ours_mode
} else {
0o100644
},
result.content,
));
slot.conflict = Some(MergeConflictKind::RenameRenameTwoToOne {
ours_path: conflict.ours_source.clone(),
theirs_path: conflict.theirs_source.clone(),
});
slot.auto_merged = !mode_conflict;
}
Ok(())
}
fn rename_merged_leaf(
db: &FileObjectDatabase,
base: Option<(u32, ObjectId)>,
ours: Option<(u32, ObjectId)>,
theirs: Option<(u32, ObjectId)>,
path: &[u8],
options: &MergeTreesOptions<'_>,
) -> Result<Option<(u32, ObjectId)>> {
match (ours, theirs) {
(None, None) => Ok(None),
(Some(entry), None) | (None, Some(entry)) => Ok(Some(entry)),
(Some((ours_mode, ours_oid)), Some((theirs_mode, theirs_oid))) => {
if (ours_mode, ours_oid) == (theirs_mode, theirs_oid) {
return Ok(Some((ours_mode, ours_oid)));
}
if !is_mergeable_file_mode(ours_mode) || !is_mergeable_file_mode(theirs_mode) {
return Ok(Some((ours_mode, ours_oid)));
}
let base_bytes = match base {
Some((_, oid)) => merge_blob_bytes(db, &oid)?,
None => Vec::new(),
};
let favor = merge_favor_for_path(options, path);
let result = merge_blobs(
&base_bytes,
&merge_blob_bytes(db, &ours_oid)?,
&merge_blob_bytes(db, &theirs_oid)?,
&MergeBlobOptions {
ours_label: options.ours_label,
theirs_label: options.theirs_label,
base_label: options.ancestor_label,
style: options.style,
favor,
ws_ignore: options.ws_ignore,
marker_size: merge_marker_size_for_path(options, path),
},
);
let (mode, _) = merge_file_modes(base.map(|(mode, _)| mode), ours_mode, theirs_mode);
let oid = db.write_object(EncodedObject::new(ObjectType::Blob, result.content))?;
Ok(Some((mode, oid)))
}
}
}
#[allow(clippy::too_many_arguments)]
fn apply_rename_two_to_one_and_add_conflicts(
db: &FileObjectDatabase,
base_map: &MergeEntryMap,
ours_map: &MergeEntryMap,
theirs_map: &MergeEntryMap,
renames: &MergeRenames,
paths: &mut Vec<MergedPath>,
leaves: &mut MergeEntryMap,
options: &MergeTreesOptions<'_>,
) -> Result<()> {
let mut consumed_sources: Vec<Vec<u8>> = Vec::new();
for (dest, conflict) in &renames.rename_rename_two_to_one {
let ours_leaf = rename_merged_leaf(
db,
base_map.get(&conflict.ours_source).copied(),
ours_map.get(dest).copied(),
theirs_map.get(&conflict.ours_source).copied(),
dest,
options,
)?;
let theirs_leaf = rename_merged_leaf(
db,
base_map.get(&conflict.theirs_source).copied(),
ours_map.get(&conflict.theirs_source).copied(),
theirs_map.get(dest).copied(),
dest,
options,
)?;
write_two_sided_dest_conflict(
db,
dest,
ours_leaf,
theirs_leaf,
MergeConflictKind::RenameRenameTwoToOne {
ours_path: conflict.ours_source.clone(),
theirs_path: conflict.theirs_source.clone(),
},
options,
paths,
leaves,
)?;
consumed_sources.push(conflict.ours_source.clone());
consumed_sources.push(conflict.theirs_source.clone());
}
for (dest, add) in &renames.rename_adds {
let (ours_leaf, theirs_leaf) = match add.side {
RenameSide::Ours => (
rename_merged_leaf(
db,
base_map.get(&add.source).copied(),
ours_map.get(dest).copied(),
theirs_map.get(&add.source).copied(),
dest,
options,
)?,
theirs_map.get(dest).copied(),
),
RenameSide::Theirs => (
ours_map.get(dest).copied(),
rename_merged_leaf(
db,
base_map.get(&add.source).copied(),
ours_map.get(&add.source).copied(),
theirs_map.get(dest).copied(),
dest,
options,
)?,
),
};
write_two_sided_dest_conflict(
db,
dest,
ours_leaf,
theirs_leaf,
MergeConflictKind::Content { add_add: true },
options,
paths,
leaves,
)?;
consumed_sources.push(add.source.clone());
}
for source in &consumed_sources {
leaves.remove(source);
if let Some(slot) = paths.iter_mut().find(|path| &path.path == source) {
slot.stages = MergeStages::default();
slot.result = None;
slot.worktree = None;
slot.conflict = None;
slot.auto_merged = false;
} else {
paths.push(MergedPath {
path: source.clone(),
stages: MergeStages::default(),
result: None,
worktree: None,
conflict: None,
auto_merged: false,
});
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn write_two_sided_dest_conflict(
db: &FileObjectDatabase,
dest: &[u8],
ours_leaf: Option<(u32, ObjectId)>,
theirs_leaf: Option<(u32, ObjectId)>,
kind: MergeConflictKind,
options: &MergeTreesOptions<'_>,
paths: &mut Vec<MergedPath>,
leaves: &mut MergeEntryMap,
) -> Result<()> {
let ours_bytes = match ours_leaf {
Some((mode, oid)) => Some((mode, merge_worktree_bytes(db, mode, &oid)?)),
None => None,
};
let theirs_bytes = match theirs_leaf {
Some((mode, oid)) => Some((mode, merge_worktree_bytes(db, mode, &oid)?)),
None => None,
};
let (worktree_mode, worktree_content, result_leaf) = match (&ours_bytes, &theirs_bytes) {
(Some((ours_mode, ours_content)), Some((theirs_mode, theirs_content))) => {
let favor = merge_favor_for_path(options, dest);
let merged = merge_blobs(
&[],
ours_content,
theirs_content,
&MergeBlobOptions {
ours_label: options.ours_label,
theirs_label: options.theirs_label,
base_label: options.ancestor_label,
style: options.style,
favor,
ws_ignore: options.ws_ignore,
marker_size: merge_marker_size_for_path(options, dest),
},
);
let mode = if ours_mode == theirs_mode {
*ours_mode
} else {
0o100644
};
let oid =
db.write_object(EncodedObject::new(ObjectType::Blob, merged.content.clone()))?;
(mode, merged.content, Some((mode, oid)))
}
(Some((mode, content)), None) | (None, Some((mode, content))) => {
(*mode, content.clone(), ours_leaf.or(theirs_leaf))
}
(None, None) => (0o100644, Vec::new(), None),
};
let slot = MergedPath {
path: dest.to_vec(),
stages: MergeStages {
base: None,
ours: ours_leaf,
theirs: theirs_leaf,
},
result: result_leaf,
worktree: Some((worktree_mode, worktree_content)),
conflict: Some(kind),
auto_merged: true,
};
if let Some(existing) = paths.iter_mut().find(|path| path.path == dest) {
*existing = slot;
} else {
paths.push(slot);
}
if let Some(leaf) = result_leaf {
leaves.insert(dest.to_vec(), leaf);
} else {
leaves.remove(dest);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn apply_rename_rename_one_to_two_conflicts(
db: &FileObjectDatabase,
base_map: &MergeEntryMap,
eff_ours: &MergeEntryMap,
eff_theirs: &MergeEntryMap,
conflicts: &BTreeMap<Vec<u8>, RenameRenameOneToTwo>,
paths: &mut Vec<MergedPath>,
leaves: &mut MergeEntryMap,
options: &MergeTreesOptions<'_>,
) -> Result<()> {
for (old_path, conflict) in conflicts {
let base_entry = base_map.get(old_path).copied();
let ours_entry = eff_ours.get(&conflict.ours_dest).copied();
let theirs_entry = eff_theirs.get(&conflict.theirs_dest).copied();
let theirs_add_at_ours_dest = eff_theirs.get(&conflict.ours_dest).copied();
let ours_add_at_theirs_dest = eff_ours.get(&conflict.theirs_dest).copied();
leaves.remove(old_path);
leaves.remove(&conflict.ours_dest);
leaves.remove(&conflict.theirs_dest);
paths.retain(|path| {
path.path != *old_path
&& path.path != conflict.ours_dest
&& path.path != conflict.theirs_dest
});
paths.push(MergedPath {
path: old_path.clone(),
stages: MergeStages {
base: base_entry,
ours: None,
theirs: None,
},
result: None,
worktree: None,
conflict: Some(MergeConflictKind::RenameRenameOneToTwo {
old_path: old_path.clone(),
ours_path: conflict.ours_dest.clone(),
theirs_path: conflict.theirs_dest.clone(),
ours_label: options.ours_label.to_string(),
theirs_label: options.theirs_label.to_string(),
}),
auto_merged: false,
});
let ours_worktree = match ours_entry {
Some((mode, oid)) => Some((mode, merge_worktree_bytes(db, mode, &oid)?)),
None => None,
};
paths.push(MergedPath {
path: conflict.ours_dest.clone(),
stages: MergeStages {
base: None,
ours: ours_entry,
theirs: theirs_add_at_ours_dest,
},
result: None,
worktree: ours_worktree,
conflict: Some(MergeConflictKind::RenameRenameOneToTwoStage),
auto_merged: false,
});
let theirs_worktree = match theirs_entry {
Some((mode, oid)) => Some((mode, merge_worktree_bytes(db, mode, &oid)?)),
None => None,
};
paths.push(MergedPath {
path: conflict.theirs_dest.clone(),
stages: MergeStages {
base: None,
ours: ours_add_at_theirs_dest,
theirs: theirs_entry,
},
result: None,
worktree: theirs_worktree,
conflict: Some(MergeConflictKind::RenameRenameOneToTwoStage),
auto_merged: false,
});
}
Ok(())
}
fn qualify_label(label: &str, path: &[u8]) -> String {
format!("{label}:{}", String::from_utf8_lossy(path))
}
fn entry_map_as_tracked(map: &MergeEntryMap) -> BTreeMap<Vec<u8>, TrackedEntry> {
map.iter()
.map(|(path, (mode, oid))| {
(
path.clone(),
TrackedEntry {
mode: *mode,
oid: *oid,
},
)
})
.collect()
}
pub fn virtual_ancestor_entry_map(
db: &FileObjectDatabase,
format: ObjectFormat,
bases: &[ObjectId],
merge_bases: impl Fn(&ObjectId, &ObjectId) -> Result<Vec<ObjectId>>,
) -> Result<MergeEntryMap> {
let first = bases
.first()
.ok_or_else(|| GitError::Command("virtual ancestor needs at least one base".into()))?;
let acc_tree = commit_tree_oid(db, format, first)?;
let mut acc_map = flatten_tree(db, format, &acc_tree)?;
let mut acc_commits = vec![*first];
for base in &bases[1..] {
let other_tree = commit_tree_oid(db, format, base)?;
let other_map = flatten_tree(db, format, &other_tree)?;
let sub_bases = merge_bases(&acc_commits[0], base)?;
let sub_base_map = match sub_bases.first() {
Some(sb) => {
let sb_tree = commit_tree_oid(db, format, sb)?;
flatten_tree(db, format, &sb_tree)?
}
None => MergeEntryMap::new(),
};
let merge = merge_entry_maps(
db,
format,
&sub_base_map,
&acc_map,
&other_map,
&MergeTreesOptions {
ours_label: "Temporary merge branch 1",
theirs_label: "Temporary merge branch 2",
detect_renames: true,
rename_threshold: DEFAULT_RENAME_THRESHOLD,
..Default::default()
},
)?;
let mut next = MergeEntryMap::new();
for entry in merge.paths {
if let Some(leaf) = fold_virtual_ancestor_path(db, &entry)? {
next.insert(entry.path, leaf);
}
}
acc_map = next;
acc_commits = vec![*base];
}
Ok(acc_map)
}
fn commit_tree_oid(
db: &FileObjectDatabase,
format: ObjectFormat,
commit_oid: &ObjectId,
) -> Result<ObjectId> {
let object = db.read_object(commit_oid)?;
if object.object_type != ObjectType::Commit {
return Err(GitError::InvalidObject(format!(
"expected commit {commit_oid}, found {}",
object.object_type.as_str()
)));
}
Ok(Commit::parse_ref(format, &object.body)?.tree)
}
fn fold_virtual_ancestor_path(
db: &FileObjectDatabase,
entry: &MergedPath,
) -> Result<Option<(u32, ObjectId)>> {
if entry.conflict.is_none() {
return Ok(entry.result);
}
let base = entry.stages.base;
let ours = entry.stages.ours;
let theirs = entry.stages.theirs;
if let Some((mode, _)) = entry.worktree.as_ref() {
if is_symlink_mode(*mode) {
return Ok(base);
}
if is_gitlink(*mode) {
return Ok(theirs.or(ours));
}
if let Some((mode, bytes)) = &entry.worktree {
let oid = db.write_object(EncodedObject::new(ObjectType::Blob, bytes.clone()))?;
return Ok(Some((*mode, oid)));
}
}
Ok(ours.or(theirs))
}