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;
use outpost_core::ops::list::{OutpostState, run};
use outpost_core::{OutpostError, SourceRepo};

#[test]
fn list_empty_source_returns_no_summaries() {
    let fixture = AbcFixture::new();
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    assert!(summaries.is_empty());
}

#[test]
fn list_reports_three_added_outpost_paths() {
    let fixture = AbcFixture::new();
    let one = fixture.add_outpost("C1").expect("add C1");
    let two = fixture.add_outpost("C2").expect("add C2");
    let three = fixture.add_outpost("C3").expect("add C3");
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    let paths = summaries
        .iter()
        .map(|summary| summary.path.clone())
        .collect::<Vec<_>>();
    assert_eq!(
        paths,
        vec![canonical(&one), canonical(&two), canonical(&three)]
    );
}

#[test]
fn list_includes_short_unique_outpost_id_prefixes() {
    let fixture = AbcFixture::new();
    fixture.add_outpost("C1").expect("add C1");
    fixture.add_outpost("C2").expect("add C2");
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    assert_eq!(summaries.len(), 2);
    for summary in summaries {
        assert_eq!(summary.display_id.len(), 5);
        assert!(summary.display_id.chars().all(|ch| ch.is_ascii_hexdigit()));
        assert!(
            summary
                .display_id
                .chars()
                .all(|ch| !ch.is_ascii_uppercase())
        );
    }
}

#[test]
fn list_reports_current_branch_for_each_outpost() {
    let fixture = AbcFixture::new();
    fixture.add_outpost("C").expect("add C");
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    assert_eq!(summaries.len(), 1);
    assert_eq!(
        summaries[0]
            .current_branch
            .as_ref()
            .expect("current branch")
            .as_str(),
        "main"
    );
    assert_eq!(summaries[0].state, OutpostState::Clean);
}

#[test]
fn list_reports_dirty_for_untracked_outpost_file() {
    let fixture = AbcFixture::new();
    fixture.dirty_outpost("C").expect("dirty C");
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    assert_eq!(summaries.len(), 1);
    assert_eq!(summaries[0].state, OutpostState::Dirty);
}

#[test]
fn list_reports_outpost_ahead_of_source() {
    let fixture = AbcFixture::new();
    let outpost = fixture.add_outpost("C").expect("add C");
    fixture
        .commit_in_outpost(&outpost, "outpost commit")
        .expect("commit in outpost");
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    assert_eq!(summaries.len(), 1);
    let ahead_behind = summaries[0].ahead_behind.expect("ahead behind");
    assert_eq!(ahead_behind.ahead, 1);
    assert_eq!(ahead_behind.behind, 0);
}

#[test]
fn list_reports_outpost_behind_source() {
    let fixture = AbcFixture::new();
    fixture.add_outpost("C").expect("add C");
    fixture
        .commit_in_source("source commit")
        .expect("commit in source");
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    assert_eq!(summaries.len(), 1);
    let ahead_behind = summaries[0].ahead_behind.expect("ahead behind");
    assert_eq!(ahead_behind.ahead, 0);
    assert_eq!(ahead_behind.behind, 1);
}

#[test]
fn list_reports_missing_registered_outpost() {
    let fixture = AbcFixture::new();
    let outpost = fixture.add_outpost("C").expect("add C");
    fs::remove_dir_all(&outpost).expect("remove outpost");
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    assert_eq!(summaries.len(), 1);
    assert_eq!(summaries[0].path, canonical_missing(&outpost));
    assert_eq!(summaries[0].state, OutpostState::Missing);
    assert!(summaries[0].current_branch.is_none());
}

#[test]
fn list_reports_not_managed_registered_path() {
    let fixture = AbcFixture::new();
    let outpost = fixture.add_outpost("C").expect("add C");
    fixture
        .invoker(&outpost)
        .run_check(["config", "--local", "--unset", "outpost.managed"])
        .expect("unset managed");
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    assert_eq!(summaries.len(), 1);
    assert_eq!(summaries[0].state, OutpostState::NotManaged);
    assert!(summaries[0].current_branch.is_none());
}

#[test]
fn list_reports_wrong_source_outpost_as_not_managed() {
    let fixture = AbcFixture::new();
    let outpost = fixture.add_outpost("C").expect("add C");
    fs::remove_dir_all(&outpost).expect("remove original outpost");
    let other = AbcFixture::new();
    let other_outpost = other.add_outpost("C").expect("add other C");
    fs::rename(&other_outpost, &outpost).expect("move wrong-source outpost");
    let source = fixture.source_repo().expect("source repo");

    let summaries = run(&source).expect("list summaries");

    assert_eq!(summaries.len(), 1);
    assert_eq!(summaries[0].state, OutpostState::NotManaged);
    assert!(summaries[0].current_branch.is_none());
}

#[test]
fn list_outside_source_repo_returns_not_a_repo() {
    let temp = tempfile::tempdir().expect("tempdir");

    let err = expect_error(
        SourceRepo::discover(temp.path()),
        "outside repo should fail",
    );

    assert!(matches!(err, OutpostError::NotARepo(path) if path == temp.path()));
}

#[test]
fn list_includes_lock_reason_from_registry() {
    let fixture = AbcFixture::new();
    let outpost = fixture.add_outpost("C").expect("add C");
    let source = fixture.source_repo().expect("source repo");
    let mut registry = source.registry_mut().expect("registry mut");
    registry
        .lock(&outpost, Some("release freeze".to_owned()))
        .expect("lock outpost");
    registry.save().expect("save registry");

    let summaries = run(&source).expect("list summaries");

    assert_eq!(summaries.len(), 1);
    assert!(summaries[0].locked);
    assert_eq!(summaries[0].lock_reason.as_deref(), Some("release freeze"));
}

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

fn canonical(path: &std::path::Path) -> std::path::PathBuf {
    fs::canonicalize(path).expect("canonical path")
}

fn canonical_missing(path: &std::path::Path) -> std::path::PathBuf {
    let parent = path.parent().expect("path parent");
    canonical(parent).join(path.file_name().expect("file name"))
}