use serde::{Deserialize, Serialize}; use crate::{ bare::GitBare, cmd::{command::GitCommandParams, oid::ObjectId}, errors::{GitError, GitResult}, }; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileChange { pub path: String, pub content: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateCommitParams { pub branch: String, pub message: String, pub author_name: String, pub author_email: String, pub committer_name: String, pub committer_email: String, pub files: Vec, } #[derive(Debug, Clone)] struct TreeEntry { mode: String, // e.g. "100644" kind: String, // "blob" or "tree" oid: ObjectId, path: String, } impl GitBare { /// Create a commit with the given file changes on the specified branch. /// /// Returns the OID of the newly created commit. pub fn commit_create( &self, params: CreateCommitParams, ) -> GitResult { let repo = self.gix_repo()?; // 1. Write each file as a blob and collect entries let mut new_entries: Vec = Vec::with_capacity(params.files.len()); for fc in ¶ms.files { let blob_upload_result = self.blob_upload(crate::cmd::blob::BlobUploadParams { blob: fc.content.clone(), path: fc.path.clone(), })?; new_entries.push(TreeEntry { mode: "100644".to_string(), kind: "blob".to_string(), oid: blob_upload_result.id, path: fc.path.clone(), }); } // 2. Get the parent commit (HEAD of the branch, or initial commit) let parent_oid: Option; let existing_tree_entries: Vec; let branch_ref = format!("refs/heads/{}", params.branch); match repo.find_reference(&branch_ref) { Ok(r) => { let oid = r .into_fully_peeled_id() .map_err(|e| GitError::Gix(e.to_string()))? .detach(); let oid_str = oid.to_hex().to_string(); let commit_oid = ObjectId::new(oid_str); let commit = self.commit_info(commit_oid.clone())?; let tree_entries = self._ls_tree(commit.tree_id)?; parent_oid = Some(commit_oid); existing_tree_entries = tree_entries; } Err(_) => { parent_oid = None; existing_tree_entries = Vec::new(); } } // 3. Merge new entries into existing tree (replace by path) let mut merged: Vec = Vec::new(); let mut seen_paths: std::collections::HashSet = std::collections::HashSet::new(); for entry in new_entries { seen_paths.insert(entry.path.clone()); merged.push(entry); } for entry in existing_tree_entries { if !seen_paths.contains(&entry.path) { merged.push(entry); } } // 4. Create tree with git mktree let tree_oid = self._mktree(&merged)?; // 5. Create commit with git commit-tree let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64; let timestamp = crate::cmd::parse::format_git_timestamp(now, 0); let mut commit_tree_args = vec!["commit-tree".to_string(), tree_oid.as_str().to_string()]; if let Some(parent) = &parent_oid { commit_tree_args.push("-p".to_string()); commit_tree_args.push(parent.as_str().to_string()); } commit_tree_args.push("-F".to_string()); commit_tree_args.push("-".to_string()); let commit_output = self.git_command_with( GitCommandParams::new(commit_tree_args) .with_stdin(params.message.as_bytes().to_vec()) .with_env( "GIT_AUTHOR_NAME".to_string(), params.author_name.clone(), ) .with_env( "GIT_AUTHOR_EMAIL".to_string(), params.author_email.clone(), ) .with_env("GIT_AUTHOR_DATE".to_string(), timestamp.clone()) .with_env( "GIT_COMMITTER_NAME".to_string(), params.committer_name.clone(), ) .with_env( "GIT_COMMITTER_EMAIL".to_string(), params.committer_email.clone(), ) .with_env("GIT_COMMITTER_DATE".to_string(), timestamp), )?; if !commit_output.success { return Err(GitError::CommandFailed { status_code: commit_output.status_code, stderr: commit_output.stderr_lossy(), }); } let stdout_str = commit_output.stdout_lossy(); let commit_oid_str = stdout_str.trim(); if commit_oid_str.is_empty() { return Err(GitError::CommandFailed { status_code: commit_output.status_code, stderr: "no commit OID produced".to_string(), }); } let commit_oid = ObjectId::new(commit_oid_str); // 6. Update the branch ref let update_ref_args = vec![ "update-ref".to_string(), branch_ref, commit_oid.as_str().to_string(), ]; let _update_output = self.git_command_with( GitCommandParams::new(update_ref_args).trusted(), )?; Ok(commit_oid) } /// List tree entries (mode, type, oid, path) using git ls-tree. fn _ls_tree(&self, tree_oid: ObjectId) -> GitResult> { let output = self.git_command_with(GitCommandParams::new(vec![ "ls-tree".to_string(), tree_oid.as_str().to_string(), ]))?; if !output.success { return Err(GitError::CommandFailed { status_code: output.status_code, stderr: output.stderr_lossy(), }); } let stdout = output.stdout_lossy(); let mut entries = Vec::new(); for line in stdout.lines() { let line = line.trim(); if line.is_empty() { continue; } // Output format: \t let parts: Vec<&str> = line.splitn(2, '\t').collect(); if parts.len() < 2 { continue; } let meta: Vec<&str> = parts[0].split_whitespace().collect(); if meta.len() < 3 { continue; } entries.push(TreeEntry { mode: meta[0].to_string(), kind: meta[1].to_string(), oid: ObjectId::new(meta[2]), path: parts[1].to_string(), }); } Ok(entries) } /// Create a tree object from entries using git mktree. fn _mktree(&self, entries: &[TreeEntry]) -> GitResult { // Build input for git mktree: // Each line: \t let mut input = String::new(); for entry in entries { input.push_str(&format!( "{} {} {}\t{}\n", entry.mode, entry.kind, entry.oid.as_str(), entry.path )); } let output = self.git_command_with( GitCommandParams::new(vec!["mktree".to_string()]) .with_stdin(input.into_bytes()), )?; if !output.success { return Err(GitError::CommandFailed { status_code: output.status_code, stderr: output.stderr_lossy(), }); } let stdout = output.stdout_lossy(); let oid_str = stdout.trim(); Ok(ObjectId::new(oid_str)) } }