use std::path::PathBuf;
use crate::parse::{base_command, tokenize, Operator, ParsedPipeline};
pub fn resolve_path(target: &str, base: &str) -> String {
if target.starts_with('/') {
target.to_string()
} else if target == "~" || target.starts_with("~/") {
if let Some(home) = std::env::var_os("HOME") {
let rest = target.strip_prefix("~/").unwrap_or("");
if rest.is_empty() {
home.to_string_lossy().to_string()
} else {
PathBuf::from(home).join(rest).to_string_lossy().to_string()
}
} else {
target.to_string()
}
} else {
PathBuf::from(base)
.join(target)
.to_string_lossy()
.to_string()
}
}
pub fn extract_cd_target(words: &[String]) -> Option<&str> {
words
.iter()
.find(|w| !w.starts_with('-') && *w != "cd")
.map(String::as_str)
}
pub fn extract_git_c_path(words: &[String]) -> Option<String> {
let git_idx = words.iter().position(|w| w == "git")?;
let mut i = git_idx + 1;
while i < words.len() {
if words[i] == "-C" {
return words.get(i + 1).cloned();
}
i += 1;
}
None
}
pub fn effective_cwd(pipeline: &ParsedPipeline, session_cwd: &str) -> Vec<String> {
let mut cwd = session_cwd.to_string();
let mut git_cwds: Vec<String> = Vec::new();
for (i, seg) in pipeline.segments.iter().enumerate() {
let words = tokenize(&seg.command);
if words.is_empty() {
if i < pipeline.operators.len() {
match pipeline.operators[i] {
Operator::And | Operator::Semi => {}
_ => cwd = session_cwd.to_string(),
}
}
continue;
}
let base = base_command(&seg.command);
if base == "cd" {
if let Some(target) = extract_cd_target(&words) {
cwd = resolve_path(target, &cwd);
}
}
if base == "git" {
let git_cwd = extract_git_c_path(&words);
let resolved = if let Some(path) = git_cwd {
if path.starts_with('/') {
path
} else {
PathBuf::from(&cwd)
.join(&path)
.to_string_lossy()
.to_string()
}
} else {
cwd.clone()
};
git_cwds.push(resolved);
}
if i < pipeline.operators.len() {
match pipeline.operators[i] {
Operator::And | Operator::Semi => {}
_ => cwd = session_cwd.to_string(),
}
}
}
if git_cwds.is_empty() {
vec![cwd]
} else {
git_cwds.dedup();
git_cwds
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_absolute() {
assert_eq!(resolve_path("/abs/path", "/base"), "/abs/path");
}
#[test]
fn resolve_relative() {
assert_eq!(resolve_path("subdir", "/base"), "/base/subdir");
}
#[test]
fn resolve_tilde_alone() {
let home = std::env::var("HOME").unwrap_or_default();
assert_eq!(resolve_path("~", "/base"), home);
}
#[test]
fn resolve_tilde_subdir() {
let home = std::env::var("HOME").unwrap_or_default();
let result = resolve_path("~/docs", "/base");
assert_eq!(result, format!("{home}/docs"));
}
#[test]
fn cd_target_normal() {
let words: Vec<String> = ["cd", "/foo"].iter().map(|s| s.to_string()).collect();
assert_eq!(extract_cd_target(&words), Some("/foo"));
}
#[test]
fn cd_target_with_flags() {
let words: Vec<String> = ["cd", "-L", "/foo"].iter().map(|s| s.to_string()).collect();
assert_eq!(extract_cd_target(&words), Some("/foo"));
}
#[test]
fn cd_target_no_args() {
let words: Vec<String> = ["cd"].iter().map(|s| s.to_string()).collect();
assert_eq!(extract_cd_target(&words), None);
}
#[test]
fn git_c_path_present() {
let words: Vec<String> = ["git", "-C", "/repo", "status"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(extract_git_c_path(&words), Some("/repo".to_string()));
}
#[test]
fn git_c_path_absent() {
let words: Vec<String> = ["git", "status"].iter().map(|s| s.to_string()).collect();
assert_eq!(extract_git_c_path(&words), None);
}
#[test]
fn git_c_path_multiple_flags() {
let words: Vec<String> = ["git", "--no-pager", "-C", "/repo", "log"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(extract_git_c_path(&words), Some("/repo".to_string()));
}
}