use std::path::{Path, PathBuf};
pub fn sanitize_branch_name(branch: &str) -> String {
let mut result = branch.replace('/', "-");
result = result.trim_start_matches('.').to_string();
while result.contains("--") {
result = result.replace("--", "-");
}
result = result.trim_matches('-').to_string();
if result.is_empty() {
result = "branch".to_string();
}
result
}
pub fn worktree_dir(bare_repo: &Path, branch: &str, prefix: &str) -> PathBuf {
let parent = bare_repo
.parent()
.expect("bare repo must have a parent dir");
let sanitized = sanitize_branch_name(branch);
let dir_name = if prefix.is_empty() {
sanitized
} else {
format!("{prefix}_{sanitized}")
};
parent.join(dir_name)
}
pub fn repo_name_from_url(url: &str) -> String {
let name = url
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or("repo");
name.strip_suffix(".git").unwrap_or(name).to_string()
}
pub fn relative_from(target: &Path, base: &Path) -> PathBuf {
let mut base_iter = base.components();
let mut target_iter = target.components();
let mut common = 0;
loop {
match (base_iter.clone().next(), target_iter.clone().next()) {
(Some(b), Some(t)) if b == t => {
base_iter.next();
target_iter.next();
common += 1;
}
_ => break,
}
}
if common == 0 {
return target.to_path_buf();
}
let ups = base_iter.count();
let mut result = PathBuf::new();
for _ in 0..ups {
result.push("..");
}
for component in target_iter {
result.push(component);
}
if result.as_os_str().is_empty() {
PathBuf::from(".")
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_simple_branch() {
assert_eq!(sanitize_branch_name("main"), "main");
}
#[test]
fn sanitize_slashes() {
assert_eq!(sanitize_branch_name("feature/my-thing"), "feature-my-thing");
}
#[test]
fn sanitize_leading_dot() {
assert_eq!(sanitize_branch_name(".hidden"), "hidden");
}
#[test]
fn sanitize_consecutive_dashes() {
assert_eq!(sanitize_branch_name("a//b"), "a-b");
}
#[test]
fn worktree_dir_with_prefix() {
let bare = Path::new("/repos/myproject/repo.git");
assert_eq!(
worktree_dir(bare, "feature/login", "mp"),
PathBuf::from("/repos/myproject/mp_feature-login")
);
}
#[test]
fn worktree_dir_without_prefix() {
let bare = Path::new("/repos/myproject/repo.git");
assert_eq!(
worktree_dir(bare, "feature/login", ""),
PathBuf::from("/repos/myproject/feature-login")
);
}
#[test]
fn repo_name_from_https_url() {
assert_eq!(
repo_name_from_url("https://github.com/user/repo.git"),
"repo"
);
}
#[test]
fn repo_name_from_ssh_url() {
assert_eq!(
repo_name_from_url("git@github.com:user/my-project.git"),
"my-project"
);
}
#[test]
fn repo_name_without_git_suffix() {
assert_eq!(repo_name_from_url("https://github.com/user/repo"), "repo");
}
#[test]
fn repo_name_trailing_slash() {
assert_eq!(
repo_name_from_url("https://github.com/user/repo.git/"),
"repo"
);
}
#[test]
fn relative_from_same_dir() {
assert_eq!(
relative_from(Path::new("/a/b"), Path::new("/a/b")),
PathBuf::from(".")
);
}
#[test]
fn relative_from_child() {
assert_eq!(
relative_from(Path::new("/a/b"), Path::new("/a")),
PathBuf::from("b")
);
}
#[test]
fn relative_from_sibling() {
assert_eq!(
relative_from(Path::new("/a/c"), Path::new("/a/b")),
PathBuf::from("../c")
);
}
#[test]
fn relative_from_deeply_nested() {
assert_eq!(
relative_from(Path::new("/a/b/c/d"), Path::new("/a/x/y")),
PathBuf::from("../../b/c/d")
);
}
#[test]
fn relative_from_parent() {
assert_eq!(
relative_from(Path::new("/a"), Path::new("/a/b/c")),
PathBuf::from("../..")
);
}
#[test]
fn relative_from_path_with_spaces() {
assert_eq!(
relative_from(Path::new("/a/my dir/c"), Path::new("/a/my dir")),
PathBuf::from("c")
);
}
#[test]
fn relative_from_no_common_prefix_returns_target() {
assert_eq!(
relative_from(Path::new("d:/foo"), Path::new("c:/bar")),
PathBuf::from("d:/foo")
);
}
}