monochromatic-git-policy-cli 0.6.0

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
//! What:
//!  The merge, cherry-pick and revert conclusion state copied into the shadow at preparation,
//!       and into the transaction's comparison copies.
//! Why:
//!  Native `git commit` in the shadow reads the conclusion state the real worktree holds at invocation.
//!  Landing later compares the real entries with these copies, so an entry changed by another command
//!  after invocation is kept (`conclusion_cleanup.rs` holds the landing side).
//!  Pseudorefs of a reftable repository live in the ref store and are copied with `update-ref`
//!  (`src/shadow-repository/shadow-conclusion-state.ts`).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await copyConclusionState({ gitPath, cwd, gitDir, shadowPath, transactionDirectory, refFormat });
//! ```

/// Names and helpers the landing side already uses.
use crate::conclusion_cleanup::{
    CONCLUSION_COPY_DIRECTORY, CONCLUSION_STATE_FILES, SEQUENCER_DIRECTORY, STORE_RECORD_FILENAME,
    is_store_held, resolve_pseudoref,
};
/// Private file writes and directory creation.
use crate::private_storage::{create_private_directory, write_private_file};
/// Optional reads that treat a missing entry as absent.
use crate::recovery_files::{path_present, read_optional};
/// The error of a recovery-file read, whose text becomes a Git failure.
use crate::recovery_error::RecoveryError;
/// JSON object writer for the store-held record.
use crate::json_record::ObjectWriter;
/// The shadow ref helper and the process context.
use crate::shadow_git::shadow_request;
use crate::transaction_git::{GitContext, GitFailure, GitRequest, run_git_checked};
/// Ref storage backend.
use crate::transaction_journal::RefFormat;
/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
use std::path::{Path, PathBuf};

/// What:
///  Turns the result of one filesystem step into a Git failure that names the step.
///  `std::io::Result<T>` is "the value, or an input/output error".
/// Why:
///   Every step of `copy_conclusion_state` reports its failure as `<step> failed: <error>`, and a named function
///  stands where a closure that built that failure stood.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function ioStep<T>(step: string, attempt: () => T): T; // throws GitFailure(`${step} failed: ${error}`)
/// ```
fn io_step<T>(step: &str, result: std::io::Result<T>) -> Result<T, GitFailure> {
    return match result {
        Ok(value) => Ok(value),
        Err(error) => Err(GitFailure(format!("{step} failed: {error}"))),
    };
}

/// What:
///  Turns the result of one recovery-file read into a Git failure that carries the recovery error's text.
/// Why:
///   Reads of optional and present entries report their failure the same way, and a named function stands where a
///  closure that unwrapped the error text stood.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function recoveryStep<T>(attempt: () => T): T; // throws GitFailure(error.message)
/// ```
fn recovery_step<T>(result: Result<T, RecoveryError>) -> Result<T, GitFailure> {
    return match result {
        Ok(value) => Ok(value),
        Err(error) => Err(GitFailure(error.0)),
    };
}

/// What:
///  Copies a directory tree, keeping links as links.
/// Why:
///   `cp(..., { recursive: true })` copies the sequencer directory into the shadow and the copies.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await cp(source, destination, { recursive: true });
/// ```
fn copy_tree(source: &Path, destination: &Path) -> std::io::Result<()> {
    create_private_directory(destination)?;
    for entry_result in std::fs::read_dir(source)? {
        let entry: std::fs::DirEntry = entry_result?;
        let target: PathBuf = destination.join(entry.file_name());
        let kind: std::fs::FileType = entry.file_type()?;
        if kind.is_dir() {
            copy_tree(entry.path().as_path(), target.as_path())?;
        } else if kind.is_symlink() {
            #[cfg(unix)]
            std::os::unix::fs::symlink(std::fs::read_link(entry.path())?, &target)?;
            // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
            #[cfg(not(unix))]
            std::fs::copy(entry.path(), &target).map(|_| ())?;
        } else {
            std::fs::copy(entry.path(), &target)?;
        }
    }
    return Ok(());
}

/// What:
///  Reads one pseudoref's value in the owning worktree, if it resolves.
/// Why:
///   A reftable pseudoref has no file to copy, so its value is copied through the ref store.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await resolvePseudoref({ gitPath, cwd, name });
/// ```
fn owning_pseudoref(context: &GitContext, cwd: &Path, name: &str) -> Option<String> {
    let mut request: GitRequest = GitRequest::new(cwd, &["rev-parse", "--verify", "--quiet", name]);
    request.without_prefix = true;
    return resolve_pseudoref(context, request);
}

/// What:
///  Copies every present conclusion entry of the owning worktree into the shadow and the transaction.
/// Why:
///   The shadow copy is what native commit reads; the transaction copy is what landing compares against.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await copyConclusionState({ gitPath, cwd, gitDir, shadowPath, transactionDirectory, refFormat });
/// ```
pub fn copy_conclusion_state(
    context: &GitContext,
    cwd: &Path,
    git_dir: &Path,
    shadow: &Path,
    transaction_directory: &Path,
    ref_format: RefFormat,
) -> Result<(), GitFailure> {
    let copies: PathBuf = transaction_directory.join(CONCLUSION_COPY_DIRECTORY);
    io_step("creating the conclusion copies", create_private_directory(copies.as_path()))?;
    // Store-held values in the order of the copied names, as the incumbent's JSON record lists them.
    let mut store_held: ObjectWriter = ObjectWriter::new();
    for name in CONCLUSION_STATE_FILES {
        if is_store_held(ref_format, name) {
            if let Some(value) = owning_pseudoref(context, cwd, name) {
                run_git_checked(
                    context,
                    &shadow_request(shadow, shadow, &["update-ref", name, value.as_str()]),
                )?;
                store_held.string(name, value.as_str());
            }
            continue;
        }
        let Some(bytes) = recovery_step(read_optional(git_dir.join(name).as_path()))? else {
            continue;
        };
        io_step(
            "copying a conclusion entry into the shadow",
            write_private_file(shadow.join(name).as_path(), bytes.as_slice()),
        )?;
        io_step(
            "copying a conclusion entry for comparison",
            write_private_file(copies.join(name).as_path(), bytes.as_slice()),
        )?;
    }
    let record: String = store_held.finish();
    io_step(
        "writing the store-held record",
        write_private_file(copies.join(STORE_RECORD_FILENAME).as_path(), format!("{record}\n").as_bytes()),
    )?;
    let sequencer: PathBuf = git_dir.join(SEQUENCER_DIRECTORY);
    if recovery_step(path_present(sequencer.as_path()))? {
        io_step(
            "copying the sequencer into the shadow",
            copy_tree(sequencer.as_path(), shadow.join(SEQUENCER_DIRECTORY).as_path()),
        )?;
        io_step(
            "copying the sequencer for comparison",
            copy_tree(sequencer.as_path(), copies.join(SEQUENCER_DIRECTORY).as_path()),
        )?;
    }
    return Ok(());
}

/// Conclusion copy controls stay out of the release executable.
#[cfg(test)]
#[path = "conclusion_copy_tests.rs"]
mod tests;