git-bra 0.4.0

A Git worktree manager with project-aware configuration.
Documentation
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, anyhow, bail};

use crate::git;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProjectSelector {
    Alias(String),
    RepoPath(PathBuf),
    InferredFromCwd,
}

#[derive(Debug, Clone)]
pub struct ProjectContext {
    pub alias: String,
    pub repo_root: Option<PathBuf>,
}

pub fn selector_from_arg(project: Option<&str>) -> ProjectSelector {
    match project {
        Some(value) => {
            let path = Path::new(value);
            if path.exists() {
                ProjectSelector::RepoPath(path.to_path_buf())
            } else {
                ProjectSelector::Alias(value.to_owned())
            }
        }
        None => ProjectSelector::InferredFromCwd,
    }
}

pub fn resolve_project(project: Option<&str>, cwd: &Path) -> Result<ProjectContext> {
    match selector_from_arg(project) {
        ProjectSelector::RepoPath(path) => {
            let root = git::repo_root(&path)?;
            let origin = git::origin_url(&root).with_context(|| {
                format!("failed to resolve project alias from {}", root.display())
            })?;
            let alias = alias_from_origin_url(&origin)?;
            Ok(ProjectContext {
                alias,
                repo_root: Some(root),
            })
        }
        ProjectSelector::Alias(alias) => Ok(ProjectContext {
            alias,
            repo_root: None,
        }),
        ProjectSelector::InferredFromCwd => {
            let root = git::repo_root(cwd).map_err(|_| {
                anyhow!("not inside a git repository and --project was not provided")
            })?;
            let origin = git::origin_url(&root).with_context(|| {
                format!("failed to resolve project alias from {}", root.display())
            })?;
            let alias = alias_from_origin_url(&origin)?;
            Ok(ProjectContext {
                alias,
                repo_root: Some(root),
            })
        }
    }
}

pub fn require_repo_root(context: &ProjectContext) -> Result<&Path> {
    context.repo_root.as_deref().ok_or_else(|| {
        anyhow!(
            "this command needs a local git repository; pass --project with a repository path or run inside the repo"
        )
    })
}

pub fn alias_from_origin_url(origin: &str) -> Result<String> {
    let trimmed = origin.trim_end_matches('/');
    let last = trimmed
        .rsplit(['/', ':'])
        .next()
        .ok_or_else(|| anyhow!("failed to extract project alias from origin URL '{origin}'"))?;
    let alias = last.trim_end_matches(".git");
    if alias.is_empty() {
        bail!("failed to extract project alias from origin URL '{origin}'")
    }
    Ok(alias.to_owned())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn extracts_alias_from_ssh_url() {
        assert_eq!(
            alias_from_origin_url("git@github.com:some-org/my-project.git").unwrap(),
            "my-project"
        );
    }

    #[test]
    fn extracts_alias_from_https_url() {
        assert_eq!(
            alias_from_origin_url("https://github.com/some-org/my-project.git").unwrap(),
            "my-project"
        );
    }

    #[test]
    fn treats_missing_project_as_inferred() {
        assert_eq!(selector_from_arg(None), ProjectSelector::InferredFromCwd);
    }

    #[test]
    fn treats_nonexistent_argument_as_alias() {
        assert_eq!(
            selector_from_arg(Some("my-project")),
            ProjectSelector::Alias("my-project".to_owned())
        );
    }
}