jj-navi 0.2.0

Workspace navigation and management for Jujutsu, built for parallel human and AI agent workflows.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use crate::error::{Error, Result};

pub(crate) fn find_workspace_root(path: &Path) -> Result<PathBuf> {
    path.ancestors()
        .find(|ancestor| ancestor.join(".jj").is_dir())
        .map(Path::to_path_buf)
        .ok_or(Error::NotInWorkspace)
}

pub(crate) fn resolve_repo_storage_path(workspace_root: &Path) -> Result<PathBuf> {
    let repo_path = workspace_root.join(".jj").join("repo");

    if repo_path.is_dir() {
        return Ok(repo_path);
    }

    let pointer = fs::read_to_string(&repo_path)?;
    let pointer = pointer.trim();
    if pointer.is_empty() {
        return Err(Error::InvalidRepoPointer(repo_path));
    }

    let pointer_path = PathBuf::from(pointer);
    let resolved = if pointer_path.is_absolute() {
        pointer_path
    } else {
        workspace_root.join(".jj").join(pointer_path)
    };

    let resolved = fs::canonicalize(&resolved).map_err(|error| Error::RepoPointerResolution {
        path: repo_path.clone(),
        message: error.to_string(),
    })?;
    if !resolved.is_dir() {
        return Err(Error::InvalidRepoPointer(repo_path));
    }

    Ok(resolved)
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use crate::error::Error;

    use super::resolve_repo_storage_path;

    #[test]
    fn resolves_relative_repo_pointer() {
        let temp = TempDir::new().expect("temp dir");
        let workspace_root = temp.path().join("workspace");
        let shared = temp.path().join("shared").join("repo");

        fs::create_dir_all(workspace_root.join(".jj")).expect("workspace .jj");
        fs::create_dir_all(&shared).expect("shared repo");
        fs::write(
            workspace_root.join(".jj").join("repo"),
            "../../shared/repo\n",
        )
        .expect("write repo pointer");

        let resolved = resolve_repo_storage_path(&workspace_root).expect("resolve repo pointer");

        assert_eq!(
            resolved,
            fs::canonicalize(shared).expect("canonical shared repo")
        );
    }

    #[test]
    fn rejects_missing_repo_pointer_target() {
        let temp = TempDir::new().expect("temp dir");
        let workspace_root = temp.path().join("workspace");
        let repo_pointer = workspace_root.join(".jj").join("repo");

        fs::create_dir_all(workspace_root.join(".jj")).expect("workspace .jj");
        fs::write(&repo_pointer, "../../shared/repo\n").expect("write repo pointer");

        let error = resolve_repo_storage_path(&workspace_root).expect_err("missing repo target");

        assert!(matches!(
            error,
            Error::RepoPointerResolution { path, .. } if path == repo_pointer
        ));
    }

    #[test]
    fn rejects_repo_pointer_to_non_directory() {
        let temp = TempDir::new().expect("temp dir");
        let workspace_root = temp.path().join("workspace");
        let shared_file = temp.path().join("shared").join("repo");
        let repo_pointer = workspace_root.join(".jj").join("repo");

        fs::create_dir_all(workspace_root.join(".jj")).expect("workspace .jj");
        fs::create_dir_all(shared_file.parent().expect("shared file parent"))
            .expect("shared file parent dir");
        fs::write(&shared_file, "not a dir").expect("write shared file");
        fs::write(&repo_pointer, "../../shared/repo\n").expect("write repo pointer");

        let error =
            resolve_repo_storage_path(&workspace_root).expect_err("repo pointer to file target");

        assert!(matches!(error, Error::InvalidRepoPointer(path) if path == repo_pointer));
    }
}