Add worktree startup helpers

This commit is contained in:
starr-openai
2026-04-03 17:57:59 -07:00
parent eb462e8f76
commit cab5fd2f11
11 changed files with 751 additions and 6 deletions
+1
View File
@@ -31,6 +31,7 @@ codex-core = { workspace = true }
codex-exec = { workspace = true }
codex-execpolicy = { workspace = true }
codex-features = { workspace = true }
codex-git-utils = { workspace = true }
codex-login = { workspace = true }
codex-mcp-server = { workspace = true }
codex-protocol = { workspace = true }
+14
View File
@@ -39,10 +39,13 @@ mod app_cmd;
#[cfg(target_os = "macos")]
mod desktop_app;
mod mcp_cmd;
mod worktree_cmd;
#[cfg(not(windows))]
mod wsl_paths;
use crate::mcp_cmd::McpCli;
use crate::worktree_cmd::WorktreeCli;
use crate::worktree_cmd::run_worktree_command;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
@@ -150,6 +153,9 @@ enum Subcommand {
/// Inspect feature flags.
Features(FeaturesCli),
/// Manage Codex-created local git worktrees.
Worktree(WorktreeCli),
}
#[derive(Debug, Parser)]
@@ -1028,6 +1034,14 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
disable_feature_in_config(&interactive, &feature).await?;
}
},
Some(Subcommand::Worktree(worktree_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"worktree",
)?;
run_worktree_command(worktree_cli)?;
}
}
Ok(())
+134
View File
@@ -0,0 +1,134 @@
use clap::Parser;
use codex_core::config::find_codex_home;
use codex_git_utils::CodexWorktreePruneCandidate;
use codex_git_utils::CodexWorktreePruneMode;
use codex_git_utils::CodexWorktreePruneOptions;
use codex_git_utils::CodexWorktreePruneSkipReason;
use codex_git_utils::CodexWorktreePruneSkipped;
use codex_git_utils::prune_codex_managed_worktrees;
use std::path::PathBuf;
#[derive(Debug, Parser)]
pub(crate) struct WorktreeCli {
#[command(subcommand)]
pub(crate) subcommand: WorktreeSubcommand,
}
#[derive(Debug, Parser)]
pub(crate) enum WorktreeSubcommand {
/// Remove old Codex-managed local git worktrees.
Prune(WorktreePruneCommand),
}
#[derive(Debug, Parser)]
pub(crate) struct WorktreePruneCommand {
/// Only prune worktrees created from this source repository.
#[arg(long = "repo", value_name = "DIR")]
repo: Option<PathBuf>,
/// Only prune worktrees created before this Unix timestamp (seconds).
#[arg(long = "created-before", value_name = "UNIX_SECONDS")]
created_before: Option<u64>,
/// Only prune worktrees last used before this Unix timestamp (seconds).
#[arg(long = "last-used-before", value_name = "UNIX_SECONDS")]
last_used_before: Option<u64>,
/// Print matching worktrees without deleting them.
#[arg(long = "dry-run", default_value_t = false)]
dry_run: bool,
}
pub(crate) fn run_worktree_command(worktree_cli: WorktreeCli) -> anyhow::Result<()> {
match worktree_cli.subcommand {
WorktreeSubcommand::Prune(command) => run_worktree_prune_command(command),
}
}
fn run_worktree_prune_command(command: WorktreePruneCommand) -> anyhow::Result<()> {
let options = CodexWorktreePruneOptions {
codex_home: find_codex_home()?,
source_repo_root: command.repo,
created_before: command.created_before,
last_used_before: command.last_used_before,
mode: if command.dry_run {
CodexWorktreePruneMode::DryRun
} else {
CodexWorktreePruneMode::Delete
},
};
let report = prune_codex_managed_worktrees(&options)?;
for candidate in &report.pruned {
print_candidate("pruned", candidate);
}
for skipped in &report.skipped {
print_skipped_candidate(skipped);
}
for candidate in &report.kept {
print_candidate("kept", candidate);
}
println!(
"summary: pruned={} skipped={} kept={}",
report.pruned.len(),
report.skipped.len(),
report.kept.len()
);
Ok(())
}
fn print_candidate(action: &str, candidate: &CodexWorktreePruneCandidate) {
println!(
"{action} {} repo={} created_at={} last_used_at={}",
candidate.worktree_git_root.display(),
candidate.metadata.source_repo_root.display(),
candidate.metadata.created_at,
candidate.metadata.last_used_at
);
}
fn print_skipped_candidate(skipped: &CodexWorktreePruneSkipped) {
let reason = match skipped.reason {
CodexWorktreePruneSkipReason::DirtyWorktree => "dirty-worktree",
CodexWorktreePruneSkipReason::LocalCommits => "local-commits",
CodexWorktreePruneSkipReason::MissingStartingRef => "missing-starting-ref",
};
println!(
"skipped {} reason={} repo={} created_at={} last_used_at={}",
skipped.candidate.worktree_git_root.display(),
reason,
skipped.candidate.metadata.source_repo_root.display(),
skipped.candidate.metadata.created_at,
skipped.candidate.metadata.last_used_at
);
}
#[cfg(test)]
mod tests {
use super::WorktreeCli;
use super::WorktreeSubcommand;
use clap::Parser;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
#[test]
fn parse_worktree_prune_filters() {
let cli = WorktreeCli::parse_from([
"codex-worktree",
"prune",
"--repo",
"/tmp/repo",
"--created-before",
"123",
"--last-used-before",
"456",
"--dry-run",
]);
let WorktreeSubcommand::Prune(command) = cli.subcommand;
assert_eq!(command.repo, Some(PathBuf::from("/tmp/repo")));
assert_eq!(command.created_before, Some(123));
assert_eq!(command.last_used_before, Some(456));
assert!(command.dry_run);
}
}