monochromatic-git-policy-cli 0.5.23

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
//! What:
//!  The linked worktree a `git worktree add` registered at the destination its command line names:
//!  whether that destination was a registered worktree before real Git ran, and whether it is one registered by this
//!  command after Git returned, with the canonical root of every registered worktree.
//! Why:
//!  The destination is read from the command line and its registration is found by following the pointer in its `.git`
//!  file, so a concurrent add in the same repository can neither be mistaken for this one's worktree nor make this one wait
//!  (the earlier comparison of the whole set of registrations needed a lock to keep other adds out of its window).
//!  A worktree that Git registered before failing still receives the ignored files,
//!  so the check runs after Git returns whatever its status was.
//!  The main worktree is observed only through `observe_copy_source`, which the caller reaches only for a command that
//!  creates a worktree, so its other invocations cost no filesystem reads.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const observation = await observeWorktreeRepository({ args, gitPath });
//! const { created, registeredRoots } = await findCreatedWorktree({ observation, destination, registeredBefore, gitPath });
//! ```

use super::git_metadata::path_from_git_bytes;
use super::transaction_git::{GitContext, GitRequest, run_git};
use super::worktree_add_binding::bind_registration;
use super::worktree_copy_error::{WorktreeCopyError, raw_failure};
use super::worktree_copy_selection::CopySelection;
use super::worktree_identity::WorktreeIdentity;
use std::path::{Path, PathBuf};

/// What:
///  The facts captured before real Git runs for an applicable invocation.
/// Why:
///   The copy after Git needs the common directory that owns the journals, the source worktree and which of its ignored
///  paths are taken; nothing of the repository's other worktrees is read, so a concurrent add is none of its business.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type WorktreeCopyObservation = { commonDir; effectiveCwd; sourceRoot?; selection };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorktreeCopyObservation {
    /// Canonical common Git directory, which owns the journals.
    pub common_dir: PathBuf,
    /// Working directory the Git queries run in.
    pub effective_cwd: PathBuf,
    /// Canonical source worktree root;
    ///  absent for a bare repository.
    pub source_root: Option<PathBuf>,
    /// Which of the source's ignored paths the copy takes.
    pub selection: CopySelection,
}

/// What:
///  One linked worktree this command registered.
/// Why:
///   Its root is where the copy lands.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type CreatedWorktree = { root: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CreatedWorktree {
    /// Canonical created worktree root.
    pub root: PathBuf,
}

/// What:
///  The worktree this command created, if any, and every registered root that recursive copying must exclude.
/// Why:
///   A nested registered worktree is never copied into its parent's copy.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// { created: CreatedWorktree[]; registeredRoots: string[] }
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CreatedWorktrees {
    /// The linked worktree registered at the destination, absent when this command registered none.
    pub created: Vec<CreatedWorktree>,
    /// Every registered root that exists on the filesystem.
    pub registered_roots: Vec<PathBuf>,
}

/// What:
///  Captures what the copy after Git needs for one repository identity.
/// Why:
///   The main worktree and a repository outside any worktree return `None` before any read,
///  so their invocations stay on the plain forwarding path.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function observeWorktreeRepository({ args, gitPath }): Promise<WorktreeCopyObservation | typeof WORKTREE_COPY_NOT_APPLICABLE>;
/// ```
pub fn observe_worktree_repository(
    identity: &WorktreeIdentity,
    effective_cwd: &Path,
) -> Result<Option<WorktreeCopyObservation>, WorktreeCopyError> {
    // `match` decides applicability from the identity alone; only the two repository kinds are observed.
    match identity {
        WorktreeIdentity::OutsideWorktree | WorktreeIdentity::MainWorktree { .. } => {
            return Ok(None);
        }
        WorktreeIdentity::BareRepository { common_dir, .. } => {
            return Ok(Some(observation(common_dir.as_path(), None, effective_cwd, CopySelection::Ignored)));
        }
        WorktreeIdentity::LinkedWorktree {
            common_dir,
            worktree_root,
            ..
        } => {
            return Ok(Some(observation(
                common_dir.as_path(),
                Some(worktree_root.as_path()),
                effective_cwd,
                CopySelection::Ignored,
            )));
        }
    }
}

/// What:
///  The observation that also covers a main checkout, which takes part only when the caller's command creates a worktree.
/// Why:
///   A main checkout hands over its ignored state under its leave-out rule, so a worktree added from it works at once;
///  the caller observes it only for a creating command, so the main checkout's other invocations cost no filesystem reads.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// observeCopySource({ identity, effectiveCwd }): Promise<WorktreeCopyObservation | typeof WORKTREE_COPY_NOT_APPLICABLE>;
/// ```
pub fn observe_copy_source(
    identity: &WorktreeIdentity,
    effective_cwd: &Path,
) -> Result<Option<WorktreeCopyObservation>, WorktreeCopyError> {
    // `match`: a main checkout takes the main-checkout selection; every other shape keeps the full listing.
    match identity {
        WorktreeIdentity::MainWorktree {
            common_dir,
            worktree_root,
            ..
        } => {
            return Ok(Some(observation(
                common_dir.as_path(),
                Some(worktree_root.as_path()),
                effective_cwd,
                CopySelection::MainCheckout,
            )));
        }
        _ => return observe_worktree_repository(identity, effective_cwd),
    }
}

/// What:
///  The observation of one repository with its optional source worktree.
/// Why:
///   Both repository kinds share it; only the source differs.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// { commonDir, effectiveCwd, sourceRoot, selection }
/// ```
fn observation(
    common_dir: &Path,
    source_root: Option<&Path>,
    effective_cwd: &Path,
    selection: CopySelection,
) -> WorktreeCopyObservation {
    return WorktreeCopyObservation {
        common_dir: common_dir.to_path_buf(),
        effective_cwd: effective_cwd.to_path_buf(),
        source_root: source_root.map(Path::to_path_buf),
        selection,
    };
}

/// What:
///  Whether `destination` is a registered worktree of the repository right now.
/// Why:
///   It is asked before real Git runs: a destination already registered then is not this command's creation, whatever Git
///  does with it. The answer follows the pointer in the destination's `.git` file and checks that the registration it names
///  names the destination back, so no other worktree is read.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function destinationIsRegistered(commonDir: string, destination: string): boolean;
/// ```
pub fn destination_is_registered(common_dir: &Path, destination: &Path) -> bool {
    return bind_registration(common_dir, destination).is_ok();
}

/// What:
///  The canonical root of a Git-reported worktree path,
///  or `None` when Git still lists a root that no longer exists.
/// Why:
///   Git keeps a registration after its directory is deleted;
///  such a root cannot be copied into or excluded by identity, so it is skipped.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function canonicalRegisteredRoot(root: string): Promise<string | typeof REGISTERED_ROOT_MISSING>;
/// ```
fn canonical_registered_root(root: &Path) -> Result<Option<PathBuf>, WorktreeCopyError> {
    match std::fs::canonicalize(root) {
        Ok(canonical) => return Ok(Some(canonical)),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(error) => return Err(raw_failure(&error)),
    }
}

/// What:
///  The roots of the worktrees Git lists right now, those that exist.
/// Why:
///   Git's porcelain listing with NUL separators is the one authority for registered roots,
///  so the copy never guesses a root from the administrative directory name.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function readRegisteredRoots({ observation, gitPath }): Promise<string[]>;
/// ```
fn read_registered_roots(
    context: &GitContext,
    observation: &WorktreeCopyObservation,
) -> Result<Vec<PathBuf>, WorktreeCopyError> {
    let request: GitRequest = GitRequest {
        cwd: observation.effective_cwd.clone(),
        arguments: vec![
            "--git-dir".into(),
            observation.common_dir.as_os_str().to_os_string(),
            "worktree".into(),
            "list".into(),
            "--porcelain".into(),
            "-z".into(),
        ],
        without_prefix: true,
        ..GitRequest::default()
    };
    let output = run_git(context, &request).map_err(|error| return raw_failure(&error))?;
    if !output.succeeded() {
        return Err(raw_failure(&String::from_utf8_lossy(
            output.stderr.as_slice(),
        )));
    }
    let mut roots: Vec<PathBuf> = Vec::new();
    // `filter_map` keeps only fields of the `worktree <path>` form.
    for path in output
        .stdout
        .split(|byte| return *byte == 0)
        .filter_map(|field| return field.strip_prefix(b"worktree "))
        .filter_map(path_from_git_bytes)
    {
        if let Some(canonical) = canonical_registered_root(path.as_path())? {
            roots.push(canonical);
        }
    }
    return Ok(roots);
}

/// What:
///  The worktree this command registered at `destination`, with every registered root.
/// Why:
///   Runs after real Git returned, whatever its status was, so a worktree Git registered before failing still counts.
///  It is this command's when the destination was not registered before Git ran and is registered now: the destination is
///  the one the command line named, so another add's worktree is never taken for it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function findCreatedWorktree({ observation, destination, registeredBefore, gitPath }): Promise<{ created; registeredRoots }>;
/// ```
pub fn find_created_worktree(
    context: &GitContext,
    observation: &WorktreeCopyObservation,
    destination: &Path,
    registered_before: bool,
) -> Result<CreatedWorktrees, WorktreeCopyError> {
    if registered_before || !destination_is_registered(observation.common_dir.as_path(), destination) {
        return Ok(CreatedWorktrees {
            created: Vec::new(),
            registered_roots: Vec::new(),
        });
    }
    return Ok(CreatedWorktrees {
        created: vec![CreatedWorktree { root: destination.to_path_buf() }],
        registered_roots: read_registered_roots(context, observation)?,
    });
}

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