stax 0.96.3

Fast stacked Git branches and PRs
Documentation
#![allow(clippy::result_large_err)]

use super::operation::report_operation;
use super::{
    CheckoutOutcome, OperationError, OperationErrorDetails, OperationErrorKind, OperationEvent,
    OperationOutcome, OperationProgress, OperationReceipt, OperationReporter, OperationRequest,
    OperationResult, OperationSideEffects, OperationStage, RepositorySession,
};
use crate::application::repository::MutationTargets;
use git2::BranchType;
use std::path::Path;

impl RepositorySession {
    pub fn checkout(&self, branch: &str, reporter: &mut dyn OperationReporter) -> OperationResult {
        let request = OperationRequest::Checkout {
            branch: branch.to_owned(),
        };
        report_operation(request.clone(), reporter, |reporter| {
            self.checkout_unframed(&request, branch, reporter)
        })
    }

    pub(super) fn checkout_unframed(
        &self,
        request: &OperationRequest,
        branch: &str,
        reporter: &mut dyn OperationReporter,
    ) -> OperationResult {
        self.with_mutation(request, MutationTargets::branches([branch]), || {
            checkout_explicit(self, request, branch, reporter)
        })
    }
}

fn checkout_explicit(
    session: &RepositorySession,
    request: &OperationRequest,
    branch: &str,
    reporter: &mut dyn OperationReporter,
) -> OperationResult {
    let branch = branch.trim();
    if branch.is_empty() {
        return Err(operation_error(
            request,
            OperationErrorKind::InvalidInput,
            OperationErrorDetails::None,
            "Branch name is required",
            "Choose a local branch and retry",
            "checkout branch input was empty",
            OperationSideEffects::None,
        ));
    }

    let repo = session.open_repo().map_err(|error| {
        OperationError::from_source(
            request.clone(),
            OperationErrorKind::RepositoryUnavailable,
            OperationErrorDetails::None,
            "Could not open the repository",
            "Check the repository path and retry",
            &error,
            None,
            OperationSideEffects::None,
        )
    })?;
    if repo.inner().find_branch(branch, BranchType::Local).is_err() {
        return Err(operation_error(
            request,
            OperationErrorKind::InvalidInput,
            OperationErrorDetails::Branch {
                branch: branch.to_string(),
            },
            format!("Branch '{branch}' does not exist locally"),
            "Choose an existing local branch and retry",
            "local branch lookup failed",
            OperationSideEffects::None,
        ));
    }

    if matches!(repo.current_branch(), Ok(current) if current == branch) {
        return Ok(checkout_receipt(
            request,
            CheckoutOutcome::AlreadyCurrent {
                branch: branch.to_string(),
            },
            OperationSideEffects::None,
        ));
    }

    reject_linked_worktree_checkout(session.repository_root(), request, branch, &repo)?;

    reporter.report(OperationEvent::Progress(OperationProgress {
        stage: OperationStage::CheckingOut,
        completed: 0,
        total: Some(1),
        branch: Some(branch.to_string()),
        message: format!("Checking out {branch}"),
    }));
    repo.checkout(branch).map_err(|error| {
        OperationError::from_source(
            request.clone(),
            OperationErrorKind::LocalGit,
            OperationErrorDetails::Branch {
                branch: branch.to_string(),
            },
            format!("Could not check out branch '{branch}'"),
            "Resolve the Git checkout error and retry",
            &error,
            None,
            OperationSideEffects::None,
        )
    })?;

    Ok(checkout_receipt(
        request,
        CheckoutOutcome::CheckedOut {
            branch: branch.to_string(),
        },
        OperationSideEffects::RepositoryChanged,
    ))
}

fn reject_linked_worktree_checkout(
    repository_root: &Path,
    request: &OperationRequest,
    branch: &str,
    repo: &crate::git::GitRepo,
) -> Result<(), OperationError> {
    let current_root = repository_root.canonicalize().map_err(|error| {
        OperationError::from_source(
            request.clone(),
            OperationErrorKind::RepositoryUnavailable,
            OperationErrorDetails::None,
            "Could not inspect the repository path",
            "Check the repository path and retry",
            &anyhow::Error::from(error).context("failed to canonicalize repository root"),
            None,
            OperationSideEffects::None,
        )
    })?;
    for worktree in repo.list_worktrees().map_err(|error| {
        OperationError::from_source(
            request.clone(),
            OperationErrorKind::LocalGit,
            OperationErrorDetails::None,
            "Could not inspect repository worktrees",
            "Resolve the Git worktree error and retry",
            &error,
            None,
            OperationSideEffects::None,
        )
    })? {
        if worktree.branch.as_deref() != Some(branch) {
            continue;
        }
        let path = worktree.path.canonicalize().unwrap_or(worktree.path);
        if path == current_root {
            continue;
        }
        return Err(operation_error(
            request,
            OperationErrorKind::PreconditionFailed,
            OperationErrorDetails::AlreadyCheckedOutElsewhere {
                branch: branch.to_string(),
                path,
            },
            format!("Branch '{branch}' is checked out in another worktree"),
            "Switch that worktree to another branch or remove it, then retry",
            "target branch is already checked out in a linked worktree",
            OperationSideEffects::None,
        ));
    }
    Ok(())
}

fn checkout_receipt(
    request: &OperationRequest,
    outcome: CheckoutOutcome,
    side_effects: OperationSideEffects,
) -> OperationReceipt {
    let branch = match &outcome {
        CheckoutOutcome::CheckedOut { branch } | CheckoutOutcome::AlreadyCurrent { branch } => {
            branch.clone()
        }
    };
    let summary = match &outcome {
        CheckoutOutcome::CheckedOut { branch } => format!("Checked out {branch}"),
        CheckoutOutcome::AlreadyCurrent { branch } => format!("Already on {branch}"),
    };
    OperationReceipt {
        request: request.clone(),
        summary,
        affected_branches: vec![branch],
        outcome: OperationOutcome::Checkout(outcome),
        transaction: None,
        warnings: Vec::new(),
        side_effects,
    }
}

fn operation_error(
    request: &OperationRequest,
    kind: OperationErrorKind,
    details: OperationErrorDetails,
    primary: impl Into<String>,
    action: impl Into<String>,
    diagnostic_chain: impl Into<String>,
    side_effects: OperationSideEffects,
) -> OperationError {
    OperationError {
        request: request.clone(),
        kind,
        details,
        primary: primary.into(),
        action: action.into(),
        diagnostic_chain: diagnostic_chain.into(),
        receipt: None,
        side_effects,
    }
}