use super::{collect_children, OutputItem};
use crate::alive::retrieve;
use crate::changestore::ChangeStore;
use crate::fs::{create_new_inode, inode_filename};
use crate::pristine::*;
use crate::small_string::SmallString;
use crate::working_copy::WorkingCopy;
use crate::{alive, path, vertex_buffer};
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
#[derive(Debug)]
pub enum Conflict {
Name { path: String },
ZombieFile { path: String },
MultipleNames { pos: Position<ChangeId> },
Zombie { path: String, line: usize },
Cyclic { path: String, line: usize },
Order { path: String, line: usize },
}
pub fn output_repository_no_pending<T: MutTxnT, R: WorkingCopy, P: ChangeStore>(
repo: &mut R,
changes: &P,
txn: &mut T,
channel: &mut ChannelRef<T>,
prefix: &str,
output_name_conflicts: bool,
) -> Result<Vec<Conflict>, anyhow::Error> {
output_repository(
repo,
changes,
txn,
&mut channel.r.borrow_mut(),
ChangeId::ROOT,
&mut crate::path::components(prefix),
output_name_conflicts,
)
}
fn output_repository<
'a,
T: MutTxnT,
R: WorkingCopy,
P: ChangeStore,
I: Iterator<Item = &'a str>,
>(
repo: &mut R,
changes: &P,
txn: &mut T,
channel: &mut Channel<T>,
pending_change_id: ChangeId,
prefix: &mut I,
output_name_conflicts: bool,
) -> Result<Vec<Conflict>, anyhow::Error> {
let mut conflicts = Vec::new();
let mut files = HashMap::new();
let mut next_files = HashMap::new();
let mut next_prefix_basename = prefix.next();
collect_children(
txn,
changes,
channel,
Position::ROOT,
Inode::ROOT,
"",
next_prefix_basename,
&mut files,
)?;
let mut is_first_none = true;
if next_prefix_basename.is_none() {
let dead = collect_dead_files(txn, channel, pending_change_id, Inode::ROOT);
debug!("dead (line {}) = {:?}", line!(), dead);
kill_dead_files(txn, repo, &dead)?;
is_first_none = false;
}
debug!("done collecting: {:?}", files);
let mut done = HashMap::new();
let mut done_inodes = HashSet::new();
let mut actual_moves = Vec::new();
while !files.is_empty() {
debug!("files {:?}", files.len());
next_files.clear();
next_prefix_basename = prefix.next();
for (a, mut b) in files.drain() {
debug!("files: {:?} {:?}", a, b);
b.sort_by(|u, v| {
txn.get_changeset(&channel.changes, u.0.change, None)
.unwrap()
.cmp(
&txn.get_changeset(&channel.changes, v.0.change, None)
.unwrap(),
)
});
let mut is_first_name = true;
for (name_key, mut output_item) in b {
match done.entry(output_item.pos) {
Entry::Occupied(e) => {
debug!("pos already visited: {:?} {:?}", a, output_item.pos);
if *e.get() != name_key {
conflicts.push(Conflict::MultipleNames {
pos: output_item.pos,
});
}
continue;
}
Entry::Vacant(e) => {
e.insert(name_key);
}
}
if let Some((inode, _)) = output_item.inode {
if !done_inodes.insert(inode) {
debug!("inode already visited: {:?} {:?}", a, inode);
continue;
}
}
let name = if !is_first_name {
if output_name_conflicts {
let name = make_conflicting_name(&a, name_key);
conflicts.push(Conflict::Name { path: name.clone() });
name
} else {
debug!("not outputting {:?} {:?}", a, name_key);
conflicts.push(Conflict::Name {
path: a.to_string(),
});
break;
}
} else {
is_first_name = false;
a.clone()
};
let file_name = path::file_name(&name).unwrap();
path::push(&mut output_item.path, file_name);
let path = std::mem::replace(&mut output_item.path, String::new());
let inode = move_or_create(
txn,
repo,
&output_item,
&path,
&file_name,
&mut actual_moves,
)?;
if next_prefix_basename.is_none() && is_first_none {
let dead = collect_dead_files(txn, channel, pending_change_id, inode);
debug!("dead (line {}) = {:?}", line!(), dead);
kill_dead_files(txn, repo, &dead)?;
is_first_none = false;
}
if output_item.meta.is_dir() {
repo.create_dir_all(&path)?;
collect_children(
txn,
changes,
channel,
output_item.pos,
inode,
&path,
next_prefix_basename,
&mut next_files,
)?
} else {
repo.write_file(&path, |w: &mut dyn std::io::Write| {
output_file(txn, channel, changes, &output_item, &mut conflicts, w)
})?;
}
if output_item.is_zombie {
conflicts.push(Conflict::ZombieFile {
path: name.to_string(),
})
}
repo.set_permissions(&path, output_item.meta.permissions())?;
}
}
std::mem::swap(&mut files, &mut next_files);
}
for (a, b) in actual_moves.iter() {
repo.rename(a, b)?
}
Ok(conflicts)
}
fn make_conflicting_name(name: &str, name_key: Vertex<ChangeId>) -> String {
let parent = path::parent(name).unwrap();
let basename = path::file_name(name).unwrap();
let mut parent = parent.to_string();
path::push(
&mut parent,
&format!("{}.{}", basename, &name_key.change.to_base32()),
);
parent
}
fn move_or_create<T: MutTxnT, R: WorkingCopy>(
txn: &mut T,
repo: &mut R,
output_item: &OutputItem,
path: &str,
file_name: &str,
actual_moves: &mut Vec<(String, String)>,
) -> Result<Inode, anyhow::Error> {
let file_id = OwnedPathId {
parent_inode: output_item.parent,
basename: SmallString::from_str(&file_name),
};
let file_id_ = file_id.as_file_id();
debug!("move_or_create {:?}", file_id_);
if let Some((inode, _)) = output_item.inode {
if let Some(ref current_name) = inode_filename(txn, inode) {
debug!("current_name = {:?}, path = {:?}", current_name, path);
if current_name != path {
let parent = txn.get_revtree(inode, None).unwrap().to_owned();
debug!("parent = {:?}, inode = {:?}", parent, inode);
del_tree_with_rev(txn, parent.as_file_id(), inode)?;
let mut tmp_path = path.to_string();
crate::path::pop(&mut tmp_path);
use rand::Rng;
let s: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(30)
.collect();
crate::path::push(&mut tmp_path, &s);
repo.rename(¤t_name, &tmp_path)?;
actual_moves.push((tmp_path, path.to_string()));
if let Some(inode) = txn.get_tree(file_id_, None) {
crate::fs::rec_delete(txn, file_id.clone(), inode, true)?;
}
put_inodes_with_rev(txn, inode, output_item.pos)?;
put_tree_with_rev(txn, file_id_, inode)?;
}
} else {
debug!("no current name, inserting {:?} {:?}", file_id_, inode);
if let Some(inode) = txn.get_tree(file_id_, None) {
crate::fs::rec_delete(txn, file_id.clone(), inode, true)?;
}
put_inodes_with_rev(txn, inode, output_item.pos)?;
put_tree_with_rev(txn, file_id_, inode)?;
}
Ok(inode)
} else {
if let Some(inode) = txn.get_tree(file_id_, None) {
crate::fs::rec_delete(txn, file_id.clone(), inode, true)?;
}
let inode = create_new_inode(txn);
debug!(
"created new inode {:?} {:?} {:?}",
inode, output_item.pos, file_id_
);
put_inodes_with_rev(txn, inode, output_item.pos)?;
put_tree_with_rev(txn, file_id_, inode)?;
Ok(inode)
}
}
fn output_file<T: MutTxnT, P: ChangeStore>(
txn: &mut T,
channel: &mut Channel<T>,
changes: &P,
output_item: &OutputItem,
conflicts: &mut Vec<Conflict>,
w: &mut dyn std::io::Write,
) -> Result<(), anyhow::Error> {
let mut l = retrieve(txn, channel, output_item.pos);
let mut f = vertex_buffer::ConflictsWriter::new(w, &output_item.path, conflicts);
let mut forward = Vec::new();
alive::output_graph(changes, txn, channel, &mut f, &mut l, &mut forward)?;
for &(vertex, edge) in forward.iter() {
let dest = find_block(txn, &channel, edge.dest).unwrap();
debug!("deleting forward edge {:?} {:?} {:?}", vertex, dest, edge);
del_graph_with_rev(txn, channel, edge.flag, vertex, dest, edge.introduced_by)?;
}
Ok(())
}
fn collect_dead_files<T: MutTxnT>(
txn: &mut T,
channel: &Channel<T>,
pending_change_id: ChangeId,
inode: Inode,
) -> HashMap<OwnedPathId, (Inode, Option<String>)> {
let mut inodes = vec![(inode, false)];
let mut next_inodes = Vec::new();
let mut dead = HashMap::new();
while !inodes.is_empty() {
for (inode, parent_is_dead) in inodes.drain(..) {
for (id, inode_) in txn.iter_tree(
OwnedPathId {
parent_inode: inode,
basename: SmallString::from_str(""),
},
None,
) {
if id.parent_inode > inode {
break;
} else if id.parent_inode < inode {
continue;
}
let is_dead = parent_is_dead || {
if let Some(vertex) = txn.get_inodes(inode_, None) {
vertex.change != pending_change_id
&& !is_alive(txn, channel, vertex.inode_vertex())
} else {
true
}
};
if is_dead {
dead.insert(id.to_owned(), (inode_, inode_filename(txn, inode_)));
}
if inode_ != inode {
next_inodes.push((inode_, is_dead))
}
}
}
std::mem::swap(&mut inodes, &mut next_inodes)
}
dead
}
fn kill_dead_files<T: MutTxnT, W: WorkingCopy>(
txn: &mut T,
repo: &mut W,
dead: &HashMap<OwnedPathId, (Inode, Option<String>)>,
) -> Result<(), anyhow::Error> {
for (fileid, (inode, ref name)) in dead.iter() {
debug!("killing {:?} {:?} {:?}", fileid, inode, name);
del_tree_with_rev(txn, fileid.as_file_id(), *inode)?;
if let Some(vertex) = txn.get_inodes(*inode, None) {
debug!("kill_dead_files {:?} {:?}", inode, vertex);
del_inodes_with_rev(txn, *inode, vertex)?;
}
if let Some(name) = name {
repo.remove_path(&name)?
}
}
Ok(())
}