diff --git a/Cargo.lock b/Cargo.lock index 79f75cae..89562ce4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,12 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "bytesize" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a18687293a1546b67c246452202bbbf143d239cb43494cc163da14979082da" + [[package]] name = "cassowary" version = "0.3.0" @@ -323,6 +329,7 @@ dependencies = [ "asyncgit", "backtrace", "bitflags", + "bytesize", "chrono", "clap", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index b9604d63..3098e4e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ asyncgit = { path = "./asyncgit", version = "0.7" } crossterm = "0.17" clap = { version = "2.33", default-features = false } tui = { version = "0.9", default-features = false, features = ['crossterm'] } +bytesize = { version = "1.0.1", default-features = false} itertools = "0.9" rayon-core = "1.7" log = "0.4" diff --git a/asyncgit/src/sync/diff.rs b/asyncgit/src/sync/diff.rs index c745b2c9..a4b5aa97 100644 --- a/asyncgit/src/sync/diff.rs +++ b/asyncgit/src/sync/diff.rs @@ -7,7 +7,7 @@ use git2::{ Repository, }; use scopetime::scope_time; -use std::{fs, path::Path}; +use std::{cell::RefCell, fs, path::Path, rc::Rc}; use utils::{get_head_repo, work_dir}; /// type of diff of a single line @@ -75,6 +75,10 @@ pub struct FileDiff { pub lines: usize, /// pub untracked: bool, + /// old and new file size in bytes + pub sizes: (u64, u64), + /// size delta in bytes + pub size_delta: i64, } pub(crate) fn get_diff_raw<'a>( @@ -152,110 +156,128 @@ fn raw_diff_to_file_diff<'a>( diff: &'a Diff, work_dir: &Path, ) -> Result { - let mut res: FileDiff = FileDiff::default(); - let mut current_lines = Vec::new(); - let mut current_hunk: Option = None; + let res = Rc::new(RefCell::new(FileDiff::default())); + { + let mut current_lines = Vec::new(); + let mut current_hunk: Option = None; - let mut adder = |header: &HunkHeader, lines: &Vec| { - res.hunks.push(Hunk { - header_hash: hash(header), - lines: lines.clone(), - }); - res.lines += lines.len(); - }; + let res_cell = Rc::clone(&res); + let adder = move |header: &HunkHeader, + lines: &Vec| { + let mut res = res_cell.borrow_mut(); + res.hunks.push(Hunk { + header_hash: hash(header), + lines: lines.clone(), + }); + res.lines += lines.len(); + }; - let mut put = |hunk: Option, line: git2::DiffLine| { - if let Some(hunk) = hunk { - let hunk_header = HunkHeader::from(hunk); - - match current_hunk { - None => current_hunk = Some(hunk_header), - Some(h) if h != hunk_header => { - adder(&h, ¤t_lines); - current_lines.clear(); - current_hunk = Some(hunk_header) - } - _ => (), - } - - let line_type = match line.origin() { - 'H' => DiffLineType::Header, - '<' | '-' => DiffLineType::Delete, - '>' | '+' => DiffLineType::Add, - _ => DiffLineType::None, - }; - - let diff_line = DiffLine { - content: String::from_utf8_lossy(line.content()) - .to_string(), - line_type, - }; - - current_lines.push(diff_line); - } - }; - - let new_file_diff = if diff.deltas().len() == 1 { - // it's safe to unwrap here because we check first that diff.deltas has a single element. - let delta: DiffDelta = diff.deltas().next().unwrap(); - - if delta.status() == Delta::Untracked { - let relative_path = - delta.new_file().path().ok_or_else(|| { - Error::Generic( - "new file path is unspecified.".to_string(), - ) - })?; - - let newfile_path = work_dir.join(relative_path); - - if let Some(newfile_content) = - new_file_content(&newfile_path) + let res_cell = Rc::clone(&res); + let mut put = |delta: DiffDelta, + hunk: Option, + line: git2::DiffLine| { { - let mut patch = Patch::from_buffers( - &[], - None, - newfile_content.as_bytes(), - Some(&newfile_path), - None, - )?; + let mut res = res_cell.borrow_mut(); + res.sizes = ( + delta.old_file().size(), + delta.new_file().size(), + ); + res.size_delta = (res.sizes.1 as i64) + .saturating_sub(res.sizes.0 as i64); + } + if let Some(hunk) = hunk { + let hunk_header = HunkHeader::from(hunk); - patch - .print(&mut |_delta, hunk:Option, line: git2::DiffLine| { - put(hunk,line); + match current_hunk { + None => current_hunk = Some(hunk_header), + Some(h) if h != hunk_header => { + adder(&h, ¤t_lines); + current_lines.clear(); + current_hunk = Some(hunk_header) + } + _ => (), + } + + let line_type = match line.origin() { + 'H' => DiffLineType::Header, + '<' | '-' => DiffLineType::Delete, + '>' | '+' => DiffLineType::Add, + _ => DiffLineType::None, + }; + + let diff_line = DiffLine { + content: String::from_utf8_lossy(line.content()) + .to_string(), + line_type, + }; + + current_lines.push(diff_line); + } + }; + + let new_file_diff = if diff.deltas().len() == 1 { + // it's safe to unwrap here because we check first that diff.deltas has a single element. + let delta: DiffDelta = diff.deltas().next().unwrap(); + + if delta.status() == Delta::Untracked { + let relative_path = + delta.new_file().path().ok_or_else(|| { + Error::Generic( + "new file path is unspecified." + .to_string(), + ) + })?; + + let newfile_path = work_dir.join(relative_path); + + if let Some(newfile_content) = + new_file_content(&newfile_path) + { + let mut patch = Patch::from_buffers( + &[], + None, + newfile_content.as_bytes(), + Some(&newfile_path), + None, + )?; + + patch + .print(&mut |delta, hunk:Option, line: git2::DiffLine| { + put(delta,hunk,line); true })?; - true + true + } else { + false + } } else { false } } else { false + }; + + if !new_file_diff { + diff.print( + DiffFormat::Patch, + move |delta, hunk, line: git2::DiffLine| { + put(delta, hunk, line); + true + }, + )?; } - } else { - false - }; - if !new_file_diff { - diff.print( - DiffFormat::Patch, - |_, hunk, line: git2::DiffLine| { - put(hunk, line); - true - }, - )?; + if !current_lines.is_empty() { + adder(¤t_hunk.unwrap(), ¤t_lines); + } + + if new_file_diff { + res.borrow_mut().untracked = true; + } } - - if !current_lines.is_empty() { - adder(¤t_hunk.unwrap(), ¤t_lines); - } - - if new_file_diff { - res.untracked = true; - } - - Ok(res) + let res = Rc::try_unwrap(res).expect("rc error"); + Ok(res.into_inner()) } fn new_file_content(path: &Path) -> Option { @@ -279,7 +301,7 @@ mod tests { use super::get_diff; use crate::error::Result; use crate::sync::{ - stage_add_file, + commit, stage_add_file, status::{get_status, StatusType}, tests::{get_statuses, repo_init, repo_init_empty}, }; @@ -455,4 +477,34 @@ mod tests { Ok(()) } + + #[test] + fn test_diff_delta_size() -> Result<()> { + let file_path = Path::new("bar"); + let (_td, repo) = repo_init_empty().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + File::create(&root.join(file_path))?.write_all(b"\x00")?; + + stage_add_file(repo_path, file_path).unwrap(); + + commit(repo_path, "commit").unwrap(); + + File::create(&root.join(file_path))? + .write_all(b"\x00\x02")?; + + let diff = get_diff( + repo_path, + String::from(file_path.to_str().unwrap()), + false, + ) + .unwrap(); + + dbg!(&diff); + assert_eq!(diff.sizes, (1, 2)); + assert_eq!(diff.size_delta, 1); + + Ok(()) + } } diff --git a/src/components/diff.rs b/src/components/diff.rs index fdef3443..b882f1a9 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -7,6 +7,7 @@ use crate::{ ui::{calc_scroll_top, style::SharedTheme}, }; use asyncgit::{hash, sync, DiffLine, DiffLineType, FileDiff, CWD}; +use bytesize::ByteSize; use crossterm::event::Event; use std::{borrow::Cow, cmp, path::Path}; use strings::commands; @@ -29,7 +30,7 @@ struct Current { /// pub struct DiffComponent { - diff: FileDiff, + diff: Option, selection: usize, selected_hunk: Option, current_size: (u16, u16), @@ -48,7 +49,7 @@ impl DiffComponent { queue, current: Current::default(), selected_hunk: None, - diff: FileDiff::default(), + diff: None, current_size: (0, 0), selection: 0, scroll_top: 0, @@ -56,8 +57,11 @@ impl DiffComponent { } } /// - const fn can_scroll(&self) -> bool { - self.diff.lines > 1 + fn can_scroll(&self) -> bool { + self.diff + .as_ref() + .map(|diff| diff.lines > 1) + .unwrap_or_default() } /// pub fn current(&self) -> (String, bool) { @@ -66,7 +70,7 @@ impl DiffComponent { /// pub fn clear(&mut self) -> Result<()> { self.current = Current::default(); - self.diff = FileDiff::default(); + self.diff = None; self.scroll_top = 0; self.selection = 0; self.selected_hunk = None; @@ -88,12 +92,13 @@ impl DiffComponent { is_stage, hash, }; - self.diff = diff; - self.scroll_top = 0; - self.selection = 0; self.selected_hunk = - Self::find_selected_hunk(&self.diff, self.selection)?; + Self::find_selected_hunk(&diff, self.selection)?; + + self.diff = Some(diff); + self.scroll_top = 0; + self.selection = 0; } Ok(()) @@ -103,30 +108,34 @@ impl DiffComponent { &mut self, move_type: ScrollType, ) -> Result<()> { - let old = self.selection; + if let Some(diff) = &self.diff { + let old = self.selection; - let max = self.diff.lines.saturating_sub(1) as usize; + let max = diff.lines.saturating_sub(1) as usize; - self.selection = match move_type { - ScrollType::Down => old.saturating_add(1), - ScrollType::Up => old.saturating_sub(1), - ScrollType::Home => 0, - ScrollType::End => max, - ScrollType::PageDown => self.selection.saturating_add( - self.current_size.1.saturating_sub(1) as usize, - ), - ScrollType::PageUp => self.selection.saturating_sub( - self.current_size.1.saturating_sub(1) as usize, - ), - }; + self.selection = match move_type { + ScrollType::Down => old.saturating_add(1), + ScrollType::Up => old.saturating_sub(1), + ScrollType::Home => 0, + ScrollType::End => max, + ScrollType::PageDown => { + self.selection.saturating_add( + self.current_size.1.saturating_sub(1) + as usize, + ) + } + ScrollType::PageUp => self.selection.saturating_sub( + self.current_size.1.saturating_sub(1) as usize, + ), + }; - self.selection = cmp::min(max, self.selection); + self.selection = cmp::min(max, self.selection); - if old != self.selection { - self.selected_hunk = - Self::find_selected_hunk(&self.diff, self.selection)?; + if old != self.selection { + self.selected_hunk = + Self::find_selected_hunk(&diff, self.selection)?; + } } - Ok(()) } @@ -154,49 +163,96 @@ impl DiffComponent { } fn get_text(&self, width: u16, height: u16) -> Result> { - let selection = self.selection; - - let min = self.scroll_top; - let max = min + height as usize; - let mut res = Vec::new(); - let mut line_cursor = 0_usize; - let mut lines_added = 0_usize; + if let Some(diff) = &self.diff { + if diff.hunks.is_empty() { + let is_positive = diff.size_delta >= 0; + let delta_byte_size = + ByteSize::b(diff.size_delta.abs() as u64); + let sign = if is_positive { "+" } else { "-" }; + res.extend(vec![ + Text::Raw(Cow::from("size: ")), + Text::Styled( + Cow::from(format!( + "{}", + ByteSize::b(diff.sizes.0) + )), + self.theme.text(false, false), + ), + Text::Raw(Cow::from(" -> ")), + Text::Styled( + Cow::from(format!( + "{}", + ByteSize::b(diff.sizes.1) + )), + self.theme.text(false, false), + ), + Text::Raw(Cow::from(" (")), + Text::Styled( + Cow::from(format!( + "{}{:}", + sign, delta_byte_size + )), + self.theme.diff_line( + if is_positive { + DiffLineType::Add + } else { + DiffLineType::Delete + }, + false, + ), + ), + Text::Raw(Cow::from(")")), + ]); + } else { + let selection = self.selection; - for (i, hunk) in self.diff.hunks.iter().enumerate() { - let hunk_selected = - self.selected_hunk.map_or(false, |s| s == i); + let min = self.scroll_top; + let max = min + height as usize; - if lines_added >= height as usize { - break; - } + let mut line_cursor = 0_usize; + let mut lines_added = 0_usize; - let hunk_len = hunk.lines.len(); - let hunk_min = line_cursor; - let hunk_max = line_cursor + hunk_len; + for (i, hunk) in diff.hunks.iter().enumerate() { + let hunk_selected = + self.selected_hunk.map_or(false, |s| s == i); - if Self::hunk_visible(hunk_min, hunk_max, min, max) { - for (i, line) in hunk.lines.iter().enumerate() { - if line_cursor >= min && line_cursor <= max { - Self::add_line( - &mut res, - width, - line, - selection == line_cursor, - hunk_selected, - i == hunk_len as usize - 1, - &self.theme, - ); - lines_added += 1; + if lines_added >= height as usize { + break; } - line_cursor += 1; + let hunk_len = hunk.lines.len(); + let hunk_min = line_cursor; + let hunk_max = line_cursor + hunk_len; + + if Self::hunk_visible( + hunk_min, hunk_max, min, max, + ) { + for (i, line) in hunk.lines.iter().enumerate() + { + if line_cursor >= min + && line_cursor <= max + { + Self::add_line( + &mut res, + width, + line, + selection == line_cursor, + hunk_selected, + i == hunk_len as usize - 1, + &self.theme, + ); + lines_added += 1; + } + + line_cursor += 1; + } + } else { + line_cursor += hunk_len; + } } - } else { - line_cursor += hunk_len; } } - Ok(res) } @@ -272,26 +328,34 @@ impl DiffComponent { } fn unstage_hunk(&mut self) -> Result<()> { - if let Some(hunk) = self.selected_hunk { - let hash = self.diff.hunks[hunk].header_hash; - sync::unstage_hunk(CWD, self.current.path.clone(), hash)?; - self.queue_update(); + if let Some(diff) = &self.diff { + if let Some(hunk) = self.selected_hunk { + let hash = diff.hunks[hunk].header_hash; + sync::unstage_hunk( + CWD, + self.current.path.clone(), + hash, + )?; + self.queue_update(); + } } Ok(()) } fn stage_hunk(&mut self) -> Result<()> { - if let Some(hunk) = self.selected_hunk { - let path = self.current.path.clone(); - if self.diff.untracked { - sync::stage_add_file(CWD, Path::new(&path))?; - } else { - let hash = self.diff.hunks[hunk].header_hash; - sync::stage_hunk(CWD, path, hash)?; - } + if let Some(diff) = &self.diff { + if let Some(hunk) = self.selected_hunk { + let path = self.current.path.clone(); + if diff.untracked { + sync::stage_add_file(CWD, Path::new(&path))?; + } else { + let hash = diff.hunks[hunk].header_hash; + sync::stage_hunk(CWD, path, hash)?; + } - self.queue_update(); + self.queue_update(); + } } Ok(()) @@ -306,21 +370,22 @@ impl DiffComponent { } fn reset_hunk(&self) -> Result<()> { - if let Some(hunk) = self.selected_hunk { - let hash = self.diff.hunks[hunk].header_hash; + if let Some(diff) = &self.diff { + if let Some(hunk) = self.selected_hunk { + let hash = diff.hunks[hunk].header_hash; - self.queue - .as_ref() - .expect("try using queue in immutable diff") - .borrow_mut() - .push_back(InternalEvent::ConfirmAction( - Action::ResetHunk( - self.current.path.clone(), - hash, - ), - )); + self.queue + .as_ref() + .expect("try using queue in immutable diff") + .borrow_mut() + .push_back(InternalEvent::ConfirmAction( + Action::ResetHunk( + self.current.path.clone(), + hash, + ), + )); + } } - Ok(()) } @@ -466,10 +531,12 @@ impl Component for DiffComponent { if !self.is_immutable() && !self.is_stage() => { - if self.diff.untracked { - self.reset_untracked()?; - } else { - self.reset_hunk()?; + if let Some(diff) = &self.diff { + if diff.untracked { + self.reset_untracked()?; + } else { + self.reset_hunk()?; + } } Ok(true) }