ralph_workflow/git_helpers/repo/
commit.rs1use std::io;
2use std::path::Path;
3
4use crate::git_helpers::git2_to_io_error;
5use crate::git_helpers::identity::GitIdentity;
6
7fn index_has_changes_to_commit(repo: &git2::Repository, index: &git2::Index) -> io::Result<bool> {
8 match repo.head() {
9 Ok(head) => {
10 let head_tree = head.peel_to_tree().map_err(|e| git2_to_io_error(&e))?;
11 let diff = repo
12 .diff_tree_to_index(Some(&head_tree), Some(index), None)
13 .map_err(|e| git2_to_io_error(&e))?;
14 Ok(diff.deltas().len() > 0)
15 }
16 Err(ref e) if e.code() == git2::ErrorCode::UnbornBranch => Ok(!index.is_empty()),
17 Err(e) => Err(git2_to_io_error(&e)),
18 }
19}
20
21fn is_internal_agent_artifact(path: &std::path::Path) -> bool {
22 let path_str = path.to_string_lossy();
23 path_str == ".no_agent_commit"
24 || path_str == ".agent"
25 || path_str.starts_with(".agent/")
26 || path_str == ".git"
27 || path_str.starts_with(".git/")
28}
29
30pub fn git_add_specific_in_repo(repo_root: &Path, files: &[&str]) -> io::Result<bool> {
44 let repo = git2::Repository::discover(repo_root).map_err(|e| git2_to_io_error(&e))?;
45 let mut index = repo.index().map_err(|e| git2_to_io_error(&e))?;
46
47 match repo.head() {
50 Ok(head) => {
51 let head_tree = head.peel_to_tree().map_err(|e| git2_to_io_error(&e))?;
52 index
53 .read_tree(&head_tree)
54 .map_err(|e| git2_to_io_error(&e))?;
55 }
56 Err(ref e) if e.code() == git2::ErrorCode::UnbornBranch => {
57 index.clear().map_err(|e| git2_to_io_error(&e))?;
58 }
59 Err(e) => return Err(git2_to_io_error(&e)),
60 }
61
62 for path_str in files {
63 let path = std::path::Path::new(path_str);
64 if is_internal_agent_artifact(path) {
65 continue;
66 }
67
68 match index.add_path(path) {
69 Ok(()) => {}
70 Err(ref e) if e.code() == git2::ErrorCode::NotFound => {
71 let tracked_in_head = index.get_path(path, 0).is_some();
75 if !tracked_in_head {
76 let io_err = git2_to_io_error(e);
77 return Err(io::Error::new(
78 io_err.kind(),
79 format!(
80 "path '{}' not found for selective staging: {io_err}",
81 path.display()
82 ),
83 ));
84 }
85
86 index.remove_path(path).map_err(|remove_err| {
88 let io_err = git2_to_io_error(&remove_err);
89 io::Error::new(
90 io_err.kind(),
91 format!(
92 "failed to stage deletion for '{}': {io_err}",
93 path.display()
94 ),
95 )
96 })?;
97 }
98 Err(e) => {
99 let io_err = git2_to_io_error(&e);
100 return Err(io::Error::new(
101 io_err.kind(),
102 format!("failed to stage path '{}': {io_err}", path.display()),
103 ));
104 }
105 }
106 }
107
108 index.write().map_err(|e| git2_to_io_error(&e))?;
109 index_has_changes_to_commit(&repo, &index)
110}
111
112pub fn git_add_all() -> io::Result<bool> {
125 git_add_all_in_repo(Path::new("."))
126}
127
128pub fn git_add_all_in_repo(repo_root: &Path) -> io::Result<bool> {
137 let repo = git2::Repository::discover(repo_root).map_err(|e| git2_to_io_error(&e))?;
138 git_add_all_impl(&repo)
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum CommitResultFallback {
146 Success(git2::Oid),
148 NoChanges,
150 Failed(String),
152}
153
154fn git_add_all_impl(repo: &git2::Repository) -> io::Result<bool> {
156 let mut index = repo.index().map_err(|e| git2_to_io_error(&e))?;
157
158 let mut status_opts = git2::StatusOptions::new();
161 status_opts
162 .include_untracked(true)
163 .recurse_untracked_dirs(true)
164 .include_ignored(false);
165 let statuses = repo
166 .statuses(Some(&mut status_opts))
167 .map_err(|e| git2_to_io_error(&e))?;
168 for entry in statuses.iter() {
169 if entry.status().contains(git2::Status::WT_DELETED) {
170 if let Some(path) = entry.path() {
171 index
172 .remove_path(std::path::Path::new(path))
173 .map_err(|e| git2_to_io_error(&e))?;
174 }
175 }
176 }
177
178 let mut filter_cb = |path: &std::path::Path, _matched: &[u8]| -> i32 {
181 i32::from(is_internal_agent_artifact(path))
184 };
185 index
186 .add_all(
187 vec!["."],
188 git2::IndexAddOption::DEFAULT,
189 Some(&mut filter_cb),
190 )
191 .map_err(|e| git2_to_io_error(&e))?;
192
193 index.write().map_err(|e| git2_to_io_error(&e))?;
194
195 index_has_changes_to_commit(repo, &index)
197}
198
199fn resolve_commit_identity(
200 repo: &git2::Repository,
201 provided_name: Option<&str>,
202 provided_email: Option<&str>,
203 executor: Option<&dyn crate::executor::ProcessExecutor>,
204) -> GitIdentity {
205 use crate::git_helpers::identity::{default_identity, fallback_email, fallback_username};
206
207 let mut name = String::new();
209 let mut email = String::new();
210 let mut has_git_config = false;
211
212 if let Ok(sig) = repo.signature() {
213 let git_name = sig.name().unwrap_or("");
214 let git_email = sig.email().unwrap_or("");
215 if !git_name.is_empty() && !git_email.is_empty() {
216 name = git_name.to_string();
217 email = git_email.to_string();
218 has_git_config = true;
219 }
220 }
221
222 let env_name = std::env::var("RALPH_GIT_USER_NAME").ok();
229 let env_email = std::env::var("RALPH_GIT_USER_EMAIL").ok();
230
231 let final_name = if has_git_config && !name.is_empty() {
233 name.as_str()
234 } else {
235 provided_name
236 .filter(|s| !s.is_empty())
237 .or(env_name.as_deref())
238 .filter(|s| !s.is_empty())
239 .unwrap_or("")
240 };
241
242 let final_email = if has_git_config && !email.is_empty() {
243 email.as_str()
244 } else {
245 provided_email
246 .filter(|s| !s.is_empty())
247 .or(env_email.as_deref())
248 .filter(|s| !s.is_empty())
249 .unwrap_or("")
250 };
251
252 if !final_name.is_empty() && !final_email.is_empty() {
253 let identity = GitIdentity::new(final_name.to_string(), final_email.to_string());
254 if identity.validate().is_ok() {
255 return identity;
256 }
257 }
258
259 let username = fallback_username(executor);
260 let system_email = fallback_email(&username, executor);
261 let identity = GitIdentity::new(
262 if final_name.is_empty() {
263 username
264 } else {
265 final_name.to_string()
266 },
267 if final_email.is_empty() {
268 system_email
269 } else {
270 final_email.to_string()
271 },
272 );
273
274 if identity.validate().is_ok() {
275 return identity;
276 }
277
278 default_identity()
279}
280
281pub fn git_commit(
304 message: &str,
305 git_user_name: Option<&str>,
306 git_user_email: Option<&str>,
307 executor: Option<&dyn crate::executor::ProcessExecutor>,
308) -> io::Result<Option<git2::Oid>> {
309 git_commit_in_repo(
310 Path::new("."),
311 message,
312 git_user_name,
313 git_user_email,
314 executor,
315 )
316}
317
318pub fn git_commit_in_repo(
327 repo_root: &Path,
328 message: &str,
329 git_user_name: Option<&str>,
330 git_user_email: Option<&str>,
331 executor: Option<&dyn crate::executor::ProcessExecutor>,
332) -> io::Result<Option<git2::Oid>> {
333 let repo = git2::Repository::discover(repo_root).map_err(|e| git2_to_io_error(&e))?;
334 git_commit_impl(&repo, message, git_user_name, git_user_email, executor)
335}
336
337fn git_commit_impl(
338 repo: &git2::Repository,
339 message: &str,
340 git_user_name: Option<&str>,
341 git_user_email: Option<&str>,
342 executor: Option<&dyn crate::executor::ProcessExecutor>,
343) -> io::Result<Option<git2::Oid>> {
344 let mut index = repo.index().map_err(|e| git2_to_io_error(&e))?;
345
346 if !index_has_changes_to_commit(repo, &index)? {
349 return Ok(None);
350 }
351
352 let tree_oid = index.write_tree().map_err(|e| git2_to_io_error(&e))?;
353 let tree = repo.find_tree(tree_oid).map_err(|e| git2_to_io_error(&e))?;
354
355 let GitIdentity { name, email } =
356 resolve_commit_identity(repo, git_user_name, git_user_email, executor);
357
358 if std::env::var("RALPH_DEBUG").is_ok() {
360 let identity_source = if git_user_name.is_some() || git_user_email.is_some() {
361 "CLI/config override"
362 } else if std::env::var("RALPH_GIT_USER_NAME").is_ok()
363 || std::env::var("RALPH_GIT_USER_EMAIL").is_ok()
364 {
365 "environment variable"
366 } else if repo.signature().is_ok() {
367 "git config"
368 } else {
369 "system/default"
370 };
371 eprintln!("Git identity: {name} <{email}> (source: {identity_source})");
372 }
373
374 let sig = git2::Signature::now(&name, &email).map_err(|e| git2_to_io_error(&e))?;
375
376 let oid = match repo.head() {
377 Ok(head) => {
378 let head_commit = head.peel_to_commit().map_err(|e| git2_to_io_error(&e))?;
379 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&head_commit])
380 }
381 Err(ref e) if e.code() == git2::ErrorCode::UnbornBranch => {
382 let mut has_entries = false;
383 tree.walk(git2::TreeWalkMode::PreOrder, |_, _| {
384 has_entries = true;
385 1 })
387 .ok();
388
389 if !has_entries {
390 return Ok(None);
391 }
392 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[])
393 }
394 Err(e) => return Err(git2_to_io_error(&e)),
395 }
396 .map_err(|e| git2_to_io_error(&e))?;
397
398 Ok(Some(oid))
399}