+ //! 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))
+ }