mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
fix(welcome): show the live git branch on recent projects (#2851)
The welcome screen showed the branch stored when a project was last opened, so a checkout made later (in or outside the IDE) stayed stale. recent_projects now reads each project's HEAD when the list is built (linked worktrees via the .git file's gitdir; a detached HEAD shows the short commit), off the async runtime, and keeps the stored value when HEAD can't be read. Co-authored-by: GGVidal <52431033+GGVidal@users.noreply.github.com>
This commit is contained in:
@@ -10,7 +10,8 @@ pub struct RecentProject {
|
||||
pub path: ide_core::IpcPath,
|
||||
pub name: String,
|
||||
pub opened_at_ms: f64,
|
||||
/// Current git branch when last opened, shown on the welcome screen.
|
||||
/// Git branch shown on the welcome screen. Persisted as of last open; [`RecentProjects::list_live`]
|
||||
/// refreshes it from `.git/HEAD` when the list is shown.
|
||||
pub branch: Option<String>,
|
||||
}
|
||||
|
||||
@@ -94,6 +95,55 @@ fn now_ms() -> f64 {
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// The git directory for `project`: `.git/` or the path in a worktree's `.git` file (`gitdir: …`).
|
||||
fn git_dir(project: &Path) -> Option<PathBuf> {
|
||||
let dot = project.join(".git");
|
||||
let meta = std::fs::symlink_metadata(&dot).ok()?;
|
||||
if meta.file_type().is_dir() {
|
||||
return Some(dot);
|
||||
}
|
||||
if !meta.file_type().is_file() {
|
||||
return None;
|
||||
}
|
||||
let text = std::fs::read_to_string(&dot).ok()?;
|
||||
let raw = text.lines().find_map(|line| line.strip_prefix("gitdir:"))?.trim();
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let path = PathBuf::from(raw);
|
||||
Some(if path.is_absolute() { path } else { project.join(path) })
|
||||
}
|
||||
|
||||
/// Branch name, or a short commit when detached. `Err` when HEAD cannot be read (keep the stored value).
|
||||
fn read_head_branch(project: &Path) -> Result<Option<String>, ()> {
|
||||
let head = std::fs::read_to_string(git_dir(project).ok_or(())?.join("HEAD")).map_err(|_| ())?;
|
||||
let head = head.trim();
|
||||
if head.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if let Some(reference) = head.strip_prefix("ref:") {
|
||||
let reference = reference.trim();
|
||||
if let Some(branch) = reference.strip_prefix("refs/heads/") {
|
||||
return Ok(if branch.is_empty() { None } else { Some(branch.to_owned()) });
|
||||
}
|
||||
// Other symbolic refs (rare for HEAD) are not a branch name.
|
||||
return Ok(None);
|
||||
}
|
||||
// Detached HEAD: object id — show a short commit, never the previous branch.
|
||||
let short = head.get(..7).unwrap_or(head);
|
||||
Ok(if short.is_empty() { None } else { Some(short.to_owned()) })
|
||||
}
|
||||
|
||||
/// Overlay each entry's `branch` from `.git/HEAD` when readable; otherwise keep the stored value.
|
||||
pub fn with_live_branches(mut items: Vec<RecentProject>) -> Vec<RecentProject> {
|
||||
for item in &mut items {
|
||||
if let Ok(branch) = read_head_branch(item.path.as_ref()) {
|
||||
item.branch = branch;
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
impl RecentProjects {
|
||||
pub fn load(config_dir: &Path) -> Self {
|
||||
let file = config_dir.join("recent.json");
|
||||
@@ -115,6 +165,11 @@ impl RecentProjects {
|
||||
self.items.lock().clone()
|
||||
}
|
||||
|
||||
/// Like [`list`], but each `branch` is refreshed from that project's `.git/HEAD` when readable.
|
||||
pub fn list_live(&self) -> Vec<RecentProject> {
|
||||
with_live_branches(self.list())
|
||||
}
|
||||
|
||||
pub fn groups(&self) -> Vec<RecentProjectGroup> {
|
||||
self.groups.lock().clone()
|
||||
}
|
||||
@@ -261,4 +316,45 @@ mod tests {
|
||||
assert_eq!(recent.list()[0].name, "app");
|
||||
assert_eq!(recent.list().len(), 1);
|
||||
}
|
||||
|
||||
fn write_head(git_dir: &Path, contents: &str) {
|
||||
std::fs::create_dir_all(git_dir).unwrap();
|
||||
std::fs::write(git_dir.join("HEAD"), contents).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_live_reads_branch_from_head_and_keeps_stored_when_unreadable() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let repo = tmp.path().join("repo");
|
||||
write_head(&repo.join(".git"), "ref: refs/heads/feature/list-live\n");
|
||||
let plain = tmp.path().join("plain");
|
||||
std::fs::create_dir_all(&plain).unwrap();
|
||||
|
||||
let recent = RecentProjects::load(tmp.path().join("config").as_path());
|
||||
recent.touch(&repo, Some("stale".into())).unwrap();
|
||||
recent.touch(&plain, Some("kept".into())).unwrap();
|
||||
|
||||
let live = recent.list_live();
|
||||
let by_path = |p: &Path| live.iter().find(|i| i.path.as_ref() == p).unwrap();
|
||||
assert_eq!(by_path(&repo).branch.as_deref(), Some("feature/list-live"));
|
||||
assert_eq!(by_path(&plain).branch.as_deref(), Some("kept"));
|
||||
assert_eq!(recent.list().iter().find(|i| i.path.as_ref() == repo).unwrap().branch.as_deref(), Some("stale"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_live_follows_worktree_gitdir_and_shows_short_detached_head() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let main = tmp.path().join("main");
|
||||
let wt = tmp.path().join("wt");
|
||||
std::fs::create_dir_all(&wt).unwrap();
|
||||
let wt_git = main.join(".git/worktrees/wt");
|
||||
write_head(&wt_git, "abcdefghijklmnopqrstuvwxyz0123456789abcd\n");
|
||||
std::fs::write(wt.join(".git"), "gitdir: ../main/.git/worktrees/wt\n").unwrap();
|
||||
|
||||
assert_eq!(read_head_branch(&wt).unwrap().as_deref(), Some("abcdefg"));
|
||||
|
||||
let recent = RecentProjects::load(tmp.path().join("config").as_path());
|
||||
recent.touch(&wt, Some("old-branch".into())).unwrap();
|
||||
assert_eq!(recent.list_live()[0].branch.as_deref(), Some("abcdefg"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,15 @@ pub fn app_info(state: State<'_, AppState>) -> AppInfo {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn recent_projects(state: State<'_, AppState>) -> Vec<RecentProject> {
|
||||
pub async fn recent_projects(app: tauri::AppHandle) -> Vec<RecentProject> {
|
||||
let state = app.state::<AppState>();
|
||||
state.note_ipc();
|
||||
state.recent.list()
|
||||
let items = state.recent.list();
|
||||
// Reading every project's `.git/HEAD` may touch slow disks: off the async runtime.
|
||||
match tauri::async_runtime::spawn_blocking(move || ide_workspace::recent::with_live_branches(items)).await {
|
||||
Ok(items) => items,
|
||||
Err(_) => state.recent.list(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
Reference in New Issue
Block a user