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_all() -> io::Result<bool> {
39 git_add_all_in_repo(Path::new("."))
40}
41
42pub fn git_add_all_in_repo(repo_root: &Path) -> io::Result<bool> {
47 let repo = git2::Repository::discover(repo_root).map_err(|e| git2_to_io_error(&e))?;
48 git_add_all_impl(&repo)
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum CommitResultFallback {
56 Success(git2::Oid),
58 NoChanges,
60 Failed(String),
62}
63
64fn git_add_all_impl(repo: &git2::Repository) -> io::Result<bool> {
66 let mut index = repo.index().map_err(|e| git2_to_io_error(&e))?;
67
68 let mut status_opts = git2::StatusOptions::new();
71 status_opts
72 .include_untracked(true)
73 .recurse_untracked_dirs(true)
74 .include_ignored(false);
75 let statuses = repo
76 .statuses(Some(&mut status_opts))
77 .map_err(|e| git2_to_io_error(&e))?;
78 for entry in statuses.iter() {
79 if entry.status().contains(git2::Status::WT_DELETED) {
80 if let Some(path) = entry.path() {
81 index
82 .remove_path(std::path::Path::new(path))
83 .map_err(|e| git2_to_io_error(&e))?;
84 }
85 }
86 }
87
88 let mut filter_cb = |path: &std::path::Path, _matched: &[u8]| -> i32 {
91 i32::from(is_internal_agent_artifact(path))
94 };
95 index
96 .add_all(
97 vec!["."],
98 git2::IndexAddOption::DEFAULT,
99 Some(&mut filter_cb),
100 )
101 .map_err(|e| git2_to_io_error(&e))?;
102
103 index.write().map_err(|e| git2_to_io_error(&e))?;
104
105 index_has_changes_to_commit(repo, &index)
107}
108
109fn resolve_commit_identity(
110 repo: &git2::Repository,
111 provided_name: Option<&str>,
112 provided_email: Option<&str>,
113 executor: Option<&dyn crate::executor::ProcessExecutor>,
114) -> GitIdentity {
115 use crate::git_helpers::identity::{default_identity, fallback_email, fallback_username};
116
117 let mut name = String::new();
119 let mut email = String::new();
120 let mut has_git_config = false;
121
122 if let Ok(sig) = repo.signature() {
123 let git_name = sig.name().unwrap_or("");
124 let git_email = sig.email().unwrap_or("");
125 if !git_name.is_empty() && !git_email.is_empty() {
126 name = git_name.to_string();
127 email = git_email.to_string();
128 has_git_config = true;
129 }
130 }
131
132 let env_name = std::env::var("RALPH_GIT_USER_NAME").ok();
139 let env_email = std::env::var("RALPH_GIT_USER_EMAIL").ok();
140
141 let final_name = if has_git_config && !name.is_empty() {
143 name.as_str()
144 } else {
145 provided_name
146 .filter(|s| !s.is_empty())
147 .or(env_name.as_deref())
148 .filter(|s| !s.is_empty())
149 .unwrap_or("")
150 };
151
152 let final_email = if has_git_config && !email.is_empty() {
153 email.as_str()
154 } else {
155 provided_email
156 .filter(|s| !s.is_empty())
157 .or(env_email.as_deref())
158 .filter(|s| !s.is_empty())
159 .unwrap_or("")
160 };
161
162 if !final_name.is_empty() && !final_email.is_empty() {
163 let identity = GitIdentity::new(final_name.to_string(), final_email.to_string());
164 if identity.validate().is_ok() {
165 return identity;
166 }
167 }
168
169 let username = fallback_username(executor);
170 let system_email = fallback_email(&username, executor);
171 let identity = GitIdentity::new(
172 if final_name.is_empty() {
173 username
174 } else {
175 final_name.to_string()
176 },
177 if final_email.is_empty() {
178 system_email
179 } else {
180 final_email.to_string()
181 },
182 );
183
184 if identity.validate().is_ok() {
185 return identity;
186 }
187
188 default_identity()
189}
190
191pub fn git_commit(
210 message: &str,
211 git_user_name: Option<&str>,
212 git_user_email: Option<&str>,
213 executor: Option<&dyn crate::executor::ProcessExecutor>,
214) -> io::Result<Option<git2::Oid>> {
215 git_commit_in_repo(
216 Path::new("."),
217 message,
218 git_user_name,
219 git_user_email,
220 executor,
221 )
222}
223
224pub fn git_commit_in_repo(
229 repo_root: &Path,
230 message: &str,
231 git_user_name: Option<&str>,
232 git_user_email: Option<&str>,
233 executor: Option<&dyn crate::executor::ProcessExecutor>,
234) -> io::Result<Option<git2::Oid>> {
235 let repo = git2::Repository::discover(repo_root).map_err(|e| git2_to_io_error(&e))?;
236 git_commit_impl(&repo, message, git_user_name, git_user_email, executor)
237}
238
239fn git_commit_impl(
240 repo: &git2::Repository,
241 message: &str,
242 git_user_name: Option<&str>,
243 git_user_email: Option<&str>,
244 executor: Option<&dyn crate::executor::ProcessExecutor>,
245) -> io::Result<Option<git2::Oid>> {
246 let mut index = repo.index().map_err(|e| git2_to_io_error(&e))?;
247
248 if !index_has_changes_to_commit(repo, &index)? {
251 return Ok(None);
252 }
253
254 let tree_oid = index.write_tree().map_err(|e| git2_to_io_error(&e))?;
255 let tree = repo.find_tree(tree_oid).map_err(|e| git2_to_io_error(&e))?;
256
257 let GitIdentity { name, email } =
258 resolve_commit_identity(repo, git_user_name, git_user_email, executor);
259
260 if std::env::var("RALPH_DEBUG").is_ok() {
262 let identity_source = if git_user_name.is_some() || git_user_email.is_some() {
263 "CLI/config override"
264 } else if std::env::var("RALPH_GIT_USER_NAME").is_ok()
265 || std::env::var("RALPH_GIT_USER_EMAIL").is_ok()
266 {
267 "environment variable"
268 } else if repo.signature().is_ok() {
269 "git config"
270 } else {
271 "system/default"
272 };
273 eprintln!("Git identity: {name} <{email}> (source: {identity_source})");
274 }
275
276 let sig = git2::Signature::now(&name, &email).map_err(|e| git2_to_io_error(&e))?;
277
278 let oid = match repo.head() {
279 Ok(head) => {
280 let head_commit = head.peel_to_commit().map_err(|e| git2_to_io_error(&e))?;
281 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&head_commit])
282 }
283 Err(ref e) if e.code() == git2::ErrorCode::UnbornBranch => {
284 let mut has_entries = false;
285 tree.walk(git2::TreeWalkMode::PreOrder, |_, _| {
286 has_entries = true;
287 1 })
289 .ok();
290
291 if !has_entries {
292 return Ok(None);
293 }
294 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[])
295 }
296 Err(e) => return Err(git2_to_io_error(&e)),
297 }
298 .map_err(|e| git2_to_io_error(&e))?;
299
300 Ok(Some(oid))
301}