outpost-core 0.1.3

Core library for Git Outpost, a clone-backed alternative to git worktree workflows.
Documentation
#[allow(dead_code)]
mod common;

use std::fs;

use common::fixture::{AbcFixture, CapturingReporter};
use outpost_core::ops::source::{SourcePullOptions, pull};
use outpost_core::{BranchName, Outpost, OutpostError, OutpostResult, StepKind};

#[test]
fn sp01_source_pull_fast_forwards_unchecked_out_source_branch_without_switching() {
    let fixture = AbcFixture::new();
    let feature = fixture
        .create_source_branch("feature/source-refresh")
        .expect("create source branch");
    fixture.push_source_branch(&feature).expect("push feature");
    let outpost_path = fixture
        .add_outpost_on_branch("C", Some(feature.clone()))
        .expect("add feature outpost");
    fixture
        .invoker(&fixture.source)
        .run_check(["switch", "main"])
        .expect("switch source back to main");
    let upstream_oid = fixture
        .commit_file_in_upstream(
            feature.as_str(),
            "advance feature upstream",
            "feature.txt",
            "from upstream\n",
        )
        .expect("upstream feature commit");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let report = pull(
        &outpost,
        SourcePullOptions {
            branch: feature.clone(),
        },
        &mut reporter,
    )
    .expect("source pull");

    assert!(report.updated);
    assert_eq!(report.branch.as_str(), feature.as_str());
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/feature/source-refresh")
            .expect("feature source oid"),
        upstream_oid
    );
    assert_eq!(
        fixture
            .current_branch_name(&fixture.source)
            .expect("source current branch"),
        "main"
    );
}

#[test]
fn sp02_source_pull_updates_checked_out_source_worktree() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    let upstream_oid = fixture
        .commit_file_in_upstream(
            "main",
            "advance main upstream",
            "main.txt",
            "from upstream\n",
        )
        .expect("upstream main commit");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let report = pull(
        &outpost,
        SourcePullOptions {
            branch: branch("main"),
        },
        &mut reporter,
    )
    .expect("source pull");

    assert!(report.updated);
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "HEAD")
            .expect("source HEAD oid"),
        upstream_oid
    );
    assert_eq!(
        fs::read_to_string(fixture.source.join("main.txt")).expect("source worktree file"),
        "from upstream\n"
    );
}

#[test]
fn sp03_source_pull_returns_divergence_when_source_and_origin_diverge() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    fixture
        .commit_file_in_source("source side", "source.txt", "from source\n")
        .expect("source commit");
    fixture
        .commit_file_in_upstream("main", "origin side", "origin.txt", "from origin\n")
        .expect("upstream commit");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        pull(
            &outpost,
            SourcePullOptions {
                branch: branch("main"),
            },
            &mut reporter,
        ),
        "source pull should reject divergence",
    );

    assert!(matches!(err, OutpostError::Divergence { branch } if branch == "main"));
}

#[test]
fn sp04_source_pull_missing_branch_returns_branch_not_found() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    let outpost = outpost(&fixture, &outpost_path);
    let missing = branch("feature/missing");
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        pull(
            &outpost,
            SourcePullOptions {
                branch: missing.clone(),
            },
            &mut reporter,
        ),
        "source pull should reject missing branch",
    );

    assert!(
        matches!(err, OutpostError::BranchNotFound { branch, repo } if branch == missing.as_str() && repo == canonical_source(&fixture))
    );
    assert!(reporter.steps.is_empty());
}

#[test]
fn sp05_source_pull_records_source_fetch_event() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    fixture
        .commit_in_upstream("main", "advance upstream")
        .expect("upstream commit");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    pull(
        &outpost,
        SourcePullOptions {
            branch: branch("main"),
        },
        &mut reporter,
    )
    .expect("source pull");

    assert_eq!(
        reporter.step_kinds().first().copied(),
        Some(StepKind::SourceFetch)
    );
    assert!(
        reporter.warnings.is_empty(),
        "source pull should not warn on baseline path: {:?}",
        reporter.warnings
    );
}

fn outpost(fixture: &AbcFixture, path: &std::path::Path) -> Outpost {
    Outpost::at_with(path, &fixture.git_env).expect("open outpost")
}

fn branch(name: &str) -> BranchName {
    BranchName::parse(name).expect("branch name")
}

fn canonical_source(fixture: &AbcFixture) -> std::path::PathBuf {
    fs::canonicalize(&fixture.source).expect("canonical source")
}

fn expect_error<T>(result: OutpostResult<T>, message: &str) -> OutpostError {
    match result {
        Ok(_) => panic!("{message}"),
        Err(err) => err,
    }
}