use std::path::Path; use gix::ObjectId; use tokio::io::{AsyncBufReadExt, AsyncReadExt}; use crate::GitBaby; use crate::command::error::CmdError; use crate::diff::helper; use crate::diff::types::{ ChangedPath, ChangedPathDiff, ChangedPathRequest, DiffOptions, DiffRequest, NumStat, RangeDiffSpec, ShortStat, }; use crate::error::BabyError; impl GitBaby { pub async fn raw_diff( &self, req: &DiffRequest, hex_len: Option, ) -> Result { let opts = DiffOptions::default(); let mut cmd = self .spawn_diff_cmd(req, hex_len, &opts, helper::build_raw_diff_cmd) .await?; run_capture(&mut cmd, &req.left.to_string(), &req.right.to_string()).await } pub async fn raw_diff_stream( &self, req: &DiffRequest, hex_len: Option, #[allow(unused_mut)] mut callback: F, ) -> Result<(), BabyError> where F: FnMut(&[u8]) + Send, { let opts = DiffOptions::default(); let mut cmd = self .spawn_diff_cmd(req, hex_len, &opts, helper::build_raw_diff_cmd) .await?; run_stream( &mut cmd, &req.left.to_string(), &req.right.to_string(), callback, ) .await } pub async fn patch_diff( &self, req: &DiffRequest, hex_len: Option, ) -> Result { let opts = DiffOptions::default(); let mut cmd = self .spawn_diff_cmd(req, hex_len, &opts, helper::build_patch_diff_cmd) .await?; run_capture(&mut cmd, &req.left.to_string(), &req.right.to_string()).await } pub async fn patch_diff_stream( &self, req: &DiffRequest, hex_len: Option, #[allow(unused_mut)] mut callback: F, ) -> Result<(), BabyError> where F: FnMut(&[u8]) + Send, { let opts = DiffOptions::default(); let mut cmd = self .spawn_diff_cmd(req, hex_len, &opts, helper::build_patch_diff_cmd) .await?; run_stream( &mut cmd, &req.left.to_string(), &req.right.to_string(), callback, ) .await } pub async fn commit_delta( &self, req: &DiffRequest, opts: &DiffOptions, hex_len: Option, ) -> Result, BabyError> { if let Some(paths) = &opts.paths { helper::validate_paths(paths)?; } let hex_len = match hex_len { Some(n) => n, None => self.repo_hex_len().await?, }; let mut cmd = self .spawn_diff_cmd(req, Some(hex_len), opts, helper::build_commit_delta_cmd) .await?; let output = run_output(&mut cmd, &req.left.to_string(), &req.right.to_string()).await?; let abbrev = hex_len as usize; let mut paths: Vec = Vec::new(); for line in String::from_utf8_lossy(&output.stdout).lines() { if line.is_empty() { continue; } paths.push(helper::parse_raw_diff_line(line, abbrev)?); } Ok(paths) } pub async fn commit_diff( &self, req: &DiffRequest, opts: &DiffOptions, hex_len: Option, ) -> Result, BabyError> { if let Some(paths) = &opts.paths { helper::validate_paths(paths)?; } let hex_len = match hex_len { Some(n) => n, None => self.repo_hex_len().await?, }; let mut cmd = self .spawn_diff_cmd(req, Some(hex_len), opts, helper::build_commit_diff_cmd) .await?; let output = run_output(&mut cmd, &req.left.to_string(), &req.right.to_string()).await?; let abbrev = hex_len as usize; helper::parse_commit_diff_output(&output.stdout, abbrev) } pub async fn find_changed_paths( &self, requests: Vec, filter_status: Option, detect_renames: Option, merge_parents: bool, ) -> Result, BabyError> { let mut cmd = self .spawn_diff_state_cmd(&requests, filter_status, detect_renames, merge_parents) .await?; let payload = helper::build_diff_tree_stdin(&requests); let output = match cmd.feed(&payload).await { Ok(()) => match cmd.run().await { Ok(o) => o, Err(CmdError::NonZeroExit { stderr, .. }) => { return Err(helper::classify_stderr( &stderr, &requests.first().map_or_else(String::new, |r| match r { ChangedPathRequest::Commit(o) => o.to_hex().to_string(), ChangedPathRequest::TreePair(l, _) => l.to_hex().to_string(), }), "", )); } Err(e) => return Err(BabyError::from(e)), }, Err(e) => return Err(BabyError::from(e)), }; let abbrev = self.repo_hex_len().await? as usize; helper::parse_diff_tree_z_stream(&output.stdout, abbrev) } pub async fn diff_stats( &self, req: &DiffRequest, opts: &DiffOptions, ) -> Result, BabyError> { if let Some(paths) = &opts.paths { helper::validate_paths(paths)?; } let mut cmd = self .spawn_diff_cmd_no_hex(req, opts, helper::build_numstat_cmd) .await?; let output = run_output(&mut cmd, &req.left.to_string(), &req.right.to_string()).await?; helper::parse_numstat_output(&output.stdout) } pub async fn shortstat( &self, req: &DiffRequest, opts: &DiffOptions, ) -> Result { if let Some(paths) = &opts.paths { helper::validate_paths(paths)?; } let mut cmd = self .spawn_diff_cmd_no_hex(req, opts, helper::build_shortstat_cmd) .await?; let output = run_output(&mut cmd, &req.left.to_string(), &req.right.to_string()).await?; helper::parse_shortstat_output(&output.stdout) } pub async fn range_diff( &self, spec: &RangeDiffSpec, opts: &DiffOptions, hex_len: Option, ) -> Result { let mut cmd = self.spawn_range_diff_cmd(spec, opts, hex_len).await?; let (left, right) = match spec { RangeDiffSpec::Range(r1, r2) | RangeDiffSpec::RevisionRange(r1, r2) => { (r1.clone(), r2.clone()) } RangeDiffSpec::BaseWithRevisions(_, r1, r2) => (r1.clone(), r2.clone()), }; run_capture(&mut cmd, &left, &right).await } async fn spawn_diff_cmd( &self, req: &DiffRequest, hex_len: Option, opts: &DiffOptions, builder: F, ) -> Result where F: FnOnce( &Path, crate::command::env::Env, &DiffRequest, &DiffOptions, Option, ) -> crate::command::cmd::Cmd, { let dir = self .facade .git_repo_dir() .await .map_err(crate::share::facade_error)?; let alternates = self .facade .git_alternate_object_directories() .await .map_err(crate::share::facade_error)?; let resolved = match hex_len { Some(n) => n, None => self.repo_hex_len().await?, }; let env = crate::share::build_env_safe(&alternates); Ok(builder(&dir, env, req, opts, Some(resolved))) } async fn spawn_diff_cmd_no_hex( &self, req: &DiffRequest, opts: &DiffOptions, builder: F, ) -> Result where F: FnOnce( &Path, crate::command::env::Env, &DiffRequest, &DiffOptions, ) -> crate::command::cmd::Cmd, { let dir = self .facade .git_repo_dir() .await .map_err(crate::share::facade_error)?; let alternates = self .facade .git_alternate_object_directories() .await .map_err(crate::share::facade_error)?; let env = crate::share::build_env_safe(&alternates); Ok(builder(&dir, env, req, opts)) } async fn spawn_diff_state_cmd( &self, requests: &[ChangedPathRequest], filter_status: Option, detect_renames: Option, merge_parents: bool, ) -> Result { let dir = self .facade .git_repo_dir() .await .map_err(crate::share::facade_error)?; let alternates = self .facade .git_alternate_object_directories() .await .map_err(crate::share::facade_error)?; let env = crate::share::build_env_safe(&alternates); Ok(helper::build_find_changed_paths_cmd( &dir, env, requests, filter_status, detect_renames, merge_parents, )) } async fn spawn_range_diff_cmd( &self, spec: &RangeDiffSpec, opts: &DiffOptions, hex_len: Option, ) -> Result { let dir = self .facade .git_repo_dir() .await .map_err(crate::share::facade_error)?; let alternates = self .facade .git_alternate_object_directories() .await .map_err(crate::share::facade_error)?; let resolved = match hex_len { Some(n) => n, None => self.repo_hex_len().await?, }; let env = crate::share::build_env_safe(&alternates); Ok(helper::build_range_diff_cmd( &dir, env, spec, opts, Some(resolved), )) } async fn repo_hex_len(&self) -> Result { let repo = self .facade .gix_repo() .await .map_err(crate::share::facade_error)?; Ok(repo.object_hash().len_in_hex() as u8) } } async fn run_capture( cmd: &mut crate::command::cmd::Cmd, left: &str, right: &str, ) -> Result { let output = match cmd.run().await { Ok(o) => o, Err(CmdError::NonZeroExit { ref stderr, .. }) => { return Err(helper::classify_stderr(stderr, left, right)); } Err(e) => return Err(helper::classify_cmd_error(e, left, right)), }; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), left, right, )); } Ok(String::from_utf8_lossy(&output.stdout).into_owned()) } async fn run_output( cmd: &mut crate::command::cmd::Cmd, left: &str, right: &str, ) -> Result { let output = match cmd.run().await { Ok(o) => o, Err(CmdError::NonZeroExit { ref stderr, .. }) => { return Err(helper::classify_stderr(stderr, left, right)); } Err(e) => return Err(helper::classify_cmd_error(e, left, right)), }; if !output.status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&output.stderr), left, right, )); } Ok(output) } async fn run_stream( cmd: &mut crate::command::cmd::Cmd, left: &str, right: &str, mut callback: F, ) -> Result<(), BabyError> where F: FnMut(&[u8]) + Send, { if let Err(e) = cmd.spawn().await { return Err(helper::classify_cmd_error(e, left, right)); } let stdout = std::mem::replace(&mut cmd.stdout, Box::new(tokio::io::empty())); let stderr = std::mem::replace(&mut cmd.stderr, Box::new(tokio::io::empty())); // Drain stderr concurrently so a chatty child cannot fill the pipe buffer // and deadlock while we consume stdout. let stderr_task = tokio::spawn(async move { let mut stderr = stderr; let mut sbuf = Vec::new(); let _ = stderr.read_to_end(&mut sbuf).await; sbuf }); let mut reader = tokio::io::BufReader::new(stdout); let mut buf = Vec::with_capacity(8192); loop { buf.clear(); let n = match reader.read_until(b'\n', &mut buf).await { Ok(n) => n, Err(source) => { return Err(BabyError::Custom(format!("stream read failed: {}", source))); } }; if n == 0 { break; } callback(&buf); } let status = match cmd.wait().await { Ok(s) => s, Err(e) => return Err(helper::classify_cmd_error(e, left, right)), }; let sbuf = match stderr_task.await { Ok(v) => v, Err(e) => { return Err(BabyError::Custom(format!( "stderr drain task failed: {}", e ))); } }; if !status.success() { return Err(helper::classify_stderr( &String::from_utf8_lossy(&sbuf), left, right, )); } Ok(()) } #[allow(dead_code)] fn _oid_marker(_o: ObjectId) {}