feat(core/git): after creating worktree, ensure 'origin' remote exists by aliasing a writable remote (fork/upstream-push) and set origin/HEAD for default-branch detection; improves default git command UX in worktrees

This commit is contained in:
James Peter
2025-09-08 11:02:30 +10:00
parent 16b7800501
commit c59fd7e2ba
3 changed files with 107 additions and 1 deletions
+76
View File
@@ -126,9 +126,85 @@ pub async fn setup_worktree(git_root: &Path, branch_id: &str) -> Result<(PathBuf
}
}
// Ensure an 'origin' remote exists and points to a writable remote so that
// generic commands like `git push origin HEAD:main` work out of the box.
ensure_origin_remote(git_root).await.ok();
Ok((worktree_path, effective_branch))
}
/// Ensure a remote named `origin` exists. If it's missing, choose a likely
/// writable remote (prefer `fork`, then `upstream-push`, then the first push
/// URL we find) and alias it as `origin`. Finally, set the remote HEAD so
/// `origin/HEAD` points at the default branch.
async fn ensure_origin_remote(git_root: &Path) -> Result<(), String> {
// Check existing remotes
let remotes_out = Command::new("git")
.current_dir(git_root)
.args(["remote"])
.output()
.await
.map_err(|e| format!("git remote failed: {}", e))?;
if !remotes_out.status.success() {
return Err("git remote returned error".to_string());
}
let remotes: Vec<String> = String::from_utf8_lossy(&remotes_out.stdout)
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if remotes.iter().any(|r| r == "origin") {
// Make sure origin/HEAD is set; ignore errors
let _ = Command::new("git")
.current_dir(git_root)
.args(["remote", "set-head", "origin", "-a"])
.output()
.await;
return Ok(());
}
// Prefer candidates in this order
let mut candidates = vec!["fork", "upstream-push", "upstream"]; // typical setups
// Append any other remotes as fallbacks
for r in &remotes {
if !candidates.contains(&r.as_str()) { candidates.push(r); }
}
// Find a candidate with a URL
for cand in candidates {
let url_out = Command::new("git")
.current_dir(git_root)
.args(["remote", "get-url", cand])
.output()
.await;
if let Ok(out) = url_out {
if out.status.success() {
let url = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !url.is_empty() {
// Add origin pointing to this URL
let add = Command::new("git")
.current_dir(git_root)
.args(["remote", "add", "origin", &url])
.output()
.await
.map_err(|e| format!("git remote add origin failed: {}", e))?;
if !add.status.success() {
return Err("failed to add origin".to_string());
}
let _ = Command::new("git")
.current_dir(git_root)
.args(["remote", "set-head", "origin", "-a"])
.output()
.await;
return Ok(());
}
}
}
}
// No usable remote found; leave as-is
Err("no suitable remote to alias as origin".to_string())
}
/// Copy uncommitted (modified + untracked) files from `src_root` into the `worktree_path`.
/// Returns the number of files copied.
pub async fn copy_uncommitted_to_worktree(src_root: &Path, worktree_path: &Path) -> Result<usize, String> {
+1 -1
View File
@@ -872,7 +872,7 @@ impl App<'_> {
if let AppState::Chat { widget } = &mut self.app_state {
if let Some(prompt) = initial_prompt {
if !prompt.is_empty() {
let preface = "[internal] When you finish this task, ask the user if they want any changes. If they are happy, offer to merge the branch back into the repository's default branch and delete the worktree. Wait for explicit confirmation before merging.".to_string();
let preface = "[internal] When you finish this task, ask the user if they want any changes. If they are happy, offer to merge the branch back into the repository's default branch and delete the worktree. Use '/branch finalize' (or an equivalent git worktree remove + switch) rather than deleting the folder directly so the UI can switch back cleanly. Wait for explicit confirmation before merging.".to_string();
widget.submit_text_message_with_preface(prompt, preface);
}
}
+30
View File
@@ -1891,6 +1891,36 @@ impl ChatWidget<'_> {
// Fade the welcome cell only when a user actually posts a message.
for cell in &self.history_cells { cell.trigger_fade(); }
let UserMessage { display_text, mut ordered_items } = user_message;
// If our configured cwd no longer exists (e.g., a worktree folder was
// deleted outside the app), try to automatically recover to the repo
// root for worktrees and re-submit the same message there.
if !self.config.cwd.exists() {
let missing = self.config.cwd.clone();
let missing_s = missing.display().to_string();
if missing_s.contains("/.code/branches/") {
// Recover by walking up to '<repo>/.code/branches/<branch>' -> repo root
let mut anc = missing.as_path();
// Walk up 3 parents if available
for _ in 0..3 { if let Some(p) = anc.parent() { anc = p; } }
let fallback_root = anc.to_path_buf();
if fallback_root.exists() {
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
let msg = format!("⚠️ Worktree directory is missing: {}\nSwitching to repo root: {}",
missing.display(), fallback_root.display());
let _ = self.app_event_tx.send(AppEvent::CodexEvent(Event { id: "cwd-recover".to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: msg }), order: None }));
// Re-submit this exact message after switching cwd
self.app_event_tx
.send(AppEvent::SwitchCwd(fallback_root, Some(display_text.clone())));
return;
}
}
// If we can't recover, surface an error and drop the message to prevent loops
self.history_push(history_cell::new_error_event(format!(
"Working directory is missing: {}",
self.config.cwd.display()
)));
return;
}
let original_text = display_text.clone();
// Build a combined string view of the text-only parts to process slash commands
let mut text_only = String::new();