Skip to content
Merged
Changes from 2 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
38 changes: 31 additions & 7 deletions crates/east-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,25 @@ async fn do_update(workspace_root: &Path) -> miette::Result<()> {
return Ok(());
}

let total = projects.len() as u64;
let mp = MultiProgress::new();
let style = ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")

// Top-level progress bar showing overall completion
let overall_style = ProgressStyle::default_bar()
.template("[{bar:30.cyan/dim}] {pos}/{len} {msg}")
.expect("valid template")
.progress_chars("##-");
let overall = mp.add(ProgressBar::new(total));
overall.set_style(overall_style);
overall.set_message("updating...");

// Style for per-task spinners (inserted below the overall bar)
let spinner_style = ProgressStyle::default_spinner()
.template(" {spinner:.green} {msg}")
.expect("valid template");

let semaphore = std::sync::Arc::new(Semaphore::new(MAX_CONCURRENT_GIT));
let overall = std::sync::Arc::new(overall);
let mut handles = Vec::new();

for project in &projects {
Expand All @@ -221,8 +234,9 @@ async fn do_update(workspace_root: &Path) -> miette::Result<()> {
let clone_url = manifest.project_clone_url(project).ok();
let project_name = project.name.clone();
let sem = semaphore.clone();
let pb = mp.add(ProgressBar::new_spinner());
pb.set_style(style.clone());
let overall = overall.clone();
let pb = mp.insert_after(&overall, ProgressBar::new_spinner());
pb.set_style(spinner_style.clone());
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Per-task ProgressBar::new_spinner() never calls tick() or enable_steady_tick(...), so the spinner glyph will stay static (and may not render as intended) until some other redraw happens. Consider enabling a steady tick for these spinners (bounded by MAX_CONCURRENT_GIT), or explicitly ticking on state updates so the UI matches the intended “spinner” behavior.

Copilot uses AI. Check for mistakes.

let handle = tokio::spawn(async move {
let _permit = sem.acquire().await.expect("semaphore closed");
Expand All @@ -249,9 +263,16 @@ async fn do_update(workspace_root: &Path) -> miette::Result<()> {
pb.set_message(format!("{project_name}: initializing..."));
Git::init_and_fetch(url, &project_path, revision.as_deref()).await
} else {
// Clone
// Clone — fallback to init+fetch if the directory was
// created by a concurrent sibling clone in the meantime.
pb.set_message(format!("{project_name}: cloning..."));
Git::clone(url, &project_path, revision.as_deref()).await
let clone_result = Git::clone(url, &project_path, revision.as_deref()).await;
if clone_result.is_err() && project_path.exists() {
pb.set_message(format!("{project_name}: initializing (fallback)..."));
Git::init_and_fetch(url, &project_path, revision.as_deref()).await
} else {
clone_result
}
Comment on lines +320 to +334
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The clone fallback triggers on any clone error as long as project_path.exists() afterward. Since git clone often creates the destination directory even when failing (e.g., auth/network failures), this can replace the original error with an init_and_fetch failure (e.g., remote origin already exists) and make debugging harder. Restrict the fallback to the specific “destination exists / not empty” failure mode (e.g., by inspecting the error’s stderr), or re-check that the destination is still a non-repo directory before falling back.

Copilot uses AI. Check for mistakes.
}
} else {
Err(east_vcs::error::VcsError::GitFailed {
Expand All @@ -260,10 +281,12 @@ async fn do_update(workspace_root: &Path) -> miette::Result<()> {
})
};

// Update UI: remove spinner on success, keep failure visible
match &result {
Ok(()) => pb.finish_with_message(format!("{project_name}: done")),
Ok(()) => pb.finish_and_clear(),
Err(e) => pb.finish_with_message(format!("{project_name}: FAILED ({e})")),
}
overall.inc(1);
result
});
handles.push((project.name.clone(), handle));
Expand All @@ -277,6 +300,7 @@ async fn do_update(workspace_root: &Path) -> miette::Result<()> {
Err(e) => errors.push(format!("{name}: task panicked: {e}")),
}
}
overall.finish_and_clear();

if errors.is_empty() {
println!("updated {} projects", projects.len());
Expand Down