Skip to content
Closed
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
67 changes: 66 additions & 1 deletion app/src/code/file_tree/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ use repo_metadata::local_model::IndexedRepoState;
use repo_metadata::FileTreeEntry;
use repo_metadata::RepoMetadataModel;
use std::collections::{HashMap, HashSet};
use std::io::Read;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use warp_util::path::LineAndColumnArg;
use warp_util::standardized_path::StandardizedPath;

use repo_metadata::repositories::DetectedRepositories;
use repo_metadata::repositories::{DetectedRepositories, RepoDetectionSource};
use warp_core::send_telemetry_from_ctx;
use warpui::elements::{
AcceptedByDropTarget, Align, Clipped, ConstrainedBox, Container, Dismiss, Draggable,
Expand Down Expand Up @@ -1533,6 +1534,58 @@ impl FileTreeView {
// When the file tree is active, index the lazy-loaded path through the
// model so that a file watcher is started.
if self.is_active && !self.registered_lazy_loaded_paths.contains(path) {
// Check if this path appears to be a valid git repository by looking for .git directory
// or .git file (used by worktrees/submodules). For .git files, we need to parse the
// gitdir path and verify the actual git directory has a valid HEAD.
// Cap .git file read to 1KB to prevent unbounded reads from malformed local folders.
let has_git_entry = path
.to_local_path()
.map(|p| {
let git_entry = p.join(".git");
if git_entry.is_dir() {
// Standard .git directory - verify HEAD exists
git_entry.join("HEAD").is_file()
} else if git_entry.is_file() {
// .git file (worktree/submodule) - read gitdir path and verify HEAD there
// Cap read to 1KB to prevent malicious/malformed folders from stalling UI.
std::fs::File::open(&git_entry)
.ok()
.and_then(|mut file| {
let mut contents = vec![0u8; 1024];
let n = file.read(&mut contents).ok()?;
contents.truncate(n);
String::from_utf8(contents).ok()
})
.and_then(|contents| {
contents
.trim()
.strip_prefix("gitdir:")
.map(|gitdir| PathBuf::from(gitdir.trim()))
})
.map(|gitdir| {
let gitdir = if gitdir.is_absolute() {
gitdir
} else {
p.join(gitdir)
};
gitdir.join("HEAD").is_file()
})
.unwrap_or(false)
} else {
false
}
})
.unwrap_or(false);

// Check if this repo has already been attempted and failed. If so, fallback
// to lazy-loading to avoid repeatedly retrying failed detection.
let repo_id = repo_metadata::RepositoryIdentifier::local(path.clone());
let repo_state = RepoMetadataModel::as_ref(ctx).repository_state(&repo_id, ctx);
let previously_failed = matches!(repo_state, Some(IndexedRepoState::Failed(_)));

// Always use lazy-loading as a fallback in case detection fails or returns None.
// This ensures the tree is never empty - detection will update it with full content
// if successful, otherwise lazy-loaded content remains visible.
let index_result = self
.repository_metadata_model
.update(ctx, |model: &mut RepoMetadataModel, ctx| {
Expand All @@ -1552,6 +1605,18 @@ impl FileTreeView {
if RepoMetadataModel::as_ref(ctx).is_lazy_loaded_path(path, ctx) {
self.registered_lazy_loaded_paths.insert(path.clone());
}

// Additionally, if this appears to be a git repo and hasn't failed before,
// trigger proper detection to get full repo indexing instead of shallow tree.
if has_git_entry && !previously_failed {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] detect_possible_git_repo returns None without updating RepoMetadataModel when .git exists but is invalid/unreadable, so this branch skips lazy-loading and leaves the root empty indefinitely. Add a fallback when detection completes with no repo, or validate enough synchronously before bypassing index_lazy_loaded_path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] detect_possible_git_repo returns None without updating RepoMetadataModel when .git exists but is invalid/unreadable, so this branch skips lazy-loading and leaves the root empty indefinitely. Add a fallback when detection completes with no repo, or validate enough synchronously before bypassing index_lazy_loaded_path.

Copy link
Copy Markdown
Author

@Chukwuebuka-2003 Chukwuebuka-2003 May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/oz-review The code at 8c6b8b6:

  • Uses .exists() which handles both .git directories and .git files (worktrees/submodules)
  • Falls back to lazy-loading if repo previously failed indexing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] When detect_possible_git_repo returns None for an invalid .git directory/file/symlink, repo_state stays None, so this branch is retried forever and index_lazy_loaded_path never runs; await/track detection failure or explicitly fall back to lazy loading.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] If .git exists but is malformed, stale, or an unsupported symlink, detect_possible_git_repo resolves to None without setting RepoMetadataModel to Failed, so this branch never falls back to index_lazy_loaded_path and the directory can remain empty/retry forever.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 [SUGGESTION] This new detection path is not covered by the existing file tree tests; add a regression case where an active tree opens a repo root with .git before DetectedRepositories has cached the root.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] If full repo indexing fails after this detection starts, index_directory removes the model-side lazy-loaded entry and marks the repo Failed while registered_lazy_loaded_paths still contains path; the next refresh skips index_lazy_loaded_path and replaces the visible fallback with an empty root. Preserve the lazy entry until full indexing succeeds, or clear/retry the lazy registration on UpdatingRepositoryFailed.

DetectedRepositories::handle(ctx).update(ctx, |repos, ctx| {
std::mem::drop(repos.detect_possible_git_repo(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] If detect_possible_git_repo returns None without emitting DetectedGitRepo (for example find_git_repo stops at $HOME before checking ~/.git, or watcher registration fails), this branch has already skipped index_lazy_loaded_path, so the tree remains empty and retries on every refresh; await the returned future or trigger lazy-loading on None.

&path.to_string(),
RepoDetectionSource::TerminalNavigation,
ctx,
));
});
Comment thread
Chukwuebuka-2003 marked this conversation as resolved.
Comment on lines +1612 to 1618
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [CRITICAL] This drops the Future returned by detect_possible_git_repo; futures are #[must_use], so cargo clippy -- -D warnings will reject this hunk.

Suggested change
DetectedRepositories::handle(ctx).update(ctx, |repos, ctx| {
std::mem::drop(repos.detect_possible_git_repo(
&path.to_string(),
RepoDetectionSource::TerminalNavigation,
ctx,
));
});
DetectedRepositories::handle(ctx).update(ctx, |repos, ctx| {
std::mem::drop(repos.detect_possible_git_repo(
&path.to_string(),
RepoDetectionSource::TerminalNavigation,
ctx,
));
});

}
}

let id = repo_metadata::RepositoryIdentifier::local(path.clone());
Expand Down
1 change: 1 addition & 0 deletions crates/integration/src/bin/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ fn register_tests() -> HashMap<&'static str, BoxedBuilderFn> {
register_test!(test_file_tree_keyboard_navigation);
register_test!(test_file_tree_non_openable_files);
register_test!(test_file_tree_nested_file_opening);
register_test!(test_file_tree_loads_git_repo_on_first_open);

// Go to Line tests
register_test!(test_goto_line_dialog_open_close);
Expand Down
49 changes: 49 additions & 0 deletions crates/integration/src/test/file_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,52 @@ pub fn test_file_tree_nested_file_opening() -> Builder {
.add_assertion(assert_pane_title(0, 1, Regex::new(r"helper\.js$").unwrap())),
)
}

/// Regression test for issue #9846: File tree sidebar does not load when opened
/// immediately after cloning a repository.
///
/// This test verifies that when the file tree is opened on a directory that
/// contains a valid .git entry, the proper git detection is triggered instead
/// of falling back to shallow lazy-loading.
pub fn test_file_tree_loads_git_repo_on_first_open() -> Builder {
new_builder()
.with_setup(|utils| {
let test_dir = utils.test_dir();

// Create a valid git repository structure (simulating a freshly cloned repo)
let git_dir = test_dir.join(".git");
std::fs::create_dir_all(&git_dir).expect("Failed to create .git directory");

// Create a valid HEAD file (required for valid git repo detection)
std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n")
.expect("Failed to create HEAD file");

// Create some files in the repo to verify the full tree loads
std::fs::write(test_dir.join("README.md"), "# Test Repo").expect("Failed to create README");
std::fs::create_dir_all(test_dir.join("src")).expect("Failed to create src dir");
std::fs::write(test_dir.join("src/main.rs"), "fn main() {}")
.expect("Failed to create main.rs");

let dir_string = test_dir
.to_str()
.expect("Should be able to convert test dir to str");
write_all_rc_files_for_test(&test_dir, format!("cd {dir_string}"));
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open file tree panel")
.with_action(|app, _, _| open_file_tree_panel(app)),
)
// With the fix, detection triggers immediately on .git dirs, so the repo
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 [SUGGESTION] This test waits for terminal bootstrap and then expands src, so the old lazy-loading path can still reveal main.rs; assert the repo is not lazy-loaded or use a lower-level test that opens the file tree before terminal git detection runs.

// should NOT be in lazy-loaded state. We verify by clicking on main.rs
// which only works if full indexing happened (lazy-loading loads shallow tree).
// Note: This tests the detection path; old lazy-loading would need expand.
.with_step(
new_step_with_default_assertions("Expand src to verify nested content loads")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 [SUGGESTION] This still passes with the old lazy-load behavior because expanding src calls load_directory; assert the model is no longer lazy-loaded or that src/main.rs is already present before expansion to make this a true regression test.

.with_click_on_saved_position("file_tree_item:src"),
)
.with_step(
new_step_with_default_assertions("Verify main.rs visible (proves full tree loaded)")
.with_click_on_saved_position("file_tree_item:main.rs"),
)
}
1 change: 1 addition & 0 deletions crates/integration/tests/integration/ui_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ integration_tests! {
test_file_tree_keyboard_navigation,
test_file_tree_non_openable_files,
test_file_tree_nested_file_opening,
test_file_tree_loads_git_repo_on_first_open,

// Go to Line tests
test_goto_line_dialog_open_close,
Expand Down