diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-30 23:58:15 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-30 23:58:34 +0300 |
| commit | fc4e1bbf6b0fa2ec37445a5b72356514c7c6be6a (patch) | |
| tree | 93af04af44a4dcc062a7886c51d70641c1903144 /src/workspace/git/discovery.rs | |
| parent | d477341e6af816ff4396042b084a0378ef437b4a (diff) | |
perf: reduce sidebar git polling overhead
refs #353
Diffstat (limited to 'src/workspace/git/discovery.rs')
| -rw-r--r-- | src/workspace/git/discovery.rs | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/src/workspace/git/discovery.rs b/src/workspace/git/discovery.rs new file mode 100644 index 00000000..de4b090c --- /dev/null +++ b/src/workspace/git/discovery.rs @@ -0,0 +1,278 @@ +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GitSpaceMetadata { + pub key: String, + pub checkout_key: String, + pub label: String, + pub repo_root: PathBuf, + pub is_linked_worktree: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GitWorktreeInfo { + pub repo_root: PathBuf, + pub git_dir: PathBuf, + pub git_common_dir: PathBuf, + pub is_bare: bool, + pub is_linked_worktree: bool, +} + +pub fn derive_label_from_cwd(cwd: &Path) -> String { + if let Some(repo_root) = git_repo_root(cwd) { + if let Some(name) = repo_root.file_name().and_then(|n| n.to_str()) { + return name.to_string(); + } + } + + if let Ok(home) = std::env::var("HOME") { + let home = Path::new(&home); + if cwd == home { + return "~".to_string(); + } + } + + cwd.file_name() + .and_then(|n| n.to_str()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .unwrap_or_else(|| cwd.display().to_string()) +} + +pub fn git_worktree_info(cwd: &Path) -> Option<GitWorktreeInfo> { + let repo_root = git_repo_root(cwd)?; + let git_dir = canonicalize_best_effort_path(&git_dir_for_repo_root(&repo_root)?); + let git_common_dir = canonicalize_best_effort_path(&git_common_dir_for_git_dir(&git_dir)); + let is_linked_worktree = git_dir != git_common_dir; + + Some(GitWorktreeInfo { + repo_root, + git_dir, + git_common_dir, + is_bare: false, + is_linked_worktree, + }) +} + +pub fn git_space_metadata(cwd: &Path) -> Option<GitSpaceMetadata> { + git_repo_root(cwd)?; + + let info = git_worktree_info(cwd)?; + if info.is_bare { + return None; + } + let key = canonicalize_best_effort_path(&info.git_common_dir) + .display() + .to_string(); + let checkout_key = canonicalize_best_effort_path(&info.repo_root) + .display() + .to_string(); + let label_path = if info + .git_common_dir + .file_name() + .and_then(|name| name.to_str()) + == Some(".git") + { + info.git_common_dir.parent().unwrap_or(&info.repo_root) + } else { + &info.repo_root + }; + let label = label_path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("repo") + .to_string(); + Some(GitSpaceMetadata { + key, + checkout_key, + label, + repo_root: info.repo_root, + is_linked_worktree: info.is_linked_worktree, + }) +} + +pub(super) fn canonicalize_best_effort_path(path: &Path) -> PathBuf { + std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) +} + +fn git_common_dir_for_git_dir(git_dir: &Path) -> PathBuf { + let commondir = git_dir.join("commondir"); + let Ok(contents) = std::fs::read_to_string(commondir) else { + return git_dir.to_path_buf(); + }; + let path = Path::new(contents.trim()); + if path.is_absolute() { + path.to_path_buf() + } else { + git_dir.join(path) + } +} + +pub fn git_branch(cwd: &Path) -> Option<String> { + let repo_root = git_repo_root(cwd)?; + let git_dir = git_dir_for_repo_root(&repo_root)?; + let head = std::fs::read_to_string(git_dir.join("HEAD")).ok()?; + parse_git_head_branch(&head) +} + +pub(super) fn git_dir_for_repo_root(repo_root: &Path) -> Option<PathBuf> { + let git_path = repo_root.join(".git"); + if git_path.is_dir() { + return Some(git_path); + } + + let gitdir = std::fs::read_to_string(&git_path).ok()?; + let relative = gitdir.trim().strip_prefix("gitdir:")?.trim(); + let resolved = Path::new(relative); + Some(if resolved.is_absolute() { + resolved.to_path_buf() + } else { + repo_root.join(resolved) + }) +} + +fn parse_git_head_branch(head: &str) -> Option<String> { + let branch = head.trim().strip_prefix("ref: refs/heads/")?; + (!branch.is_empty()).then(|| branch.to_string()) +} + +pub(super) fn git_repo_root(start: &Path) -> Option<PathBuf> { + let mut current = if start.is_dir() { + start.to_path_buf() + } else { + start.parent()?.to_path_buf() + }; + + loop { + if git_dir_for_repo_root(¤t) + .map(|git_dir| git_dir.join("HEAD").is_file()) + .unwrap_or(false) + { + return Some(current); + } + if !current.pop() { + return None; + } + } +} + +pub(super) fn read_ref_oid(common_dir: &Path, full_ref: &str) -> Option<String> { + let loose_ref = common_dir.join(full_ref); + if let Ok(contents) = std::fs::read_to_string(loose_ref) { + let oid = contents.trim(); + if !oid.is_empty() { + return Some(oid.to_string()); + } + } + + let packed_refs = std::fs::read_to_string(common_dir.join("packed-refs")).ok()?; + for line in packed_refs.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with('^') { + continue; + } + let mut parts = line.split_whitespace(); + let oid = parts.next()?; + let name = parts.next()?; + if name == full_ref { + return Some(oid.to_string()); + } + } + None +} + +#[cfg(test)] +mod tests { + use std::path::{Path, PathBuf}; + use std::time::{SystemTime, UNIX_EPOCH}; + + use super::*; + + fn temp_test_dir(name: &str) -> PathBuf { + let unique = format!( + "herdr-workspace-tests-{}-{}-{}", + name, + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + let path = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&path).unwrap(); + path + } + + #[test] + fn git_branch_reads_head_from_standard_repo() { + let root = temp_test_dir("standard-repo"); + std::fs::create_dir_all(root.join(".git")).unwrap(); + std::fs::write(root.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap(); + + assert_eq!(git_branch(&root).as_deref(), Some("main")); + + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn git_branch_reads_head_from_worktree_gitdir_file() { + let root = temp_test_dir("worktree"); + let worktree_git_dir = root.join(".bare/worktrees/feature"); + std::fs::create_dir_all(&worktree_git_dir).unwrap(); + std::fs::write(root.join(".git"), "gitdir: .bare/worktrees/feature\n").unwrap(); + std::fs::write(worktree_git_dir.join("HEAD"), "ref: refs/heads/feature\n").unwrap(); + + assert_eq!(git_branch(&root).as_deref(), Some("feature")); + + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn git_branch_returns_none_for_detached_head() { + let root = temp_test_dir("detached-head"); + std::fs::create_dir_all(root.join(".git")).unwrap(); + std::fs::write(root.join(".git/HEAD"), "3e1b9a8d\n").unwrap(); + + assert_eq!(git_branch(&root), None); + + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn git_repo_root_ignores_invalid_git_marker() { + let base = temp_test_dir("invalid-git-root"); + let cwd = base.join("workspace"); + std::fs::create_dir_all(base.join(".git")).unwrap(); + std::fs::create_dir_all(&cwd).unwrap(); + + assert_eq!(git_repo_root(&cwd), None); + + std::fs::remove_dir_all(base).unwrap(); + } + + #[test] + fn derive_label_prefers_repo_root_name() { + let root = temp_test_dir("label-repo"); + let nested = root.join("nested"); + std::fs::create_dir_all(root.join(".git")).unwrap(); + std::fs::write(root.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap(); + std::fs::create_dir_all(&nested).unwrap(); + + assert_eq!( + derive_label_from_cwd(&nested), + root.file_name().and_then(|name| name.to_str()).unwrap() + ); + + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn derive_label_uses_path_name_outside_git() { + let root = temp_test_dir("label-plain"); + let label = root.file_name().and_then(|name| name.to_str()).unwrap(); + + assert_eq!(derive_label_from_cwd(Path::new(&root)), label); + + std::fs::remove_dir_all(root).unwrap(); + } +} |
