git-branchless 0.6.0

Branchless workflow for Git
Documentation
use std::fmt::Write;
use std::{collections::HashSet, time::SystemTime};

use itertools::Itertools;
use lib::git::{CategorizedReferenceName, MaybeZeroOid};
use lib::{
    core::{
        effects::{Effects, OperationType},
        eventlog::{Event, EventLogDb, EventReplayer},
        formatting::Pluralize,
    },
    git::Repo,
    util::ExitCode,
};

pub fn repair(effects: &Effects, dry_run: bool) -> eyre::Result<ExitCode> {
    let repo = Repo::from_current_dir()?;
    let conn = repo.get_db_conn()?;
    let event_log_db = EventLogDb::new(&conn)?;
    let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?;
    let event_cursor = event_replayer.make_default_cursor();

    let broken_commits = {
        let (effects, progress) = effects.start_operation(OperationType::RepairCommits);
        let _effects = effects;
        let cursor_oids = event_replayer.get_cursor_oids(event_cursor);
        progress.notify_progress(0, cursor_oids.len());
        let mut result = HashSet::new();
        for oid in cursor_oids {
            if repo.find_commit(oid)?.is_none() {
                result.insert(oid);
            }
            progress.notify_progress_inc(1);
        }
        result
    };

    let broken_branches = {
        let (effects, progress) = effects.start_operation(OperationType::RepairBranches);
        let _effects = effects;
        let references_snapshot = event_replayer.get_references_snapshot(&repo, event_cursor)?;
        let branch_names = references_snapshot
            .branch_oid_to_names
            .into_iter()
            .flat_map(|(oid, reference_names)| {
                reference_names
                    .into_iter()
                    .map(move |reference_name| (oid, reference_name))
            })
            .collect_vec();
        progress.notify_progress(0, branch_names.len());
        let mut result = HashSet::new();
        for (oid, reference_name) in branch_names {
            if repo.find_reference(&reference_name)?.is_none() {
                result.insert((oid, reference_name));
            }
            progress.notify_progress_inc(1);
        }
        result
    };

    let now = SystemTime::now();
    let timestamp = now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs_f64();
    let event_tx_id = event_log_db.make_transaction_id(now, "repair")?;

    let num_broken_commits = broken_commits.len();
    let commit_events = broken_commits
        .iter()
        .map(|commit_oid| Event::ObsoleteEvent {
            timestamp,
            event_tx_id,
            commit_oid: *commit_oid,
        })
        .collect_vec();
    let num_broken_branches = broken_branches.len();
    let branch_events =
        broken_branches
            .iter()
            .map(|(old_oid, reference_name)| Event::RefUpdateEvent {
                timestamp,
                event_tx_id,
                ref_name: reference_name.to_owned(),
                old_oid: MaybeZeroOid::NonZero(*old_oid),
                new_oid: MaybeZeroOid::Zero,
                message: None,
            });

    if !dry_run {
        let events = commit_events.into_iter().chain(branch_events).collect_vec();
        event_log_db.add_events(events)?;
    }

    if num_broken_commits > 0 {
        writeln!(
            effects.get_output_stream(),
            "Found and repaired {}: {}",
            Pluralize {
                determiner: None,
                amount: num_broken_commits,
                unit: ("broken commit", "broken commits")
            },
            broken_commits.into_iter().sorted().join(", "),
        )?;
    }
    if num_broken_branches > 0 {
        writeln!(
            effects.get_output_stream(),
            "Found and repaired {}: {}",
            Pluralize {
                determiner: None,
                amount: num_broken_branches,
                unit: ("broken branch", "broken branches")
            },
            broken_branches
                .into_iter()
                .map(
                    |(_oid, reference_name)| CategorizedReferenceName::new(&reference_name)
                        .render_suffix()
                )
                .sorted()
                .join(", "),
        )?;
    }

    if dry_run {
        writeln!(
            effects.get_output_stream(),
            "(This was a dry-run; run with --no-dry-run to apply changes.)"
        )?;
    }

    Ok(ExitCode(0))
}