use std::path::Path; use crate::GitBaby; use crate::commit::helper; use crate::commit::types::{ CommitInfo, CommitShortStat, CommitStats, ListCommitsOptions, TreeEntry, }; use crate::error::BabyError; impl GitBaby { pub async fn get_commit(&self, revision: &str) -> Result { helper::validate_revision(revision)?; let id = self .rev_parse(revision) .await .map_err(helper::facade_error)?; let pool = crate::cache::try_global_cache(); let key = crate::cache::object_key(crate::cache::NS_COMMIT, &id); if let Some(p) = &pool && let Ok(Some(v)) = p.store().get(&key).await && let Ok(info) = serde_json::from_slice(&v) { return Ok(info); } let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { helper::build_get_commit_cmd(dir, env, revision) }) .await?; let ctx = helper::StderrCtx { revision, path: None, }; let output = cmd .run() .await .map_err(|e| helper::classify_cmd_error(e, &ctx))?; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), &ctx, )); } let info = helper::parse_commit_porcelain(id, &output.stdout)?; if let Some(p) = &pool && let Ok(v) = serde_json::to_vec(&info) { p.store().insert(key, v); } Ok(info) } pub async fn rev_parse(&self, revision: &str) -> Result { helper::validate_revision(revision)?; let pool = crate::cache::try_global_cache(); let key = if pool.is_some() { let dir = self .facade .git_repo_dir() .await .map_err(helper::facade_error)?; let dir = std::fs::canonicalize(&dir) .map_err(|source| BabyError::Io { path: dir, source })?; Some(crate::cache::rev_key(&dir, revision)) } else { None }; if let (Some(p), Some(key)) = (&pool, &key) && let Ok(Some(v)) = p.store().get(key).await && let Ok(hex) = String::from_utf8(v) && let Ok(oid) = gix::ObjectId::from_hex(hex.trim().as_bytes()) { return Ok(oid); } let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { helper::build_rev_parse_cmd(dir, env, revision) }) .await?; let ctx = helper::StderrCtx { revision, path: None, }; let output = cmd .run() .await .map_err(|e| helper::classify_cmd_error(e, &ctx))?; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), &ctx, )); } let text = String::from_utf8_lossy(&output.stdout); let hex = text.trim(); let oid = gix::ObjectId::from_hex(hex.as_bytes()).map_err(|source| { BabyError::Custom(format!( "rev-parse returned invalid oid `{}`: {}", hex, source )) })?; if let (Some(p), Some(key)) = (&pool, &key) { p.store().insert(key.clone(), hex.as_bytes().to_vec()); } Ok(oid) } pub async fn is_ancestor(&self, ancestor: &str, descendant: &str) -> Result { helper::validate_revision(ancestor)?; helper::validate_revision(descendant)?; let mut cmd = self .spawn_commit_cmd(descendant, |dir, env| { helper::build_is_ancestor_cmd(dir, env, ancestor, descendant) }) .await?; let ctx = helper::StderrCtx { revision: descendant, path: None, }; let output = match cmd.run().await { Ok(o) => o, Err(CmdError::NonZeroExit { stderr, .. }) => { if stderr.trim().is_empty() { return Ok(false); } return Err(helper::classify_stderr(&stderr, &ctx)); } Err(e) => return Err(helper::classify_cmd_error(e, &ctx)), }; if output.status.success() { Ok(true) } else { Ok(false) } } pub async fn list_commits( &self, opts: ListCommitsOptions, ) -> Result, BabyError> { helper::validate_list_commits_options(&opts)?; let rev = opts.revision.clone(); let mut cmd = self .spawn_commit_cmd(&rev, |dir, env| { helper::build_list_commits_cmd(dir, env, &opts) }) .await?; let ctx = helper::StderrCtx { revision: &rev, path: opts.path.as_deref(), }; let output = cmd .run() .await .map_err(|e| helper::classify_cmd_error(e, &ctx))?; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), &ctx, )); } helper::parse_log_porcelain(&output.stdout) } pub async fn count_commits(&self, opts: ListCommitsOptions) -> Result { helper::validate_list_commits_options(&opts)?; let rev = opts.revision.clone(); let mut cmd = self .spawn_commit_cmd(&rev, |dir, env| { helper::build_count_commits_cmd(dir, env, &opts) }) .await?; let ctx = helper::StderrCtx { revision: &rev, path: opts.path.as_deref(), }; let output = cmd .run() .await .map_err(|e| helper::classify_cmd_error(e, &ctx))?; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), &ctx, )); } let s = String::from_utf8_lossy(&output.stdout); s.trim().parse::().map_err(|_| BabyError::CommitParse { line_number: 0, message: format!("rev-list --count returned `{}`", s.trim()), }) } pub async fn commit_stats( &self, revision: &str, path: Option<&Path>, ) -> Result { helper::validate_revision(revision)?; if let Some(p) = path { helper::validate_path_local_no_escape(p)?; } let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { helper::build_commit_stats_cmd(dir, env, revision, path) }) .await?; let ctx = helper::StderrCtx { revision, path }; let output = cmd .run() .await .map_err(|e| helper::classify_cmd_error(e, &ctx))?; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), &ctx, )); } helper::parse_shortstat(&output.stdout) } pub async fn commit_short_stats( &self, revision: &str, ) -> Result, BabyError> { helper::validate_revision(revision)?; let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { helper::build_commit_short_stats_cmd(dir, env, revision) }) .await?; let ctx = helper::StderrCtx { revision, path: None, }; let output = cmd .run() .await .map_err(|e| helper::classify_cmd_error(e, &ctx))?; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), &ctx, )); } helper::parse_commit_short_stats(&output.stdout) } pub async fn get_tree_entries( &self, revision: &str, path: &Path, ) -> Result, BabyError> { helper::validate_revision(revision)?; helper::validate_path_local_no_escape(path)?; let pool = crate::cache::try_global_cache(); let key = if pool.is_some() { match self.rev_parse(revision).await.map_err(helper::facade_error) { Ok(oid) => { let mut k = Vec::with_capacity(oid.as_bytes().len() + 1 + path.as_os_str().len()); k.extend_from_slice(oid.as_bytes()); k.push(0); k.extend_from_slice(path.to_string_lossy().as_bytes()); Some(crate::cache::encode(crate::cache::NS_TREE_ENTRIES, &k)) } Err(_) => None, } } else { None }; if let (Some(p), Some(key)) = (&pool, &key) && let Ok(Some(v)) = p.store().get(key).await && let Ok(entries) = serde_json::from_slice::>(&v) { return Ok(entries); } let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { helper::build_tree_entries_cmd(dir, env, revision, path) }) .await?; let ctx = helper::StderrCtx { revision, path: Some(path), }; let output = cmd .run() .await .map_err(|e| helper::classify_cmd_error(e, &ctx))?; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), &ctx, )); } let entries = helper::parse_tree_entries(&output.stdout)?; if let (Some(p), Some(key)) = (&pool, &key) && let Ok(v) = serde_json::to_vec(&entries) { p.store().insert(key.clone(), v); } Ok(entries) } pub async fn tree_entry_at( &self, revision: &str, path: &Path, ) -> Result, BabyError> { helper::validate_revision(revision)?; helper::validate_path_local_no_escape(path)?; let raw = path.to_string_lossy(); let clean = raw.trim().trim_start_matches("./"); if clean.is_empty() || clean == "." { return Ok(None); } let (parent, base) = match clean.rsplit_once('/') { Some((p, b)) => (p, b), None => ("", clean), }; if base.is_empty() { return Ok(None); } let treeish = if parent.is_empty() { format!("{}:", revision) } else { format!("{}:{}", revision, parent) }; let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { let args: Vec = ["-c", "core.quotepath=off", "ls-tree", treeish.as_str()] .into_iter() .map(String::from) .collect(); crate::share::git_cmd(helper::CALLER, dir, env, None, args) }) .await?; let ctx = helper::StderrCtx { revision, path: Some(path), }; let output = cmd .run() .await .map_err(|e| helper::classify_cmd_error(e, &ctx))?; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), &ctx, )); } let entries = helper::parse_tree_entries(&output.stdout)?; Ok(entries.into_iter().find(|e| e.name == base)) } pub async fn last_commit_for_path( &self, revision: &str, path: &Path, ) -> Result { helper::validate_revision(revision)?; helper::validate_path_local_no_escape(path)?; let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { helper::build_last_commit_for_path_cmd(dir, env, revision, path) }) .await?; let ctx = helper::StderrCtx { revision, path: Some(path), }; let output = cmd .run() .await .map_err(|e| helper::classify_cmd_error(e, &ctx))?; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), &ctx, )); } let mut commits = helper::parse_log_porcelain(&output.stdout)?; commits .pop() .ok_or_else(|| BabyError::RevisionNotFound(revision.to_string())) } async fn spawn_commit_cmd( &self, rev: &str, builder: F, ) -> Result where F: FnOnce(&Path, crate::command::env::Env) -> crate::command::cmd::Cmd, { let _ = rev; let dir = self .facade .git_repo_dir() .await .map_err(helper::facade_error)?; let alternates = self .facade .git_alternate_object_directories() .await .map_err(helper::facade_error)?; let env = helper::build_env(&alternates); Ok(builder(&dir, env)) } } use crate::command::error::CmdError;