use super::{collect_children, OutputError, OutputItem, PristineOutputError};
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>,
done: &mut HashMap<Position<ChangeId>, Vertex<ChangeId>>,
prefix: &str,
output_name_conflicts: bool,
if_modified_since: Option<std::time::SystemTime>,
) -> Result<Vec<Conflict>, OutputError<P::Error, T::GraphError, R::Error>> {
output_repository(
repo,
changes,
txn,
&mut channel.r.borrow_mut(),
ChangeId::ROOT,
done,
&mut crate::path::components(prefix),
output_name_conflicts,
if_modified_since,
)
}
fn output_repository<
'a,
T: TreeMutTxnT + ChannelMutTxnT + GraphMutTxnT<GraphError = <T as TreeTxnT>::TreeError>,
R: WorkingCopy,
P: ChangeStore,
I: Iterator<Item = &'a str>,
>(
repo: &mut R,
changes: &P,
txn: &mut T,
channel: &mut T::Channel,
pending_change_id: ChangeId,
done: &mut HashMap<Position<ChangeId>, Vertex<ChangeId>>,
prefix: &mut I,
output_name_conflicts: bool,
if_modified_after: Option<std::time::SystemTime>,
) -> Result<Vec<Conflict>, OutputError<P::Error, T::TreeError, R::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,
T::graph(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, T::graph(channel), pending_change_id, Inode::ROOT)?;
debug!("dead (line {}) = {:?}", line!(), dead);
kill_dead_files::<T, R, P>(txn, repo, &dead)?;
is_first_none = false;
}
debug!("done collecting: {:?}", files);
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_unstable_by(|u, v| {
txn.get_changeset(T::changes(&channel), u.0.change)
.unwrap()
.cmp(&txn.get_changeset(T::changes(&channel), v.0.change).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);
}
}
let output_item_inode =
if let Some(inode) = txn.get_revinodes(output_item.pos, None)? {
Some((inode, txn.get_inodes(inode, None)?.unwrap()))
} else {
None
};
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::<T, R, P>(
txn,
repo,
&output_item,
output_item_inode,
&path,
&file_name,
&mut actual_moves,
)?;
if next_prefix_basename.is_none() && is_first_none {
let dead =
collect_dead_files(txn, T::graph(channel), pending_change_id, inode)?;
debug!("dead (line {}) = {:?}", line!(), dead);
kill_dead_files::<T, R, P>(txn, repo, &dead)?;
is_first_none = false;
}
if output_item.meta.is_dir() {
repo.create_dir_all(&path)
.map_err(OutputError::WorkingCopy)?;
collect_children(
txn,
changes,
T::graph(channel),
output_item.pos,
inode,
&path,
next_prefix_basename,
&mut next_files,
)?;
} else {
if needs_output(repo, if_modified_after, &path) {
repo.write_file(&path, |w: &mut dyn std::io::Write| {
output_file::<_, _, R>(
txn,
channel,
changes,
&output_item,
&mut conflicts,
w,
)
})
.map_err(OutputError::from)?
} else {
debug!("Not outputting {:?}", path)
}
}
if output_item.is_zombie {
conflicts.push(Conflict::ZombieFile {
path: name.to_string(),
})
}
repo.set_permissions(&path, output_item.meta.permissions())
.map_err(OutputError::WorkingCopy)?;
}
}
std::mem::swap(&mut files, &mut next_files);
}
for (a, b) in actual_moves.iter() {
repo.rename(a, b).map_err(OutputError::WorkingCopy)?
}
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 needs_output<R: WorkingCopy>(
repo: &R,
if_modified_after: Option<std::time::SystemTime>,
path: &str,
) -> bool {
if let Some(m) = if_modified_after {
if let Ok(last) = repo.modified_time(path) {
debug!("modified_after: {:?} {:?}", m, last);
if let Ok(d) = last.duration_since(m) {
return d.as_secs() > 0;
} else {
return false;
}
}
}
true
}
fn move_or_create<T: TreeMutTxnT, R: WorkingCopy, C: ChangeStore>(
txn: &mut T,
repo: &mut R,
output_item: &OutputItem,
output_item_inode: Option<(Inode, Position<ChangeId>)>,
path: &str,
file_name: &str,
actual_moves: &mut Vec<(String, String)>,
) -> Result<Inode, OutputError<C::Error, T::TreeError, R::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)
.map(|c| c as char)
.collect();
crate::path::push(&mut tmp_path, &s);
repo.rename(¤t_name, &tmp_path)
.map_err(OutputError::WorkingCopy)?;
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)
.map_err(PristineOutputError::Fs)?;
}
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)
.map_err(PristineOutputError::Fs)?;
}
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).map_err(PristineOutputError::Fs)?;
}
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)?;
if output_item.meta.is_dir() {
let path_id = OwnedPathId {
parent_inode: inode,
basename: SmallString::new(),
};
txn.put_tree(path_id.as_file_id(), inode)?;
}
Ok(inode)
}
}
fn output_file<T: ChannelMutTxnT + GraphMutTxnT, P: ChangeStore, W: WorkingCopy>(
txn: &mut T,
channel: &mut T::Channel,
changes: &P,
output_item: &OutputItem,
conflicts: &mut Vec<Conflict>,
w: &mut dyn std::io::Write,
) -> Result<(), OutputError<P::Error, T::GraphError, W::Error>> {
let mut l = retrieve(txn, T::graph(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)
.map_err(PristineOutputError::from)?;
for &(vertex, edge) in forward.iter() {
let dest = txn.find_block(T::graph(channel), edge.dest).unwrap();
debug!("deleting forward edge {:?} {:?} {:?}", vertex, dest, edge);
del_graph_with_rev(
txn,
T::graph_mut(channel),
edge.flag,
vertex,
dest,
edge.introduced_by,
)?;
}
Ok(())
}
fn collect_dead_files<T: TreeMutTxnT + GraphTxnT<GraphError = <T as TreeTxnT>::TreeError>>(
txn: &mut T,
channel: &T::Graph,
pending_change_id: ChangeId,
inode: Inode,
) -> Result<HashMap<OwnedPathId, (Inode, Option<String>)>, TxnErr<T::GraphError>> {
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 x in txn.iter_tree(
OwnedPathId {
parent_inode: inode,
basename: SmallString::from_str(""),
},
None,
)? {
let (id, inode_) = x?;
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)
}
Ok(dead)
}
fn kill_dead_files<T: TreeMutTxnT, W: WorkingCopy, C: ChangeStore>(
txn: &mut T,
repo: &mut W,
dead: &HashMap<OwnedPathId, (Inode, Option<String>)>,
) -> Result<(), OutputError<C::Error, T::TreeError, W::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).map_err(OutputError::WorkingCopy)?
}
}
}
Ok(())
}