monochromatic-git-policy-cli 0.5.5

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
//! What:
//!  The push of a merge that integrated a lost race, planned and run inside the running wrapper.
//! Why:
//!  The merge push must pass every gate a typed `git push` passes, so the policies read the integrated result.
//!  An earlier version started the wrapper's own executable with a marker variable to get those gates; the wrapper
//!  never starts itself, so the same planning (`plan_invocation`) now runs in this process, with the marker set only
//!  in the environment handed to the planner, never in the process environment, and the planned action is run here.
//!  The marker stops that planned push from stacking again.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const action = planInvocation(['push', ...tokens, remote, `${mergeOid}:${ref}`], { ...env, CLI_GIT_STACKED_PUSH: '1' });
//! return await runAction(action);
//! ```

/// The coordinator's Git and its error type.
use super::auto_push_single_flight::{PushGit, SingleFlightError};
/// The planned action and the engine failure status.
use super::action::{Action, ENGINE_FAILURE_EXIT_CODE};
/// The diagnostic log of the wrapper.
use super::diagnostic_log::debug;
/// The planner of an invocation.
use super::entry::plan_invocation;
/// The pushes run as children.
use super::push_stacking::{STACKED_PUSH_MARKER, run_stacked_push};
/// The facts a planner needs about this process.
use super::real_git::process_resolution_inputs;
/// The settled or plain run of the planned Git.
use super::worktree_copy_forward::{ForwardedRun, run_forwarded_git};
/// Owned OS strings for Git arguments.
use std::ffi::OsString;
/// Standard streams as writable values.
use std::io::Write;

/// Tag used for diagnostics from this module.
const TAG: &str = "cli-git";

/// What:
///  The environment handed to the planner: the coordinator's complete environment with the stacking marker set once.
/// Why:
///   A marker the caller already had must not appear twice, because the first entry would win a lookup.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// ({ ...environment, CLI_GIT_STACKED_PUSH: '1' })
/// ```
fn marked_environment(environment: &[(OsString, OsString)]) -> Vec<(OsString, OsString)> {
    let mut marked: Vec<(OsString, OsString)> = environment
        .iter()
        .filter(|(name, _)| {
            return name.as_os_str() != STACKED_PUSH_MARKER;
        })
        .cloned()
        .collect();
    marked.push((OsString::from(STACKED_PUSH_MARKER), OsString::from("1")));
    return marked;
}

/// What:
///  Run a planned action to its end as a child of this process and return its status.
/// Why:
///   Only the actions a marked push can plan are handled: a plain run, a settled run, a push child, or a refusal;
///  anything else is a planning bug and ends with the engine failure status and a plain message.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function runAction(action: Action): Promise<number>;
/// ```
fn run_planned(action: Action) -> i32 {
    // Git and its hooks see the marker, as they did when the merge push was a separate process.
    let marker = (OsString::from(STACKED_PUSH_MARKER), OsString::from("1"));
    let write_stderr = |text: &str| {
        // Terminal output error: nowhere is left to report a failed diagnostic write.
        let _written = std::io::stderr().write_all(text.as_bytes());
    };
    let ran = |run: ForwardedRun| -> i32 {
        return match run {
            ForwardedRun::Ran(code) => code,
            ForwardedRun::Failed { stderr, code } => {
                write_stderr(stderr.as_str());
                code
            }
        };
    };
    match action {
        Action::Forward { real_git, arguments, mut overlay, stderr } => {
            write_stderr(stderr.as_str());
            overlay.push(marker);
            return ran(run_forwarded_git(real_git.as_path(), arguments.as_slice(), overlay.as_slice(), None));
        }
        Action::ForwardSettled { real_git, arguments, mut overlay, copy, stderr } => {
            write_stderr(stderr.as_str());
            overlay.push(marker);
            return ran(run_forwarded_git(real_git.as_path(), arguments.as_slice(), overlay.as_slice(), Some(&*copy)));
        }
        Action::ForwardPush { mut forward, stderr } => {
            write_stderr(stderr.as_str());
            forward.overlay.push(marker);
            return run_stacked_push(&forward);
        }
        Action::Exit { code, stdout, stderr } => {
            // Terminal output error: the exit status carries the result.
            let _written = std::io::stdout().write_all(stdout.as_bytes());
            write_stderr(stderr.as_str());
            return code;
        }
        other => {
            debug(TAG, format!("a merge push planned an action it cannot run: {other:?}").as_str());
            write_stderr("cli-git: the merge push could not be planned; nothing was pushed.\n");
            return ENGINE_FAILURE_EXIT_CODE;
        }
    }
}

/// What:
///  Push a merge through the push gates of this process and report Git's status.
/// Why:
///   The status is what the stacking loop reads; Git's own output went to the caller's streams.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function gatedMergePush(git, globalPrefix, tokens, [remote, refspec]): Promise<number>; // throws SingleFlightError
/// ```
///
/// # Errors
/// The facts of this process could not be read.
pub fn gated_merge_push(
    git: &PushGit,
    global_prefix: &[OsString],
    tokens: &[OsString],
    destination: &[String],
) -> Result<i32, SingleFlightError> {
    let mut arguments: Vec<OsString> = global_prefix.to_vec();
    arguments.push(OsString::from("push"));
    arguments.extend(tokens.iter().cloned());
    arguments.extend(destination.iter().map(OsString::from));
    let environment: Vec<(OsString, OsString)> = marked_environment(git.environment.as_slice());
    let inputs = process_resolution_inputs(environment.as_slice()).map_err(SingleFlightError::Io)?;
    let action: Action = plan_invocation(arguments.as_slice(), environment.as_slice(), &inputs);
    return Ok(run_planned(action));
}