Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions codex-rs/core/src/git_info_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ async fn get_git_repo_root_with_fs_starts_at_parent_for_file() {
let proj = tmp.path().join("proj");
let nested = proj.join("nested");
std::fs::create_dir_all(proj.join(".git")).unwrap();
std::fs::write(proj.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::create_dir_all(&nested).unwrap();
let file = nested.join("file.txt");
std::fs::write(&file, "contents").unwrap();
Expand All @@ -637,6 +638,7 @@ async fn get_git_repo_root_with_fs_ignores_metadata_errors() {
let proj = tmp.path().join("proj");
let nested = proj.join("nested");
std::fs::create_dir_all(proj.join(".git")).unwrap();
std::fs::write(proj.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::create_dir_all(&nested).unwrap();
let fs = FailingMetadataFileSystem {
path: PathUri::from_abs_path(&nested.join(".git").abs()),
Expand All @@ -654,6 +656,7 @@ async fn get_git_repo_root_with_fs_supports_windows_namespace_paths() {
let tmp = TempDir::new().expect("tempdir");
let repo = tmp.path().join("repo");
std::fs::create_dir_all(repo.join(".git")).unwrap();
std::fs::write(repo.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::create_dir_all(repo.join("nested")).unwrap();

let namespace_repo = PathBuf::from(format!(r"\\?\{}", repo.display()));
Expand Down Expand Up @@ -855,3 +858,37 @@ fn test_git_info_serialization_with_nones() {
assert!(!parsed.as_object().unwrap().contains_key("branch"));
assert!(!parsed.as_object().unwrap().contains_key("repository_url"));
}

#[tokio::test]
async fn get_git_repo_root_with_fs_ignores_empty_git_dir() {
skip_if_sandbox!();
let temp_root = TempDir::new().unwrap();

// Create an empty .git directory (e.g. from a bubblewrap mount)
let dot_git = temp_root.path().join(".git");
fs::create_dir(&dot_git).unwrap();

let nested = temp_root.path().join("nested");
fs::create_dir(&nested).unwrap();

assert_eq!(
get_git_repo_root_with_fs(LOCAL_FS.as_ref(), &nested.abs()).await,
None
);
}

#[tokio::test]
async fn get_git_repo_root_with_fs_skips_empty_git_dir_for_valid_parent() {
skip_if_sandbox!();
let temp_root = TempDir::new().unwrap();
let repo = temp_root.path().join("repo");
let nested = repo.join("nested");
fs::create_dir_all(repo.join(".git")).unwrap();
fs::write(repo.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
fs::create_dir_all(nested.join(".git")).unwrap();

assert_eq!(
get_git_repo_root_with_fs(LOCAL_FS.as_ref(), &nested.abs()).await,
Some(repo.abs())
);
}
72 changes: 61 additions & 11 deletions codex-rs/git-utils/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,38 @@ pub async fn get_git_repo_root_with_fs(
cwd: &AbsolutePathBuf,
) -> Option<AbsolutePathBuf> {
let cwd_uri = PathUri::from_abs_path(cwd);
let base = match fs.get_metadata(&cwd_uri, /*sandbox*/ None).await {
let mut search_base = match fs.get_metadata(&cwd_uri, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_directory => cwd.clone(),
_ => cwd.parent()?,
};
find_nearest_native_ancestor_with_markers(
fs,
&base,
vec![".git".to_string()],
FindUpErrorPolicy::Ignore,
/*sandbox*/ None,
)
.await
.ok()?

loop {
let repo_root = find_nearest_native_ancestor_with_markers(
fs,
&search_base,
vec![".git".to_string()],
FindUpErrorPolicy::Ignore,
/*sandbox*/ None,
)
.await
.ok()??;
let dot_git = repo_root.join(".git");
let dot_git_uri = PathUri::from_abs_path(&dot_git);
let head_uri = PathUri::from_abs_path(&dot_git.join("HEAD"));
let objects_uri = PathUri::from_abs_path(&dot_git.join("objects"));
let (dot_git_metadata, head_metadata, objects_metadata) = tokio::join!(
fs.get_metadata(&dot_git_uri, /*sandbox*/ None),
fs.get_metadata(&head_uri, /*sandbox*/ None),
fs.get_metadata(&objects_uri, /*sandbox*/ None),
);
match dot_git_metadata {
Ok(metadata) if !metadata.is_directory => return Some(repo_root),
Ok(_) if head_metadata.is_ok() || objects_metadata.is_ok() => return Some(repo_root),
_ => {}
}

search_base = repo_root.parent()?;
}
}

/// Timeout for git commands to prevent freezing on large repositories
Expand Down Expand Up @@ -848,7 +867,13 @@ fn find_ancestor_git_entry(base_dir: &Path) -> Option<(PathBuf, PathBuf)> {
loop {
let dot_git = dir.join(".git");
if dot_git.exists() {
return Some((dir, dot_git));
if dot_git.is_dir() {
if dot_git.join("HEAD").exists() || dot_git.join("objects").exists() {
return Some((dir, dot_git));
}
} else {
return Some((dir, dot_git));
}
}

// Pop one component (go up one directory). `pop` returns false when
Expand Down Expand Up @@ -948,6 +973,31 @@ mod tests {
}
}

#[test]
fn get_git_repo_root_ignores_empty_git_directory() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
std::fs::create_dir(temp_dir.path().join(".git")).expect("create empty .git");
let nested = temp_dir.path().join("nested");
std::fs::create_dir(&nested).expect("create nested directory");

assert_eq!(get_git_repo_root(&nested), None);
}

#[test]
fn get_git_repo_root_accepts_git_directory_with_head() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
let dot_git = temp_dir.path().join(".git");
std::fs::create_dir(&dot_git).expect("create .git");
std::fs::write(dot_git.join("HEAD"), "ref: refs/heads/main\n").expect("write HEAD");
let nested = temp_dir.path().join("nested");
std::fs::create_dir(&nested).expect("create nested directory");

assert_eq!(
get_git_repo_root(&nested),
Some(temp_dir.path().to_path_buf())
);
}

#[tokio::test]
async fn local_git_branches_excludes_detached_head_entry() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/chatwidget/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,7 @@ pub(super) fn strip_osc8_for_snapshot(text: &str) -> String {

pub(super) fn plugins_test_absolute_path(path: &str) -> AbsolutePathBuf {
std::env::temp_dir()
.join("codex_mock_cwd")
.join("codex-plugin-menu-tests")
.join(path)
.abs()
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/chatwidget/tests/review_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,7 @@ async fn review_branch_picker_escape_navigates_back_then_dismisses() {
chat.open_review_popup();

// Open the branch picker submenu (child view). Using a temp cwd with no git repo is fine.
let cwd = std::env::temp_dir();
let cwd = std::env::temp_dir().join("codex_mock_cwd");
chat.show_review_branch_picker(&cwd).await;

// Verify child view header.
Expand Down
8 changes: 4 additions & 4 deletions codex-rs/tui/src/history_cell/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use rmcp::model::Content;

const SMALL_PNG_BASE64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
async fn test_config() -> Config {
let codex_home = std::env::temp_dir();
let codex_home = std::env::temp_dir().join("codex_mock_cwd");
ConfigBuilder::default()
.codex_home(codex_home.clone())
.build()
Expand All @@ -44,7 +44,7 @@ async fn test_config() -> Config {
fn test_cwd() -> PathBuf {
// These tests only need a stable absolute cwd; using temp_dir() avoids baking Unix- or
// Windows-specific root semantics into the fixtures.
std::env::temp_dir()
std::env::temp_dir().join("codex_mock_cwd")
}

#[test]
Expand Down Expand Up @@ -1534,7 +1534,7 @@ fn session_header_includes_reasoning_level_when_present() {
"gpt-4o".to_string(),
Some(ReasoningEffortConfig::High),
/*show_fast_status*/ true,
std::env::temp_dir(),
std::env::temp_dir().join("codex_mock_cwd"),
"test",
);

Expand All @@ -1554,7 +1554,7 @@ fn session_header_hides_fast_status_when_disabled() {
"gpt-4o".to_string(),
Some(ReasoningEffortConfig::High),
/*show_fast_status*/ false,
std::env::temp_dir(),
std::env::temp_dir().join("codex_mock_cwd"),
"test",
);

Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/markdown_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ impl MarkdownStreamCollector {
fn test_cwd() -> PathBuf {
// These tests only need a stable absolute cwd; using temp_dir() avoids baking Unix- or
// Windows-specific root semantics into the fixtures.
std::env::temp_dir()
std::env::temp_dir().join("codex_mock_cwd")
}

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/streaming/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ mod tests {
fn test_cwd() -> PathBuf {
// These tests only need a stable absolute cwd; using temp_dir() avoids baking Unix- or
// Windows-specific root semantics into the fixtures.
std::env::temp_dir()
std::env::temp_dir().join("codex_mock_cwd")
}

fn stream_controller(width: Option<usize>) -> StreamController {
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/streaming/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ mod tests {
fn test_cwd() -> PathBuf {
// These tests only need a stable absolute cwd; using temp_dir() avoids baking Unix- or
// Windows-specific root semantics into the fixtures.
std::env::temp_dir()
std::env::temp_dir().join("codex_mock_cwd")
}

#[test]
Expand Down
9 changes: 6 additions & 3 deletions codex-rs/tui/src/update_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,12 @@ mod tests {

#[test]
fn maps_install_context_to_update_action() {
let native_release_dir =
AbsolutePathBuf::from_absolute_path(std::env::temp_dir().join("native-release"))
.expect("temp dir path should be absolute");
let native_release_dir = AbsolutePathBuf::from_absolute_path(
std::env::temp_dir()
.join("codex_mock_cwd")
.join("native-release"),
)
.expect("temp dir path should be absolute");

assert_eq!(
UpdateAction::from_install_context(&InstallContext {
Expand Down
Loading