add support for git import

tzemanovic
Feb 11, 2026, 1:35 PM
XQDYES5MDSTFO7OPUPCRQLLQ6NAVELJOCCLSS6YE7TI6QDDFANDQC

Dependencies

  • [2] 6YZAVBWU Initial commit
  • [3] KLR5FRIB add fs state read/write of repos
  • [4] IQDCHWCP load a pijul repo
  • [5] SWWE2R6M display basic repo stuff
  • [6] 2VUX5BTD load identity
  • [7] 6SW7UVSH update iced version
  • [8] WI2BVQ6J rm client lib crate
  • [9] UF5NJKAS test load repo
  • [10] VJYEVHL5 update libpijul
  • [11] 6LF2U2Y6 improve file encoding detection
  • [12] F6O6FGOJ improve diffs encoding detection
  • [13] TEDT26JQ add push and pull sub-menus
  • [14] YRGDFHAB project dir picker
  • [15] LPSUBGUB add projects picker
  • [16] MORKDJUE use allowed actions binding for key subs
  • [17] 5BAPU7K6 dir picker key navigation
  • [18] TMDH7GPV dir picker scrollables handling + confirmation
  • [19] J3AD2D2J improve push and pull keys
  • [20] IOXNOVX2 allow to initiate a new repo
  • [21] I7EWMAHY allow to init new repo in existing dir and improve dir picker view
  • [22] 5ZRDYL6K fork channel, fix recording esc key
  • [23] YK3MOJJL chonky refactor, wip other channels logs & diffs
  • [24] SEJXDXPW improve file watcher to respect .ignore file
  • [25] WAOGSCOJ very nice refactor, wip adding channels logs
  • [*] 23SFYK4Q big view refactor into a new crate
  • [*] 7WCB5YQJ refactor msgs and modules
  • [*] EJPSD5XO shared allowed actions conditions between update and view

Change contents

  • edit in libflorescence/src/repo.rs at line 1
    [5.7]
    [9.884]
    pub mod git;
  • file addition: git.rs (----------)
    [9.1941]
    //! 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))
    }
  • edit in libflorescence/Cargo.toml at line 39
    [12.3436]
    [12.3436]
    workspace = true
    [dependencies.git2]
  • edit in libflorescence/Cargo.toml at line 59
    [8.858]
    [4.882]
    workspace = true
    [dependencies.sanakirja]
  • replacement in inflorescence_view/src/view.rs at line 160
    [14.19435][14.19435:19559](),[14.19559][17.26:72](),[17.72][21.26:69](),[21.69][17.112:314](),[17.112][17.112:314](),[17.314][21.70:110](),[21.110][17.314:359](),[17.314][17.314:359](),[17.359][14.19656:19738](),[15.14939][14.19656:19738](),[14.19656][14.19656:19738]()
    let model::PickingRepoDir { picker } = state;
    let main = el(column([
    el(text("Select project directory:")),
    dir_picker::view(
    picker,
    theme::Container::FadedBorder,
    theme::Container::NavSelectedSection,
    theme::Button::Normal,
    theme::Button::Selected,
    theme::Scrollable::Normal,
    theme::Scrollable::Selected,
    theme::Text::SlightlyFaded,
    )
    .map(Msg::PickingRepoDir),
    ])
    .spacing(SPACING)
    .width(Length::Fill)
    .height(Length::Fill));
    [14.19435]
    [14.19738]
    let model::PickingRepoDir {
    picker,
    waiting_to_init,
    } = state;
    let main = if let Some(init_kind) = waiting_to_init {
    el(column([
    el(text("Waiting to finish initializing Pijul...")),
    el(text(match init_kind {
    model::ProjectInitKind::New => "",
    model::ProjectInitKind::ImportFromGit => {
    "Importing from Git is still experimental and it might take a while"
    }
    })),
    ])
    .spacing(SPACING)
    .width(Length::Fill)
    .height(Length::Fill))
    } else {
    el(column([
    el(text("Select project directory:")),
    dir_picker::view(
    picker,
    theme::Container::FadedBorder,
    theme::Container::NavSelectedSection,
    theme::Button::Normal,
    theme::Button::Selected,
    theme::Scrollable::Normal,
    theme::Scrollable::Selected,
    theme::Text::SlightlyFaded,
    )
    .map(Msg::PickingRepoDir),
    ])
    .spacing(SPACING)
    .width(Length::Fill)
    .height(Length::Fill))
    };
  • edit in inflorescence_model/src/model.rs at line 67
    [14.3592271]
    [14.3592271]
    pub waiting_to_init: Option<ProjectInitKind>,
    }
    #[derive(Debug, Clone, Copy)]
    pub enum ProjectInitKind {
    New,
    ImportFromGit,
  • edit in inflorescence_model/src/model.rs at line 88
    [20.1118]
    [13.531968]
    ImportFromGit { path: PathBuf },
  • edit in inflorescence_model/src/action.rs at line 234
    [20.1304]
    [20.1304]
    }
    model::SubMenu::ImportFromGit { path: _ } => {
    vec![confirm("confirm import Git repo"), cancel()]
  • edit in inflorescence_model/src/action.rs at line 321
    [17.2175]
    [17.2175]
    waiting_to_init,
  • replacement in inflorescence_model/src/action.rs at line 396
    [16.172025][17.4190:4602](),[17.4602][16.172679:172689](),[16.172679][16.172679:172689](),[16.172689][17.4603:4610](),[17.4610][16.172697:172698](),[16.172697][16.172697:172698](),[16.172698][21.294:585](),[21.585][18.544:563](),[19.45432][18.544:563](),[18.544][18.544:563](),[18.563][17.4611:4741](),[16.172698][17.4611:4741]()
    let can_down_or_up = match selection {
    dir_picker::Selection::Input => {
    !matched_child_dirs.is_empty() || !child_dirs.is_empty()
    }
    dir_picker::Selection::SubDir(_) => true,
    dir_picker::Selection::ProjectPijul(_)
    | dir_picker::Selection::ProjectGit(_) => {
    !found_repos_dirs_pijul.is_empty()
    || !found_repos_dirs_git.is_empty()
    }
    };
    push(
    || {
    confirm(match current_kind {
    Some(dir_picker::RepoKind::Pijul) => "open Pijul repository",
    Some(dir_picker::RepoKind::Git) => "import from Git",
    None => "initialize new repository",
    })
    },
    ma,
    );
    push_if(can_down_or_up, down, ma);
    push_if(can_down_or_up, up, ma);
    push_if(
    matches!(
    selection,
    [16.172025]
    [17.4741]
    if waiting_to_init.is_none() {
    let can_down_or_up = match selection {
    dir_picker::Selection::Input => {
    !matched_child_dirs.is_empty() || !child_dirs.is_empty()
    }
    dir_picker::Selection::SubDir(_) => true,
  • replacement in inflorescence_model/src/action.rs at line 403
    [17.4792][17.4792:5092]()
    | dir_picker::Selection::ProjectGit(_)
    ),
    left,
    ma,
    );
    push_if(
    matches!(
    selection,
    dir_picker::Selection::Input | dir_picker::Selection::SubDir(_)
    ),
    right,
    ma,
    );
    push(focus_next, ma);
    [17.4792]
    [17.5092]
    | dir_picker::Selection::ProjectGit(_) => {
    !found_repos_dirs_pijul.is_empty()
    || !found_repos_dirs_git.is_empty()
    }
    };
    push(
    || {
    confirm(match current_kind {
    Some(dir_picker::RepoKind::Pijul) => {
    "open Pijul repository"
    }
    Some(dir_picker::RepoKind::Git) => "import from Git",
    None => "initialize new repository",
    })
    },
    ma,
    );
    push_if(can_down_or_up, down, ma);
    push_if(can_down_or_up, up, ma);
    push_if(
    matches!(
    selection,
    dir_picker::Selection::ProjectPijul(_)
    | dir_picker::Selection::ProjectGit(_)
    ),
    left,
    ma,
    );
    push_if(
    matches!(
    selection,
    dir_picker::Selection::Input | dir_picker::Selection::SubDir(_)
    ),
    right,
    ma,
    );
    push(focus_next, ma);
    }
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 601
    [18.10326]
    [18.10326]
    state.input = String::default();
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 609
    [18.10621]
    [18.10621]
    state.input = String::default();
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 632
    [18.11302]
    [18.11302]
    update_current_kind(state);
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 636
    [18.11324]
    [14.8248]
    #[allow(clippy::too_many_arguments)]
  • replacement in inflorescence/src/main.rs at line 204
    [14.3611390][15.17264:17314]()
    let model = model::PickingRepoDir { picker };
    [14.3611390]
    [15.17314]
    let model = model::PickingRepoDir {
    picker,
    waiting_to_init: None,
    };
  • edit in inflorescence/src/main.rs at line 385
    [20.1912]
    [20.1912]
    if let model::SubState::PickingRepoDir(state) = &mut state.model.sub
    {
    state.waiting_to_init = None;
    }
  • replacement in inflorescence/src/main.rs at line 783
    [14.3615471][14.3615471:3615521]()
    let model::PickingRepoDir { picker } = state;
    [14.3615471]
    [18.11403]
    let model::PickingRepoDir {
    picker,
    waiting_to_init: _,
    } = state;
  • replacement in inflorescence/src/main.rs at line 814
    [21.4617][21.4617:4919]()
    // Allow to import from Git or init new Pijul
    // history TODO
    // *sub_menu = Some(model::SubMenu::InitRepo {
    // path: dir.clone(),
    // });
    [21.4617]
    [21.4919]
    // Allow to import from Git
    *sub_menu =
    Some(model::SubMenu::ImportFromGit {
    path: dir.clone(),
    });
  • replacement in inflorescence/src/main.rs at line 849
    [17.18160][20.4560:4907]()
    action::FilteredMsg::Confirm => match &sub_menu {
    Some(model::SubMenu::InitRepo { path }) => {
    let path = path.clone();
    Task::perform(
    async move { repo::init(path).await },
    Msg::InitedRepo,
    )
    }
    [17.18160]
    [20.4907]
    action::FilteredMsg::Confirm => {
    let (task, clear_sub_menu) = match &sub_menu {
    Some(model::SubMenu::InitRepo { path }) => {
    state.waiting_to_init =
    Some(model::ProjectInitKind::New);
    let path = path.clone();
    (
    Task::perform(
    async move { repo::init(path).await },
    Msg::InitedRepo,
    ),
    true,
    )
    }
    Some(model::SubMenu::ImportFromGit { path }) => {
    state.waiting_to_init =
    Some(model::ProjectInitKind::ImportFromGit);
    let path = path.clone();
    (
    Task::perform(
    async move { repo::git::import(path).await },
    Msg::InitedRepo,
    ),
    true,
    )
    }
  • replacement in inflorescence/src/main.rs at line 876
    [20.4908][20.4908:5477]()
    Some(
    model::SubMenu::Push
    | model::SubMenu::Pull
    | model::SubMenu::ResetChange,
    )
    | None => {
    let (task, action) =
    dir_picker::confirm_input_or_selection(picker);
    let dir_picker_task =
    task.map(view::Msg::PickingRepoDir).map(Msg::View);
    let action_task = handle_action(sub_menu, action);
    Task::batch([dir_picker_task, action_task])
    [20.4908]
    [20.5477]
    Some(
    model::SubMenu::Push
    | model::SubMenu::Pull
    | model::SubMenu::ResetChange,
    )
    | None => {
    let (task, action) =
    dir_picker::confirm_input_or_selection(picker);
    let dir_picker_task =
    task.map(view::Msg::PickingRepoDir).map(Msg::View);
    let action_task = handle_action(sub_menu, action);
    (Task::batch([dir_picker_task, action_task]), false)
    }
    };
    if clear_sub_menu {
    *sub_menu = None;
  • replacement in inflorescence/src/main.rs at line 893
    [20.5495][20.5495:5510](),[20.5510][18.12877:12917](),[18.12877][18.12877:12917](),[18.12917][17.18243:18293](),[17.18243][17.18243:18293]()
    },
    action::FilteredMsg::Cancel
    | action::FilteredMsg::PostponeRecord
    [20.5495]
    [17.18293]
    task
    }
    action::FilteredMsg::Cancel => {
    *sub_menu = None;
    Task::none()
    }
    action::FilteredMsg::PostponeRecord
  • replacement in inflorescence/src/main.rs at line 1035
    [20.6227][20.6227:6289]()
    Some(model::SubMenu::InitRepo { .. }) | None => {
    [20.6227]
    [20.6289]
    Some(
    model::SubMenu::InitRepo { .. }
    | model::SubMenu::ImportFromGit { .. },
    )
    | None => {
  • edit in Cargo.toml at line 51
    [11.2984]
    [2.4227]
    [workspace.dependencies.git2]
    version = "0.20"
  • edit in Cargo.toml at line 111
    [10.1067]
    [3.5995]
    [workspace.dependencies.sanakirja]
    version = "2.0.0-beta" # has to match libpijul version
    default-features = false
    features = ["crc32"]
  • edit in Cargo.lock at line 2030
    [2.35034]
    [2.35034]
    name = "git2"
    version = "0.20.4"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
    dependencies = [
    "bitflags 2.10.0",
    "libc",
    "libgit2-sys",
    "log",
    "openssl-probe",
    "openssl-sys",
    "url",
    ]
    [[package]]
  • edit in Cargo.lock at line 3056
    [12.496306]
    [15.27011]
    "git2",
  • edit in Cargo.lock at line 3067
    [10.2063]
    [3.7675]
    "sanakirja",
  • edit in Cargo.lock at line 3092
    [2.44967]
    [2.45059]
    name = "libgit2-sys"
    version = "0.18.3+1.9.2"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
    dependencies = [
    "cc",
    "libc",
    "libssh2-sys",
    "libz-sys",
    "openssl-sys",
    "pkg-config",
    ]
    [[package]]
  • edit in Cargo.lock at line 3190
    [6.14803]
    [7.11818]
    ]
    [[package]]
    name = "libssh2-sys"
    version = "0.3.1"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
    dependencies = [
    "cc",
    "libc",
    "libz-sys",
    "openssl-sys",
    "pkg-config",
    "vcpkg",
    ]
    [[package]]
    name = "libz-sys"
    version = "1.1.23"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7"
    dependencies = [
    "cc",
    "libc",
    "pkg-config",
    "vcpkg",