//! Git support, most of this is taken from `pijul/src/commands/git.rs`

#![allow(
    clippy::clone_on_copy,
    clippy::unwrap_or_default,
    clippy::needless_borrow,
    clippy::mem_replace_with_default,
    clippy::let_and_return,
    clippy::while_let_on_iterator,
    clippy::collapsible_if,
    clippy::unnecessary_sort_by,
    clippy::type_complexity,
    clippy::needless_borrows_for_generic_args
)]

use crate::prelude::*;
use crate::PijulConfig;

use anyhow::Context;
use libpijul::pristine::{GraphIter, InodeMetadata, TxnErr};
use libpijul::{
    ArcTxn, Base32, ChannelRef, HashSet, MutTxnT, MutTxnTExt, TxnT, TxnTExt,
};
use pijul_repository::PRISTINE_DIR;
use sanakirja::RootPageMut;
use tracing::trace;

use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};

pub async fn import(path: PathBuf) -> Result<PathBuf, anyhow::Error> {
    let config =
        PijulConfig::load(None, vec![]).context("Loading Pijul config")?;
    let repo = if let Ok(repo) = pijul::Repository::find_root(Some(&path)) {
        repo
    } else {
        pijul::Repository::init(&config, Some(&path), None, None)?
    };
    let git = git2::Repository::open(&path)?;
    let st = git.statuses(None)?;
    let mut uncommitted = false;
    for i in 0..st.len() {
        if let Some(x) = st.get(i) {
            if x.path_bytes().starts_with(b".pijul")
                || x.path_bytes().starts_with(b".ignore")
            {
                continue;
            }
            debug!("status = {:?}", x.status());
            if x.status() != git2::Status::CURRENT
                && x.status() != git2::Status::IGNORED
            {
                eprintln!("Uncommitted file: {:?}", x.path().unwrap());
                uncommitted = true;
            }
        }
    }
    if uncommitted {
        bail!("There were uncommitted files")
    }
    let head = git.head()?;
    info!("Loading Git history…");
    let oid = head.target().unwrap();
    let mut path_git = repo.path.join(pijul::DOT_DIR);
    path_git.push("git");
    std::fs::create_dir_all(&path_git)?;
    let mut env_git = ::sanakirja::Env::new(&path_git.join("db"), 1 << 15, 2)?;
    let dag = Dag::dfs(&git, oid, &mut env_git)?;

    trace!(target: "dag", "{:?}", dag);
    debug!("Done");
    let mut pristine = repo.path.join(pijul::DOT_DIR);
    pristine.push(PRISTINE_DIR);
    std::fs::create_dir_all(&pristine)?;
    let mut repo = OpenRepo {
        repo,
        n: 0,
        current_commit: None,
    };
    do_import(&git, &mut env_git, &mut repo, &dag)?;

    let txn = repo.repo.pristine.arc_txn_begin()?;
    if let Some(oid) = repo.current_commit {
        let channel = txn.read().load_channel(&format!("{}", oid))?;
        if let Some(channel) = channel {
            libpijul::output::output_repository_no_pending(
                &libpijul::working_copy::FileSystem::from_root(&repo.repo.path),
                &repo.repo.changes,
                &txn,
                &channel,
                "",
                false,
                None,
                std::thread::available_parallelism()?.get(),
                0,
            )?;
        }
    }
    Ok(path)
}

struct OpenRepo {
    repo: pijul::Repository,
    n: usize,
    current_commit: Option<git2::Oid>,
}

#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
struct Oid(git2::Oid);

::sanakirja::direct_repr!(Oid);

#[derive(Debug)]
struct Dag {
    children: BTreeMap<git2::Oid, Vec<git2::Oid>>,
    parents: BTreeMap<git2::Oid, Vec<git2::Oid>>,
    root: Vec<(git2::Oid, Option<libpijul::Merkle>)>,
}

impl Dag {
    /// Load a Git repository in memory. The main reason this is
    /// needed is to compute the *backward* relations from a commit to
    /// its parents.
    fn dfs(
        git: &git2::Repository,
        oid: git2::Oid,
        env_git: &mut ::sanakirja::Env,
    ) -> Result<Self, anyhow::Error> {
        let mut stack = vec![git.find_commit(oid)?];
        let mut oids_set = BTreeSet::new();
        let mut dag = Dag {
            children: BTreeMap::new(),
            parents: BTreeMap::new(),
            root: Vec::new(),
        };
        oids_set.insert(oid.clone());
        let mut txn_git = ::sanakirja::Env::mut_txn_begin(env_git)?;
        let db: ::sanakirja::btree::UDb<
            Oid,
            libpijul::pristine::SerializedMerkle,
        > = unsafe {
            if let Some(db) = txn_git.root(0) {
                ::sanakirja::btree::UDb::from_page(db)
            } else {
                ::sanakirja::btree::create_db_(&mut txn_git)?
            }
        };
        let mut state = BTreeMap::new();
        for x in ::sanakirja::btree::iter(&txn_git, &db, None)? {
            let (commit, merk) = x?;
            state.insert(commit, merk.clone());
        }
        debug!("state = {:?}", state);
        while let Some(commit) = stack.pop() {
            if let Some(state) = state.get(&Oid(commit.id())) {
                dag.root.push((commit.id(), Some(state.into())));
                continue;
            }
            let mut has_parents = false;
            for p in commit.parents() {
                trace!("parent {:?}", p);
                dag.children
                    .entry(p.id())
                    .or_insert(Vec::new())
                    .push(commit.id());
                dag.parents
                    .entry(commit.id())
                    .or_insert(Vec::new())
                    .push(p.id());
                if oids_set.insert(p.id()) {
                    stack.push(p);
                }
                has_parents = true
            }
            if !has_parents {
                dag.root.push((commit.id(), None))
            }
        }
        txn_git.set_root(0, db.db.into());
        ::sanakirja::Commit::commit(txn_git)?;
        Ok(dag)
    }

    fn collect_dead_parents<T: MutTxnTExt>(
        &self,
        oid: &git2::Oid,
        todo: &mut Todo,
        txn: &ArcTxn<T>,
    ) -> Result<(), anyhow::Error> {
        if let Some(parents) = self.parents.get(oid) {
            debug!("parents {:?}", parents);
            for p in parents {
                let rc = todo.refs.get_mut(p).unwrap();
                *rc -= 1;
                if *rc == 0 {
                    let p_name = format!("{}", p);
                    debug!("dropping channel {:?}", p_name);
                    let mut txn = txn.write();
                    txn.drop_channel(&p_name)?;
                }
            }
        }
        Ok(())
    }

    fn insert_children_in_todo(&self, oid: &git2::Oid, todo: &mut Todo) {
        if let Some(c) = self.children.get(&oid) {
            for child in c {
                debug!("child = {:?}", c);
                if todo.next_todo_set.insert(*child) {
                    todo.next_todo.push(*child);
                }
                *todo.refs.entry(*oid).or_insert(0) += 1;
            }
        } else {
            debug!("no children")
        }
    }
}

#[derive(Debug)]
struct Todo {
    todo: Vec<git2::Oid>,
    todo_set: BTreeSet<git2::Oid>,
    next_todo: Vec<git2::Oid>,
    next_todo_set: BTreeSet<git2::Oid>,
    // For each key k, number of items in the union of todo and
    // next_todo that have k as a parent. Moreover, all commits that
    // were imported are in this map.
    refs: BTreeMap<git2::Oid, usize>,
}

impl Todo {
    fn new() -> Self {
        Todo {
            todo: Vec::new(),
            todo_set: BTreeSet::new(),
            next_todo: Vec::new(),
            next_todo_set: BTreeSet::new(),
            refs: BTreeMap::new(),
        }
    }

    fn swap_next(&mut self, todo: Vec<git2::Oid>) {
        self.todo = todo;
        std::mem::swap(&mut self.todo, &mut self.next_todo);
        self.todo_set.clear();
        std::mem::swap(&mut self.todo_set, &mut self.next_todo_set);
    }

    fn insert_next(&mut self, oid: git2::Oid) {
        if self.next_todo_set.insert(oid) {
            self.next_todo.push(oid)
        }
    }

    fn is_empty(&self) -> bool {
        self.todo.is_empty()
    }

    fn all_processed(&self, parents: &[git2::Oid]) -> bool {
        parents.iter().all(|x| self.refs.contains_key(x))
    }
}

/// Import the entire Git DAG into Pijul.
fn do_import(
    git: &git2::Repository,
    env_git: &mut ::sanakirja::Env,
    repo: &mut OpenRepo,
    dag: &Dag,
) -> Result<(), anyhow::Error> {
    let mut ws = libpijul::ApplyWorkspace::new();
    let mut todo = Todo::new();

    let txn = repo.repo.pristine.arc_txn_begin()?;
    for &(oid, merkle) in dag.root.iter() {
        if let Some(merkle) = merkle {
            let oid_ = format!("{}", oid);
            let channel = txn
                .read()
                .load_channel(&oid_)?
                .ok_or_else(|| anyhow!("No such channel: {}", &oid_))?;

            let (_, &p) = txn
                .read()
                .changeid_reverse_log(&*channel.read(), None)?
                .next()
                .unwrap()?;
            let merkle_: libpijul::Merkle = (&p.b).into();
            if merkle != merkle_ {
                bail!(
                    "Pijul channel changed since last import. Please unrecord channel {} to state {}",
                    oid_,
                    merkle.to_base32()
                )
            }
            if let Some(children) = dag.children.get(&oid) {
                *todo.refs.entry(oid).or_insert(0) += children.len();
                for c in children.iter() {
                    todo.insert_next(*c);
                }
            }
        } else {
            todo.insert_next(oid);
            if let Some(parents) = dag.parents.get(&oid) {
                for p in parents.iter() {
                    *todo.refs.entry(*p).or_insert(0) += 1;
                }
            }
        }
    }
    std::mem::drop(txn);
    todo.swap_next(Vec::new());

    while !todo.is_empty() {
        debug!("TODO: {:?}", todo);
        let mut todo_ = std::mem::replace(&mut todo.todo, Vec::new());
        {
            let mut draining = todo_.drain(..);
            let txn = repo.repo.pristine.arc_txn_begin()?;
            while let Some(oid) = draining.next() {
                let channel = if let Some(parents) = dag.parents.get(&oid) {
                    // If we don't have all the parents, continue.
                    if !todo.all_processed(&parents) {
                        todo.insert_next(oid);
                        continue;
                    }
                    let first_parent = parents.iter().next().unwrap();
                    let parent_name = format!("{}", first_parent);
                    let mut txn = txn.write();
                    let parent_channel =
                        txn.load_channel(&parent_name)?.unwrap();

                    let name = format!("{}", oid);
                    let channel = txn.fork(&parent_channel, &name)?;

                    channel
                } else {
                    // Create a new channel for this commit.
                    let name = format!("{}", oid);
                    let mut txn = txn.write();
                    let channel = txn.open_or_create_channel(&name)?;
                    channel
                };

                import_commit_parents(
                    repo, dag, &txn, &channel, &oid, &mut ws,
                )?;
                let state = import_commit(git, repo, &txn, &channel, &oid)?;
                save_state(env_git, &oid, state)?;
                dag.collect_dead_parents(&oid, &mut todo, &txn)?;
                dag.insert_children_in_todo(&oid, &mut todo);

                // Just add the remaining commits to the todo list,
                // because we prefer to move each channel as far as
                // possible before switching channels.
                while let Some(oid) = draining.next() {
                    todo.insert_next(oid)
                }
            }
            txn.commit()?;
        }
        todo.swap_next(todo_)
    }
    Ok(())
}

fn save_state(
    git: &mut ::sanakirja::Env,
    oid: &git2::Oid,
    state: libpijul::Merkle,
) -> Result<(), anyhow::Error> {
    use ::sanakirja::Commit;
    let mut txn = ::sanakirja::Env::mut_txn_begin(git)?;
    let mut db: ::sanakirja::btree::UDb<
        Oid,
        libpijul::pristine::SerializedMerkle,
    > = unsafe {
        if let Some(db) = txn.root(0) {
            ::sanakirja::btree::UDb::from_page(db)
        } else {
            ::sanakirja::btree::create_db_(&mut txn)?
        }
    };
    ::sanakirja::btree::put(&mut txn, &mut db, &Oid(*oid), &state.into())?;
    txn.set_root(0, db.db.into());
    txn.commit()?;
    Ok(())
}

fn make_apply_plan<T: TxnTExt>(
    repo: &OpenRepo,
    txn: &ArcTxn<T>,
    channel: &ChannelRef<T>,
    dag: &Dag,
    oid: &git2::Oid,
) -> Result<(bool, Vec<(libpijul::Hash, u64)>), anyhow::Error> {
    let mut to_apply = Vec::new();
    let mut to_apply_set = BTreeSet::new();
    let mut needs_output = false;
    if let Some(parents) = dag.parents.get(&oid) {
        let txn = txn.read();
        for p in parents {
            // If one of the parents is not the repo's current commit,
            // then we're doing either a merge or a checkout of
            // another branch. If that is the case, we need to output
            // the entire repository to update the
            // tree/revtree/inodes/revinodes tables.
            if let Some(current_commit) = repo.current_commit {
                if current_commit != *p {
                    needs_output = true
                }
            }
            let p_name = format!("{}", p);
            let p_channel = txn.load_channel(&p_name)?.unwrap();
            for x in txn.log(&*p_channel.read(), 0)? {
                let (n, (h, _)) = x?;
                let h: libpijul::Hash = h.into();
                if txn.has_change(&channel, &h)?.is_none() {
                    if to_apply_set.insert(h) {
                        to_apply.push((h, n));
                    }
                }
            }
        }
    } else {
        needs_output = true
    }

    // Since we're pulling from multiple channels, the change numbers
    // are not necessarily in order (especially since we've
    // de-duplicated using `to_apply_set`.

    to_apply.sort_by(|a, b| a.1.cmp(&b.1));
    Ok((needs_output, to_apply))
}

/// Apply the changes corresponding to a commit's parents to `channel`.
fn import_commit_parents<
    T: TxnTExt + MutTxnTExt + GraphIter + Send + Sync + 'static,
>(
    repo: &mut OpenRepo,
    dag: &Dag,
    txn: &ArcTxn<T>,
    channel: &ChannelRef<T>,
    oid: &git2::Oid,
    ws: &mut libpijul::ApplyWorkspace,
) -> Result<(), anyhow::Error> {
    // Apply all the parent's logs to `channel`
    let (_needs_output, to_apply) =
        make_apply_plan(repo, &txn, &channel, dag, oid)?;
    for h in to_apply.iter() {
        debug!("to_apply {:?}", h)
    }
    let mut txn_ = txn.write();
    for (h, _) in to_apply.iter() {
        let mut channel_ = channel.write();
        info!("applying {:?} to {:?}", h, txn_.name(&channel_));

        txn_.apply_change_ws(&repo.repo.changes, &mut channel_, h, ws)?;
    }
    std::mem::drop(txn_);
    Ok(())
}

/// Reset to the Git commit specified by `child`, telling Pijul which
/// files were moved in the reset.
fn git_reset<'a, T: TxnTExt + MutTxnTExt>(
    git: &'a git2::Repository,
    repo: &mut OpenRepo,

    txn: &ArcTxn<T>,
    channel: &ChannelRef<T>,

    child: &git2::Oid,
) -> Result<
    (git2::Object<'a>, BTreeMap<PathBuf, bool>, HashSet<String>),
    anyhow::Error,
> {
    // Reset the Git branch.

    debug!("resetting the git branch to {:?}", child);
    let object = git.find_object(*child, None)?;
    repo.current_commit = Some(*child);
    debug!("reset done");

    let mut prefixes = BTreeMap::new();
    let mut pref = HashSet::new();
    {
        let commit = object.as_commit().unwrap();
        let new_tree = commit.tree().unwrap();

        debug!("inspecting commit");
        let mut has_parents = false;
        for parent in commit.parents() {
            has_parents = true;
            let old_tree = parent.tree().unwrap();
            let mut diff = git
                .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)
                .unwrap();
            diff.find_similar(None).unwrap();
            let mut moves = Vec::new();
            let mut txn = txn.write();
            for delta in diff.deltas() {
                let old_path = delta.old_file().path().unwrap();
                let new_path = delta.new_file().path().unwrap();
                let is_dir = delta.new_file().mode() == git2::FileMode::Tree;
                match delta.status() {
                    git2::Delta::Renamed => {
                        info!(
                            "mv {:?} {:?}",
                            old_path.to_string_lossy(),
                            new_path.to_string_lossy()
                        );
                        if let Ok((vertex, _)) = txn.follow_oldest_path(
                            &repo.repo.changes,
                            &channel,
                            &old_path.to_string_lossy(),
                        ) {
                            if let Some(inode) =
                                txn.get_revinodes(&vertex, None)?
                            {
                                if let Some(old_path) =
                                    libpijul::fs::inode_filename(&*txn, *inode)?
                                {
                                    debug!(
                                        "moving {:?} ({:?}) from {:?} to {:?}",
                                        inode, vertex, old_path, new_path
                                    );
                                    let mut tmp_path = new_path.to_path_buf();
                                    tmp_path.pop();
                                    use rand::Rng;
                                    let s: String = rand::rng()
                                        .sample_iter(&rand::distr::Alphanumeric)
                                        .take(30)
                                        .map(|x| x as char)
                                        .collect();
                                    tmp_path.push(&s);
                                    if let Err(e) = txn.move_file(
                                        &old_path,
                                        &tmp_path.to_string_lossy(),
                                        0,
                                    ) {
                                        error!("{}", e);
                                    } else {
                                        moves.push((tmp_path, new_path));
                                    }
                                }
                            }
                        }
                        let new_path_ = new_path.to_path_buf();
                        pref.insert(new_path.to_str().unwrap().to_string());
                        prefixes.insert(new_path_, is_dir);
                    }
                    git2::Delta::Deleted => {
                        let old_path = old_path.to_path_buf();
                        prefixes.insert(old_path, is_dir);
                    }
                    _ => {
                        if delta.new_file().mode() != git2::FileMode::Link {
                            debug!(
                                "delta old = {:?} new = {:?}",
                                old_path, new_path
                            );
                            let old_path_ = old_path.to_path_buf();
                            let new_path_ = new_path.to_path_buf();
                            prefixes.insert(old_path_, is_dir);
                            prefixes.insert(new_path_, is_dir);
                            pref.insert(old_path.to_str().unwrap().to_string());
                            pref.insert(new_path.to_str().unwrap().to_string());
                        }
                    }
                }
            }
            debug!("moves = {:?}", moves);
            for (a, b) in moves.drain(..) {
                if let Err(e) =
                    txn.move_file(&a.to_string_lossy(), &b.to_string_lossy(), 0)
                {
                    error!("{}", e);
                }
            }
        }
        if !has_parents {
            use git2::{TreeWalkMode, TreeWalkResult};
            new_tree
                .walk(TreeWalkMode::PreOrder, |x, t| {
                    debug!("t = {:?} {:?}", x, t.name());
                    if let Some(n) = t.name() {
                        let mut m = Path::new(x).to_path_buf();
                        m.push(n);
                        prefixes.insert(
                            m,
                            t.kind() == Some(git2::ObjectType::Tree),
                        );
                    }
                    TreeWalkResult::Ok
                })
                .unwrap();
        }
        debug!("record prefixes {:?}", prefixes);
    }
    Ok((object, prefixes, pref))
}

#[derive(Clone)]
struct Commit<'a> {
    r: &'a git2::Repository,
    c: git2::Commit<'a>,
    pref: HashSet<String>,
}

impl<'a> libpijul::working_copy::WorkingCopyRead for Commit<'a> {
    type Error = git2::Error;

    fn file_metadata(&self, file: &str) -> Result<InodeMetadata, Self::Error> {
        debug!("metadata {:?}", file);
        let entry = self.c.tree()?.get_path(Path::new(file))?;
        let is_dir = entry.kind() == Some(git2::ObjectType::Tree);
        if is_dir {
            Ok(InodeMetadata::new(0o100, true))
        } else {
            let permissions = entry.filemode();
            debug!(
                "permissions = {:o} {:o} {:?}",
                permissions,
                permissions & 0o100,
                is_dir
            );
            Ok(InodeMetadata::new(permissions as usize & 0o100, false))
        }
    }

    fn read_file(
        &self,
        file: &str,
        buffer: &mut Vec<u8>,
    ) -> Result<(), Self::Error> {
        debug!("read file {:?}", file);
        let entry = self.c.tree()?.get_path(Path::new(file))?;
        if let Ok(b) = entry.to_object(self.r)?.peel_to_blob() {
            buffer.extend(b.content());
        }
        debug!("entry {:?}", entry.kind());
        Ok(())
    }

    fn modified_time(
        &self,
        x: &str,
    ) -> Result<std::time::SystemTime, Self::Error> {
        if self.pref.contains(x) {
            Ok(std::time::SystemTime::now())
        } else {
            Ok(std::time::SystemTime::UNIX_EPOCH)
        }
    }
}

/// Reset to the Git commit specified as `child`, and record the
/// corresponding change in Pijul.
fn import_commit<
    T: TxnTExt + MutTxnTExt + GraphIter + Send + Sync + 'static,
>(
    git: &git2::Repository,
    repo: &mut OpenRepo,
    txn: &ArcTxn<T>,
    channel: &ChannelRef<T>,
    child: &git2::Oid,
) -> Result<libpijul::Merkle, anyhow::Error> {
    let (object, prefixes, prefstr) =
        git_reset(git, repo, &txn, &channel, child)?;
    debug!("prefixes = {:?}", prefixes);
    let mut txn_ = txn.write();
    let mut prefixes_ = BTreeMap::new();
    for (mut p, is_dir) in prefixes {
        use path_slash::PathExt;
        loop {
            debug!("p = {:?}", p);
            if prefixes_.contains_key(&p) {
                break;
            }
            let p_ = p.to_slash_lossy();
            debug!("adding prefix {:?}", p_);
            let (tracked, pos) = libpijul::fs::get_vertex(&*txn_, &p_)?;
            if !tracked {
                debug!("not tracked");
                if is_dir {
                    txn_.add_dir(&p_, 0).map(|_| ()).unwrap_or(());
                } else {
                    txn_.add_file(&p_, 0).map(|_| ()).unwrap_or(());
                }
            }
            debug!("pos = {:?}", pos);
            if pos.is_none() || !is_dir {
                if !p.pop() {
                    prefixes_.insert(PathBuf::new(), true);
                    break;
                }
            } else {
                prefixes_.insert(p, is_dir);
                break;
            }
        }
    }
    let commit = object.as_commit().unwrap();
    let signature = commit.author();
    // Record+Apply
    debug!("recording on channel {:?}", txn_.name(&channel.read()));

    if let Some(msg) = commit.message() {
        info!("Importing commit {:?}: {}", child, msg);
    } else {
        info!("Importing commit {:?} (no message)", child);
    }
    std::mem::drop(txn_);
    let msg = commit.message().unwrap();
    let mut msg_lines = msg.lines();
    let mut message = String::new();
    if let Some(m) = msg_lines.next() {
        message.push_str(m)
    }
    let mut description = String::new();
    for m in msg_lines {
        if !description.is_empty() {
            description.push('\n')
        }
        description.push_str(m);
    }
    let mut author = BTreeMap::new();
    author.insert("name".to_string(), signature.name().unwrap().to_string());
    author.insert("email".to_string(), signature.email().unwrap().to_string());
    let rec = record_apply(
        &txn,
        &channel,
        // &repo.repo.working_copy
        &Commit {
            r: git,
            c: git.find_commit(*child)?,
            pref: prefstr,
        },
        &repo.repo.changes,
        &prefixes_,
        libpijul::change::ChangeHeader {
            message,
            authors: vec![libpijul::change::Author(author)],
            description: if description.is_empty() {
                None
            } else {
                Some(description)
            },
            timestamp: jiff::Timestamp::from_second(
                signature.when().seconds(),
            )?,
        },
    );
    {
        let mut txn = txn.write();
        let name = txn.name(&channel.read()).to_string();
        txn.set_current_channel(&name)?;
    }
    let txn = txn.read();
    let (_n_actions, _hash, state) = match rec {
        Ok(x) => x,
        Err(libpijul::LocalApplyError::ChangeAlreadyOnChannel { hash }) => {
            error!("change already on channel: {:?}", hash);
            return Ok(txn.current_state(&channel.read())?);
        }
        Err(e) => return Err(e.into()),
    };

    repo.n += 1;
    Ok(state)
}

fn record_apply<
    T: TxnT + TxnTExt + MutTxnTExt,
    C: libpijul::changestore::ChangeStore + Clone,
    W: libpijul::working_copy::WorkingCopyRead + Clone,
>(
    txn: &ArcTxn<T>,
    channel: &ChannelRef<T>,
    working_copy: &W,
    changes: &C,
    prefixes: &BTreeMap<PathBuf, bool>,
    header: libpijul::change::ChangeHeader,
) -> Result<
    (usize, Option<libpijul::Hash>, libpijul::Merkle),
    libpijul::LocalApplyError<T>,
>
where
    W::Error: 'static,
{
    debug!("record_apply {:?}", prefixes);
    let mut state = libpijul::RecordBuilder::new();
    let mut last = None;
    for (p, _) in prefixes.iter() {
        if let Some(last) = last {
            if p.starts_with(&last) {
                continue;
            }
        }
        state
            .record_single_thread(
                txn.clone(),
                libpijul::Algorithm::default(),
                false,
                &libpijul::DEFAULT_SEPARATOR,
                channel.clone(),
                working_copy,
                changes,
                p.to_str().unwrap(),
            )
            .unwrap();
        last = Some(p);
    }
    if prefixes.is_empty() {
        state
            .record_single_thread(
                txn.clone(),
                libpijul::Algorithm::default(),
                false,
                &libpijul::DEFAULT_SEPARATOR,
                channel.clone(),
                working_copy,
                changes,
                "",
            )
            .unwrap();
    }
    let rec = state.finish();
    let mut txn = txn.write();
    if rec.actions.is_empty() {
        return Ok((
            0,
            None,
            txn.current_state(&channel.read()).map_err(TxnErr)?,
        ));
    }
    let actions: Vec<_> = rec
        .actions
        .into_iter()
        .map(|rec| rec.globalize(&*txn).unwrap())
        .collect();
    let n = actions.len();
    let (dependencies, extra_known) =
        libpijul::change::dependencies(&*txn, &channel.read(), actions.iter())?;
    let mut change = libpijul::change::LocalChange::make_change(
        &*txn,
        &channel,
        actions,
        std::mem::replace(&mut *rec.contents.lock(), Vec::new()),
        header,
        Vec::new(),
    )?;
    change.dependencies = dependencies;
    change.extra_known = extra_known;
    debug!("saving change");
    let hash = changes
        .save_change(&mut change, |_, _| Ok::<_, anyhow::Error>(()))
        .unwrap();
    debug!("saved");
    let (_, m) =
        txn.apply_local_change(channel, &change, &hash, &rec.updatables)?;
    Ok((n, Some(hash), m))
}