git-remote-htree 0.2.81

Git remote helper for hashtree - push/pull git repos via nostr and hashtree
Documentation
use std::sync::{
    atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
    Arc,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum RepoTreeBuildPhase {
    Starting = 0,
    Objects = 1,
    Refs = 2,
    Info = 3,
    Head = 4,
    Index = 5,
    GitDir = 6,
    WorkingTree = 7,
    Root = 8,
    Done = 9,
}

impl RepoTreeBuildPhase {
    fn from_u8(value: u8) -> Self {
        match value {
            1 => Self::Objects,
            2 => Self::Refs,
            3 => Self::Info,
            4 => Self::Head,
            5 => Self::Index,
            6 => Self::GitDir,
            7 => Self::WorkingTree,
            8 => Self::Root,
            9 => Self::Done,
            _ => Self::Starting,
        }
    }

    fn label(self) -> &'static str {
        match self {
            Self::Starting => "starting",
            Self::Objects => "objects",
            Self::Refs => "refs",
            Self::Info => "info",
            Self::Head => "HEAD",
            Self::Index => "index",
            Self::GitDir => ".git",
            Self::WorkingTree => "working tree",
            Self::Root => "root",
            Self::Done => "done",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepoTreeBuildProgressSnapshot {
    pub phase: RepoTreeBuildPhase,
    pub processed: usize,
    pub total: Option<usize>,
    pub object_blobs: usize,
    pub object_prefixes: usize,
    pub files: usize,
    pub dirs: usize,
    pub reused: usize,
}

impl RepoTreeBuildProgressSnapshot {
    pub fn format_for_label(&self, label: &str) -> String {
        let mut text = format!("  {}: {}", label, self.phase.label());

        if self.phase != RepoTreeBuildPhase::Done {
            match self.total {
                Some(total) => {
                    text.push_str(&format!(
                        " {}/{}",
                        self.processed,
                        total.max(self.processed)
                    ));
                }
                None if self.processed > 0 => {
                    text.push_str(&format!(" {}", self.processed));
                }
                None => {}
            }
        }

        match self.phase {
            RepoTreeBuildPhase::Objects => {
                text.push_str(&format!(
                    " ({} object blobs, {} prefixes)",
                    self.object_blobs, self.object_prefixes
                ));
            }
            RepoTreeBuildPhase::Index => {
                text.push_str(" entries");
            }
            RepoTreeBuildPhase::WorkingTree => {
                text.push_str(&format!(
                    " ({} files, {} dirs, {} reused)",
                    self.files, self.dirs, self.reused
                ));
            }
            RepoTreeBuildPhase::Done => {
                text.push_str(&format!(
                    " ({} object blobs, {} files, {} dirs, {} reused)",
                    self.object_blobs, self.files, self.dirs, self.reused
                ));
            }
            _ => {}
        }

        text
    }
}

#[derive(Debug)]
struct RepoTreeBuildProgressState {
    phase: AtomicU8,
    processed: AtomicUsize,
    total: AtomicUsize,
    total_known: AtomicBool,
    object_blobs: AtomicUsize,
    object_prefixes: AtomicUsize,
    files: AtomicUsize,
    dirs: AtomicUsize,
    reused: AtomicUsize,
}

#[derive(Debug, Clone)]
pub struct RepoTreeBuildProgress {
    state: Arc<RepoTreeBuildProgressState>,
}

impl RepoTreeBuildProgress {
    pub fn new() -> Self {
        Self {
            state: Arc::new(RepoTreeBuildProgressState {
                phase: AtomicU8::new(RepoTreeBuildPhase::Starting as u8),
                processed: AtomicUsize::new(0),
                total: AtomicUsize::new(0),
                total_known: AtomicBool::new(false),
                object_blobs: AtomicUsize::new(0),
                object_prefixes: AtomicUsize::new(0),
                files: AtomicUsize::new(0),
                dirs: AtomicUsize::new(0),
                reused: AtomicUsize::new(0),
            }),
        }
    }

    pub(crate) fn start_phase(&self, phase: RepoTreeBuildPhase, total: Option<usize>) {
        match phase {
            RepoTreeBuildPhase::Objects => {
                self.state.object_blobs.store(0, Ordering::Relaxed);
                self.state.object_prefixes.store(0, Ordering::Relaxed);
            }
            RepoTreeBuildPhase::WorkingTree => {
                self.state.files.store(0, Ordering::Relaxed);
                self.state.dirs.store(0, Ordering::Relaxed);
                self.state.reused.store(0, Ordering::Relaxed);
            }
            _ => {}
        }

        self.state.processed.store(0, Ordering::Relaxed);
        match total {
            Some(total) => {
                self.state.total.store(total, Ordering::Relaxed);
                self.state.total_known.store(true, Ordering::Relaxed);
            }
            None => {
                self.state.total.store(0, Ordering::Relaxed);
                self.state.total_known.store(false, Ordering::Relaxed);
            }
        }
        self.state.phase.store(phase as u8, Ordering::Relaxed);
    }

    pub(crate) fn increment_processed(&self) {
        self.state.processed.fetch_add(1, Ordering::Relaxed);
    }

    pub(crate) fn record_object_blob(&self) {
        self.state.object_blobs.fetch_add(1, Ordering::Relaxed);
        self.increment_processed();
    }

    pub(crate) fn record_object_prefix(&self) {
        self.state.object_prefixes.fetch_add(1, Ordering::Relaxed);
    }

    pub(crate) fn record_index_entry(&self) {
        self.increment_processed();
    }

    pub(crate) fn record_working_file(&self, reused: bool) {
        self.state.files.fetch_add(1, Ordering::Relaxed);
        if reused {
            self.state.reused.fetch_add(1, Ordering::Relaxed);
        }
        self.increment_processed();
    }

    pub(crate) fn record_working_dir(&self, reused: bool) {
        self.state.dirs.fetch_add(1, Ordering::Relaxed);
        if reused {
            self.state.reused.fetch_add(1, Ordering::Relaxed);
        }
        self.increment_processed();
    }

    pub(crate) fn mark_done(&self) {
        self.state
            .phase
            .store(RepoTreeBuildPhase::Done as u8, Ordering::Relaxed);
    }

    pub fn snapshot(&self) -> RepoTreeBuildProgressSnapshot {
        RepoTreeBuildProgressSnapshot {
            phase: RepoTreeBuildPhase::from_u8(self.state.phase.load(Ordering::Relaxed)),
            processed: self.state.processed.load(Ordering::Relaxed),
            total: self
                .state
                .total_known
                .load(Ordering::Relaxed)
                .then(|| self.state.total.load(Ordering::Relaxed)),
            object_blobs: self.state.object_blobs.load(Ordering::Relaxed),
            object_prefixes: self.state.object_prefixes.load(Ordering::Relaxed),
            files: self.state.files.load(Ordering::Relaxed),
            dirs: self.state.dirs.load(Ordering::Relaxed),
            reused: self.state.reused.load(Ordering::Relaxed),
        }
    }
}

impl Default for RepoTreeBuildProgress {
    fn default() -> Self {
        Self::new()
    }
}