diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ff7bc426..55515f76 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -950,7 +950,7 @@ dependencies = [ "clap", "codex-common", "codex-core", - "codex-git-apply", + "codex-git", "serde", "serde_json", "tempfile", @@ -1027,7 +1027,7 @@ dependencies = [ "async-trait", "chrono", "codex-backend-client", - "codex-git-apply", + "codex-git", "diffy", "serde", "serde_json", @@ -1063,7 +1063,7 @@ dependencies = [ "codex-apply-patch", "codex-async-utils", "codex-file-search", - "codex-git-tooling", + "codex-git", "codex-keyring-store", "codex-otel", "codex-protocol", @@ -1202,20 +1202,13 @@ dependencies = [ ] [[package]] -name = "codex-git-apply" -version = "0.0.0" -dependencies = [ - "once_cell", - "regex", - "tempfile", -] - -[[package]] -name = "codex-git-tooling" +name = "codex-git" version = "0.0.0" dependencies = [ "assert_matches", + "once_cell", "pretty_assertions", + "regex", "schemars 0.8.22", "serde", "tempfile", @@ -1346,7 +1339,7 @@ version = "0.0.0" dependencies = [ "anyhow", "base64", - "codex-git-tooling", + "codex-git", "codex-utils-image", "icu_decimal", "icu_locale_core", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index f9d0865f..79ad9a6d 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -18,7 +18,6 @@ members = [ "execpolicy", "keyring-store", "file-search", - "git-tooling", "linux-sandbox", "login", "mcp-server", @@ -32,7 +31,7 @@ members = [ "stdio-to-uds", "otel", "tui", - "git-apply", + "utils/git", "utils/cache", "utils/image", "utils/json-to-toml", @@ -67,7 +66,7 @@ codex-core = { path = "core" } codex-exec = { path = "exec" } codex-feedback = { path = "feedback" } codex-file-search = { path = "file-search" } -codex-git-tooling = { path = "git-tooling" } +codex-git = { path = "utils/git" } codex-keyring-store = { path = "keyring-store" } codex-linux-sandbox = { path = "linux-sandbox" } codex-login = { path = "login" } diff --git a/codex-rs/chatgpt/Cargo.toml b/codex-rs/chatgpt/Cargo.toml index 0a317086..c46046b1 100644 --- a/codex-rs/chatgpt/Cargo.toml +++ b/codex-rs/chatgpt/Cargo.toml @@ -14,7 +14,7 @@ codex-core = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["full"] } -codex-git-apply = { path = "../git-apply" } +codex-git = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/codex-rs/chatgpt/src/apply_command.rs b/codex-rs/chatgpt/src/apply_command.rs index e1289c21..ffd460e2 100644 --- a/codex-rs/chatgpt/src/apply_command.rs +++ b/codex-rs/chatgpt/src/apply_command.rs @@ -59,13 +59,13 @@ pub async fn apply_diff_from_task( async fn apply_diff(diff: &str, cwd: Option) -> anyhow::Result<()> { let cwd = cwd.unwrap_or(std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir())); - let req = codex_git_apply::ApplyGitRequest { + let req = codex_git::ApplyGitRequest { cwd, diff: diff.to_string(), revert: false, preflight: false, }; - let res = codex_git_apply::apply_git_patch(&req)?; + let res = codex_git::apply_git_patch(&req)?; if res.exit_code != 0 { anyhow::bail!( "Git apply failed (applied={}, skipped={}, conflicts={})\nstdout:\n{}\nstderr:\n{}", diff --git a/codex-rs/cloud-tasks-client/Cargo.toml b/codex-rs/cloud-tasks-client/Cargo.toml index ca45b6e1..58dfea73 100644 --- a/codex-rs/cloud-tasks-client/Cargo.toml +++ b/codex-rs/cloud-tasks-client/Cargo.toml @@ -24,4 +24,4 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2.0.12" codex-backend-client = { path = "../backend-client", optional = true } -codex-git-apply = { path = "../git-apply" } +codex-git = { workspace = true } diff --git a/codex-rs/cloud-tasks-client/src/http.rs b/codex-rs/cloud-tasks-client/src/http.rs index 912681cd..57d39b7b 100644 --- a/codex-rs/cloud-tasks-client/src/http.rs +++ b/codex-rs/cloud-tasks-client/src/http.rs @@ -362,13 +362,13 @@ mod api { }); } - let req = codex_git_apply::ApplyGitRequest { + let req = codex_git::ApplyGitRequest { cwd: std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir()), diff: diff.clone(), revert: false, preflight, }; - let r = codex_git_apply::apply_git_patch(&req) + let r = codex_git::apply_git_patch(&req) .map_err(|e| CloudTaskError::Io(format!("git apply failed to run: {e}")))?; let status = if r.exit_code == 0 { diff --git a/codex-rs/cloud-tasks-client/src/lib.rs b/codex-rs/cloud-tasks-client/src/lib.rs index 1c5a01e2..a723512f 100644 --- a/codex-rs/cloud-tasks-client/src/lib.rs +++ b/codex-rs/cloud-tasks-client/src/lib.rs @@ -26,4 +26,4 @@ pub use mock::MockClient; #[cfg(feature = "online")] pub use http::HttpClient; -// Reusable apply engine now lives in the shared crate `codex-git-apply`. +// Reusable apply engine now lives in the shared crate `codex-git`. diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 3fc6ccd0..921ca284 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -23,7 +23,7 @@ codex-app-server-protocol = { workspace = true } codex-apply-patch = { workspace = true } codex-async-utils = { workspace = true } codex-file-search = { workspace = true } -codex-git-tooling = { workspace = true } +codex-git = { workspace = true } codex-keyring-store = { workspace = true } codex-otel = { workspace = true, features = ["otel"] } codex-protocol = { workspace = true } diff --git a/codex-rs/core/src/conversation_history.rs b/codex-rs/core/src/conversation_history.rs index 9c724df6..9c9e4889 100644 --- a/codex-rs/core/src/conversation_history.rs +++ b/codex-rs/core/src/conversation_history.rs @@ -530,7 +530,7 @@ fn is_api_message(message: &ResponseItem) -> bool { #[cfg(test)] mod tests { use super::*; - use codex_git_tooling::GhostCommit; + use codex_git::GhostCommit; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::LocalShellAction; diff --git a/codex-rs/core/src/tasks/ghost_snapshot.rs b/codex-rs/core/src/tasks/ghost_snapshot.rs index f421a91e..93830a30 100644 --- a/codex-rs/core/src/tasks/ghost_snapshot.rs +++ b/codex-rs/core/src/tasks/ghost_snapshot.rs @@ -3,9 +3,9 @@ use crate::state::TaskKind; use crate::tasks::SessionTask; use crate::tasks::SessionTaskContext; use async_trait::async_trait; -use codex_git_tooling::CreateGhostCommitOptions; -use codex_git_tooling::GitToolingError; -use codex_git_tooling::create_ghost_commit; +use codex_git::CreateGhostCommitOptions; +use codex_git::GitToolingError; +use codex_git::create_ghost_commit; use codex_protocol::models::ResponseItem; use codex_protocol::user_input::UserInput; use codex_utils_readiness::Readiness; diff --git a/codex-rs/core/src/tasks/undo.rs b/codex-rs/core/src/tasks/undo.rs index e834eea0..9862a7ec 100644 --- a/codex-rs/core/src/tasks/undo.rs +++ b/codex-rs/core/src/tasks/undo.rs @@ -8,7 +8,7 @@ use crate::state::TaskKind; use crate::tasks::SessionTask; use crate::tasks::SessionTaskContext; use async_trait::async_trait; -use codex_git_tooling::restore_ghost_commit; +use codex_git::restore_ghost_commit; use codex_protocol::models::ResponseItem; use codex_protocol::user_input::UserInput; use tokio_util::sync::CancellationToken; diff --git a/codex-rs/git-apply/Cargo.toml b/codex-rs/git-apply/Cargo.toml deleted file mode 100644 index 0c17ccc3..00000000 --- a/codex-rs/git-apply/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "codex-git-apply" -version = { workspace = true } -edition = "2024" - -[lib] -name = "codex_git_apply" -path = "src/lib.rs" - -[lints] -workspace = true - -[dependencies] -once_cell = "1" -regex = "1" -tempfile = "3" - diff --git a/codex-rs/git-tooling/README.md b/codex-rs/git-tooling/README.md deleted file mode 100644 index 8833fc58..00000000 --- a/codex-rs/git-tooling/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# codex-git-tooling - -Helpers for interacting with git. - -```rust,no_run -use std::path::Path; - -use codex_git_tooling::{create_ghost_commit, restore_ghost_commit, CreateGhostCommitOptions}; - -let repo = Path::new("/path/to/repo"); - -// Capture the current working tree as an unreferenced commit. -let ghost = create_ghost_commit(&CreateGhostCommitOptions::new(repo))?; - -// Later, undo back to that state. -restore_ghost_commit(repo, &ghost)?; -``` - -Pass a custom message with `.message("…")` or force-include ignored files with -`.force_include(["ignored.log".into()])`. diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index 3258a984..f04f33bd 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -11,7 +11,7 @@ path = "src/lib.rs" workspace = true [dependencies] -codex-git-tooling = { workspace = true } +codex-git = { workspace = true } base64 = { workspace = true } codex-utils-image = { workspace = true } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index a91cef1d..96f17bab 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -11,7 +11,7 @@ use serde::ser::Serializer; use ts_rs::TS; use crate::user_input::UserInput; -use codex_git_tooling::GhostCommit; +use codex_git::GhostCommit; use codex_utils_image::error::ImageProcessingError; use schemars::JsonSchema; diff --git a/codex-rs/git-tooling/Cargo.toml b/codex-rs/utils/git/Cargo.toml similarity index 86% rename from codex-rs/git-tooling/Cargo.toml rename to codex-rs/utils/git/Cargo.toml index c30d58de..072587bd 100644 --- a/codex-rs/git-tooling/Cargo.toml +++ b/codex-rs/utils/git/Cargo.toml @@ -1,27 +1,25 @@ [package] -name = "codex-git-tooling" +name = "codex-git" version.workspace = true edition.workspace = true readme = "README.md" -[lib] -name = "codex_git_tooling" -path = "src/lib.rs" +[lints] +workspace = true [dependencies] -tempfile = { workspace = true } -thiserror = { workspace = true } -walkdir = { workspace = true } +once_cell = "1" +regex = "1" schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } +tempfile = { workspace = true } +thiserror = { workspace = true } ts-rs = { workspace = true, features = [ "uuid-impl", "serde-json-impl", "no-serde-warnings", ] } - -[lints] -workspace = true +walkdir = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/codex-rs/utils/git/README.md b/codex-rs/utils/git/README.md new file mode 100644 index 00000000..5bcc6dcc --- /dev/null +++ b/codex-rs/utils/git/README.md @@ -0,0 +1,33 @@ +# codex-git + +Helpers for interacting with git, including patch application and worktree +snapshot utilities. + +```rust,no_run +use std::path::Path; + +use codex_git::{ + apply_git_patch, create_ghost_commit, restore_ghost_commit, ApplyGitRequest, + CreateGhostCommitOptions, +}; + +let repo = Path::new("/path/to/repo"); + +// Apply a patch (omitted here) to the repository. +let request = ApplyGitRequest { + cwd: repo.to_path_buf(), + diff: String::from("...diff contents..."), + revert: false, + preflight: false, +}; +let result = apply_git_patch(&request)?; + +// Capture the current working tree as an unreferenced commit. +let ghost = create_ghost_commit(&CreateGhostCommitOptions::new(repo))?; + +// Later, undo back to that state. +restore_ghost_commit(repo, &ghost)?; +``` + +Pass a custom message with `.message("…")` or force-include ignored files with +`.force_include(["ignored.log".into()])`. diff --git a/codex-rs/git-apply/src/lib.rs b/codex-rs/utils/git/src/apply.rs similarity index 95% rename from codex-rs/git-apply/src/lib.rs rename to codex-rs/utils/git/src/apply.rs index 5d146312..c9e85032 100644 --- a/codex-rs/git-apply/src/lib.rs +++ b/codex-rs/utils/git/src/apply.rs @@ -1,3 +1,11 @@ +//! Helpers for applying unified diffs using the system `git` binary. +//! +//! The entry point is [`apply_git_patch`], which writes a diff to a temporary +//! file, shells out to `git apply` with the right flags, and then parses the +//! command’s output into structured details. Callers can opt into dry-run +//! mode via [`ApplyGitRequest::preflight`] and inspect the resulting paths to +//! learn what would change before applying for real. + use once_cell::sync::Lazy; use regex::Regex; use std::ffi::OsStr; @@ -5,6 +13,7 @@ use std::io; use std::path::Path; use std::path::PathBuf; +/// Parameters for invoking [`apply_git_patch`]. #[derive(Debug, Clone)] pub struct ApplyGitRequest { pub cwd: PathBuf, @@ -13,6 +22,7 @@ pub struct ApplyGitRequest { pub preflight: bool, } +/// Result of running [`apply_git_patch`], including paths gleaned from stdout/stderr. #[derive(Debug, Clone)] pub struct ApplyGitResult { pub exit_code: i32, @@ -24,6 +34,10 @@ pub struct ApplyGitResult { pub cmd_for_log: String, } +/// Apply a unified diff to the target repository by shelling out to `git apply`. +/// +/// When [`ApplyGitRequest::preflight`] is `true`, this behaves like `git apply --check` and +/// leaves the working tree untouched while still parsing the command output for diagnostics. pub fn apply_git_patch(req: &ApplyGitRequest) -> io::Result { let git_root = resolve_git_root(&req.cwd)?; @@ -176,6 +190,7 @@ fn render_command_for_log(cwd: &Path, git_cfg: &[String], args: &[String]) -> St ) } +/// Collect every path referenced by the diff headers inside `diff --git` sections. pub fn extract_paths_from_patch(diff_text: &str) -> Vec { static RE: Lazy = Lazy::new(|| { Regex::new(r"(?m)^diff --git a/(.*?) b/(.*)$") @@ -199,6 +214,7 @@ pub fn extract_paths_from_patch(diff_text: &str) -> Vec { set.into_iter().collect() } +/// Stage only the files that actually exist on disk for the given diff. pub fn stage_paths(git_root: &Path, diff: &str) -> io::Result<()> { let paths = extract_paths_from_patch(diff); let mut existing: Vec = Vec::new(); @@ -225,6 +241,7 @@ pub fn stage_paths(git_root: &Path, diff: &str) -> io::Result<()> { // ============ Parser ported from VS Code (TS) ============ +/// Parse `git apply` output into applied/skipped/conflicted path groupings. pub fn parse_git_apply_output( stdout: &str, stderr: &str, diff --git a/codex-rs/git-tooling/src/errors.rs b/codex-rs/utils/git/src/errors.rs similarity index 100% rename from codex-rs/git-tooling/src/errors.rs rename to codex-rs/utils/git/src/errors.rs diff --git a/codex-rs/git-tooling/src/ghost_commits.rs b/codex-rs/utils/git/src/ghost_commits.rs similarity index 100% rename from codex-rs/git-tooling/src/ghost_commits.rs rename to codex-rs/utils/git/src/ghost_commits.rs diff --git a/codex-rs/git-tooling/src/lib.rs b/codex-rs/utils/git/src/lib.rs similarity index 90% rename from codex-rs/git-tooling/src/lib.rs rename to codex-rs/utils/git/src/lib.rs index e6e53924..cf2887aa 100644 --- a/codex-rs/git-tooling/src/lib.rs +++ b/codex-rs/utils/git/src/lib.rs @@ -1,11 +1,18 @@ use std::fmt; use std::path::PathBuf; +mod apply; mod errors; mod ghost_commits; mod operations; mod platform; +pub use apply::ApplyGitRequest; +pub use apply::ApplyGitResult; +pub use apply::apply_git_patch; +pub use apply::extract_paths_from_patch; +pub use apply::parse_git_apply_output; +pub use apply::stage_paths; pub use errors::GitToolingError; pub use ghost_commits::CreateGhostCommitOptions; pub use ghost_commits::create_ghost_commit; diff --git a/codex-rs/git-tooling/src/operations.rs b/codex-rs/utils/git/src/operations.rs similarity index 100% rename from codex-rs/git-tooling/src/operations.rs rename to codex-rs/utils/git/src/operations.rs diff --git a/codex-rs/git-tooling/src/platform.rs b/codex-rs/utils/git/src/platform.rs similarity index 89% rename from codex-rs/git-tooling/src/platform.rs rename to codex-rs/utils/git/src/platform.rs index 06feff75..41d4e2ea 100644 --- a/codex-rs/git-tooling/src/platform.rs +++ b/codex-rs/utils/git/src/platform.rs @@ -34,4 +34,4 @@ pub fn create_symlink( } #[cfg(not(any(unix, windows)))] -compile_error!("codex-git-tooling symlink support is only implemented for Unix and Windows"); +compile_error!("codex-git symlink support is only implemented for Unix and Windows");