grit-lib 0.5.19

Core library for the grit Git implementation
Documentation
//! `list_worktrees` reports current, lock, and prunable fields like `git worktree list --porcelain`.

use grit_lib::config::environment::RepositoryOptions;
use grit_lib::config::git_path::path_for_disk_compare;
use grit_lib::repo::Repository;
use grit_lib::worktree::{list_worktrees, worktree_admin_dir, PrunableReason};
use grit_test_support::git;
use std::collections::HashMap;
use std::path::{Path, PathBuf};

#[derive(Debug, Default)]
struct GitPorcelainRow {
    locked: bool,
    lock_reason: Option<String>,
    prunable_reason: Option<String>,
}

fn parse_git_worktree_porcelain(text: &str) -> HashMap<PathBuf, GitPorcelainRow> {
    let mut map = HashMap::new();
    let mut current_path: Option<PathBuf> = None;
    let mut row = GitPorcelainRow::default();
    for line in text.lines() {
        if line.is_empty() {
            if let Some(path) = current_path.take() {
                map.insert(path, row);
                row = GitPorcelainRow::default();
            }
            continue;
        }
        if let Some(rest) = line.strip_prefix("worktree ") {
            if let Some(path) = current_path.take() {
                map.insert(path, row);
                row = GitPorcelainRow::default();
            }
            current_path = Some(path_for_disk_compare(Path::new(rest)));
            continue;
        }
        if line == "locked" {
            row.locked = true;
            row.lock_reason = None;
            continue;
        }
        if let Some(reason) = line.strip_prefix("locked ") {
            row.locked = true;
            row.lock_reason = Some(reason.to_owned());
            continue;
        }
        if let Some(reason) = line.strip_prefix("prunable ") {
            row.prunable_reason = Some(reason.to_owned());
        }
    }
    if let Some(path) = current_path.take() {
        map.insert(path, row);
    }
    map
}

fn open_grit(root: &Path) -> Repository {
    Repository::discover(Some(root), &RepositoryOptions::empty()).expect("discover")
}

fn assert_list_matches_git(open_from: &Path, main: &Path, expected_current_path: &Path) {
    let git_rows = parse_git_worktree_porcelain(&git(main, &["worktree", "list", "--porcelain"]));
    let repo = open_grit(open_from);
    let grit_list = list_worktrees(&repo).expect("list");
    for entry in &grit_list {
        let git_row = git_rows
            .get(&entry.path)
            .unwrap_or_else(|| panic!("git missing path {}", entry.path.display()));
        assert_eq!(
            entry.is_locked,
            git_row.locked,
            "lock flag for {}",
            entry.path.display()
        );
        assert_eq!(
            entry.lock_reason.as_deref(),
            git_row.lock_reason.as_deref(),
            "lock reason for {}",
            entry.path.display()
        );
        let grit_prunable = entry
            .prunable_reason
            .map(PrunableReason::porcelain_message)
            .map(str::to_owned);
        assert_eq!(
            grit_prunable.as_deref(),
            git_row.prunable_reason.as_deref(),
            "prunable reason for {}",
            entry.path.display()
        );
        assert_eq!(
            entry.is_prunable,
            git_row.prunable_reason.is_some(),
            "is_prunable for {}",
            entry.path.display()
        );
    }

    assert_eq!(
        grit_list.iter().filter(|e| e.is_current).count(),
        1,
        "exactly one current worktree when opened from {}",
        open_from.display()
    );
    let current = grit_list
        .iter()
        .find(|e| e.is_current)
        .expect("current row");
    assert_eq!(
        path_for_disk_compare(&current.path),
        path_for_disk_compare(expected_current_path),
        "is_current must mark the worktree root, not the cwd subdirectory"
    );
    assert_eq!(
        path_for_disk_compare(&current.admin_dir),
        path_for_disk_compare(&worktree_admin_dir(&repo)),
        "is_current admin dir must match opened repository"
    );
}

#[test]
fn worktree_list_reports_current_lock_and_prunable_like_git() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let main = tmp.path().join("main");
    std::fs::create_dir_all(&main).expect("mkdir");
    git(&main, &["init", "-q", "-b", "main"]);
    git(&main, &["config", "user.email", "t@example.com"]);
    git(&main, &["config", "user.name", "Test"]);
    std::fs::write(main.join("f"), b"x\n").unwrap();
    git(&main, &["add", "f"]);
    git(&main, &["commit", "-qm", "seed"]);

    let wt_locked_reason = tmp.path().join("wt-locked-reason");
    git(
        &main,
        &[
            "worktree",
            "add",
            wt_locked_reason.to_str().unwrap(),
            "-b",
            "locked-reason",
        ],
    );
    git(
        &main,
        &[
            "worktree",
            "lock",
            wt_locked_reason.to_str().unwrap(),
            "--reason",
            "hold",
        ],
    );

    let wt_locked_bare = tmp.path().join("wt-locked-bare");
    git(
        &main,
        &[
            "worktree",
            "add",
            wt_locked_bare.to_str().unwrap(),
            "-b",
            "locked-bare",
        ],
    );
    git(
        &main,
        &["worktree", "lock", wt_locked_bare.to_str().unwrap()],
    );

    let wt_gone = tmp.path().join("wt-gone");
    git(
        &main,
        &["worktree", "add", wt_gone.to_str().unwrap(), "-b", "gone"],
    );
    std::fs::remove_dir_all(&wt_gone).expect("remove linked tree");

    assert_list_matches_git(&main, &main, &main);

    let main_sub = main.join("nested");
    std::fs::create_dir_all(&main_sub).expect("main subdir");
    assert_list_matches_git(&main_sub, &main, &main);

    assert_list_matches_git(&wt_locked_reason, &main, &wt_locked_reason);

    let linked_sub = wt_locked_reason.join("nested");
    std::fs::create_dir_all(&linked_sub).expect("linked subdir");
    assert_list_matches_git(&linked_sub, &main, &wt_locked_reason);
}