diff --git a/Cargo.lock b/Cargo.lock index e0ff4dadf4..bc45bf62d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -218,6 +233,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -316,6 +345,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpp_demangle" version = "0.4.3" @@ -563,6 +598,7 @@ name = "gitu" version = "0.1.0" dependencies = [ "ansi-to-tui", + "chrono", "clap", "criterion", "crossterm", @@ -619,6 +655,29 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1723,6 +1782,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index cf562a7f99..ab93d38446 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] } [dependencies] ansi-to-tui = "3.1.0" +chrono = "0.4.34" clap = { version = "4.4.16", features = ["derive"] } crossterm = "0.27.0" git2 = "0.18.2" diff --git a/src/git/commit.rs b/src/git/commit.rs new file mode 100644 index 0000000000..6bb6233e3c --- /dev/null +++ b/src/git/commit.rs @@ -0,0 +1,5 @@ +#[derive(Debug)] +pub(crate) struct Commit { + pub hash: String, + pub details: String, +} diff --git a/src/git/mod.rs b/src/git/mod.rs index e8f3cac93b..c1f0aed0c5 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -1,5 +1,12 @@ use git2::{DiffLineType::*, Repository}; +use itertools::Itertools; +use self::{ + commit::Commit, + diff::{Delta, Diff, Hunk}, + merge_status::MergeStatus, + rebase_status::RebaseStatus, +}; use crate::Res; use std::{ error::Error, @@ -10,12 +17,7 @@ use std::{ str::{self, FromStr}, }; -use self::{ - diff::{Delta, Diff, Hunk}, - merge_status::MergeStatus, - rebase_status::RebaseStatus, -}; - +pub(crate) mod commit; pub(crate) mod diff; pub(crate) mod merge_status; mod parse; @@ -90,6 +92,7 @@ fn branch_name(dir: &Path, hash: &str) -> Res> { } pub(crate) fn diff(dir: &Path, args: &[&str]) -> Res { + assert!(args.is_empty(), "TODO handle args"); // TODO handle args? let repo = &Repository::open(dir)?; let diff = repo.diff_index_to_workdir(None, None)?; @@ -97,7 +100,7 @@ pub(crate) fn diff(dir: &Path, args: &[&str]) -> Res { } // TODO Move elsewhere -pub(crate) fn convert_diff<'a>(diff: git2::Diff) -> Res { +pub(crate) fn convert_diff(diff: git2::Diff) -> Res { let mut deltas = vec![]; let mut lines = String::new(); @@ -126,7 +129,7 @@ pub(crate) fn convert_diff<'a>(diff: git2::Diff) -> Res { if is_new_hunk { let delta = deltas.last_mut().unwrap(); - (*delta).hunks.push(Hunk { + delta.hunks.push(Hunk { file_header: delta.file_header.clone(), old_file: delta.old_file.clone(), new_file: delta.new_file.clone(), @@ -149,7 +152,6 @@ pub(crate) fn convert_diff<'a>(diff: git2::Diff) -> Res { } ContextEOFNL => { // TODO Handle '\ No newline at the end of file' - () } _ => (), }; @@ -188,26 +190,54 @@ pub(crate) fn status(dir: &Path) -> Res { } pub(crate) fn show(dir: &Path, reference: &str) -> Res { - // TODO Use libigt2 let repo = Repository::open(dir)?; let object = &repo.revparse_single(reference)?; - let tree = object.peel_to_tree()?; - let prev = tree.iter().skip(1).next().unwrap(); - - let diff = repo.diff_tree_to_tree( - Some(&prev.to_object(&repo)?.into_tree().unwrap()), - Some(&object.peel_to_tree()?), - None, - )?; + + let commit = object.peel_to_commit()?; + let tree = commit.tree()?; + let parent_tree = commit.parent(0)?.tree()?; + let diff = repo.diff_tree_to_tree(Some(&parent_tree), Some(&tree), None)?; convert_diff(diff) } -pub(crate) fn show_summary(dir: &Path, reference: &str) -> Res { +pub(crate) fn show_summary(dir: &Path, reference: &str) -> Res { let repo = Repository::open(dir)?; let object = &repo.revparse_single(reference)?; let commit = object.peel_to_commit()?; - Ok(commit.message().unwrap_or("").to_string()) + let author = commit.author(); + let name = author.name().unwrap_or(""); + let email = commit + .author() + .email() + .map(|email| format!("<{}>", email)) + .unwrap_or("".to_string()); + + let message = commit + .message() + .unwrap_or("") + .to_string() + .lines() + .map(|line| format!(" {}", line)) + .join("\n"); + + let offset = chrono::FixedOffset::east_opt(commit.time().offset_minutes() * 60).unwrap(); + let time = chrono::DateTime::with_timezone( + &chrono::DateTime::from_timestamp(commit.time().seconds(), 0).unwrap(), + &offset, + ); + + let details = format!( + "Author: {}\nDate: {}\n\n{}", + [name, &email].join(" "), + time.to_rfc2822(), + message + ); + + Ok(Commit { + hash: commit.id().to_string(), + details, + }) } // TODO Make this return a more useful type. Vec? diff --git a/src/items.rs b/src/items.rs index b86ce25419..4a61404312 100644 --- a/src/items.rs +++ b/src/items.rs @@ -154,3 +154,12 @@ pub(crate) fn create_log_items(log: &str) -> impl Iterator + '_ { } }) } + +pub(crate) fn blank_line() -> Item { + Item { + display: Text::raw(""), + depth: 0, + unselectable: true, + ..Default::default() + } +} diff --git a/src/lib.rs b/src/lib.rs index 4d3c6966f8..e5f9b69061 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -442,6 +442,29 @@ mod tests { insta::assert_snapshot!(redact_hashes(terminal, dir)); } + #[test] + fn log() { + let (ref mut terminal, ref mut state, dir) = setup(60, 20); + commit(&dir, "firstfile", "testing\ntesttest\n"); + commit(&dir, "secondfile", "testing\ntesttest\n"); + update(terminal, state, &[key('g'), key('l'), key('l')]).unwrap(); + insta::assert_snapshot!(redact_hashes(terminal, dir)); + } + + #[test] + fn show() { + let (ref mut terminal, ref mut state, dir) = setup(60, 20); + commit(&dir, "firstfile", "This should not be visible\n"); + commit(&dir, "secondfile", "This should be visible\n"); + update( + terminal, + state, + &[key('g'), key('l'), key('l'), key_code(KeyCode::Enter)], + ) + .unwrap(); + insta::assert_snapshot!(redact_hashes(terminal, dir)); + } + #[test] fn rebase_conflict() { let (ref mut terminal, ref mut state, dir) = setup(60, 20); @@ -504,8 +527,8 @@ mod tests { fn commit(dir: &TempDir, file_name: &str, contents: &str) { let path = dir.child(file_name); let message = match path.try_exists() { - Ok(true) => format!("modify {}", file_name), - _ => format!("add {}", file_name), + Ok(true) => format!("modify {}\n\nCommit body goes here\n", file_name), + _ => format!("add {}\n\nCommit body goes here\n", file_name), }; fs::write(path, contents).expect("error writing to file"); run(dir, &["git", "add", file_name]); @@ -516,6 +539,7 @@ mod tests { String::from_utf8( Command::new(cmd[0]) .args(&cmd[1..]) + .env("GIT_COMMITTER_DATE", "Sun Feb 18 14:00 2024 +0100") .current_dir(dir.path()) .output() .unwrap_or_else(|_| panic!("failed to execute {:?}", cmd)) @@ -528,11 +552,20 @@ mod tests { Event::Key(KeyEvent::new(KeyCode::Char(char), KeyModifiers::empty())) } + fn key_code(code: KeyCode) -> Event { + Event::Key(KeyEvent::new(code, KeyModifiers::empty())) + } + fn redact_hashes(terminal: &mut Terminal, dir: TempDir) -> String { let mut debug_output = format!("{:#?}", terminal.backend().buffer()); + + for hash in run(&dir, &["git", "log", "--all", "--format=%H", "HEAD"]).lines() { + debug_output = debug_output.replace(hash, &"_".repeat(hash.len())); + } for hash in run(&dir, &["git", "log", "--all", "--format=%h", "HEAD"]).lines() { - debug_output = debug_output.replace(hash, "_______"); + debug_output = debug_output.replace(hash, &"_".repeat(hash.len())); } + debug_output } } diff --git a/src/screen/show.rs b/src/screen/show.rs index bb7a61bacc..46a7cbfc14 100644 --- a/src/screen/show.rs +++ b/src/screen/show.rs @@ -1,10 +1,14 @@ use crate::{ git, items::{self, Item}, - util, Config, Res, + theme::CURRENT_THEME, + Config, Res, +}; +use ratatui::{ + prelude::Rect, + style::Stylize, + text::{Line, Text}, }; -use ansi_to_tui::IntoText; -use ratatui::{prelude::Rect, text::Text}; use super::Screen; @@ -14,18 +18,25 @@ pub(crate) fn create(config: &Config, size: Rect, reference: String) -> Res Res { vec![] } else { vec![ - blank_line(), + items::blank_line(), Item { id: "untracked".into(), display: Text::from( @@ -68,7 +68,7 @@ pub(crate) fn create(config: &Config, size: Rect) -> Res { vec![] } else { vec![ - blank_line(), + items::blank_line(), Item { id: "unmerged".into(), display: Text::from( @@ -100,14 +100,6 @@ pub(crate) fn create(config: &Config, size: Rect) -> Res { ) } -fn blank_line() -> Item { - Item { - display: Text::raw(""), - depth: 0, - unselectable: true, - ..Default::default() - } -} fn untracked(status: &git::status::Status) -> Vec { status .files diff --git a/src/snapshots/gitu__tests__log.snap b/src/snapshots/gitu__tests__log.snap new file mode 100644 index 0000000000..19a4ce64b2 --- /dev/null +++ b/src/snapshots/gitu__tests__log.snap @@ -0,0 +1,41 @@ +--- +source: src/lib.rs +expression: "redact_hashes(terminal, dir)" +--- +Buffer { + area: Rect { x: 0, y: 0, width: 60, height: 20 }, + content: [ + "🢒_______ (HEAD -> master) add secondfile ", + " _______ add firstfile ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Rgb(80, 73, 69), underline: Reset, modifier: NONE, + x: 1, y: 0, fg: Yellow, bg: Rgb(80, 73, 69), underline: Reset, modifier: NONE, + x: 10, y: 0, fg: Cyan, bg: Rgb(80, 73, 69), underline: Reset, modifier: BOLD, + x: 14, y: 0, fg: Yellow, bg: Rgb(80, 73, 69), underline: Reset, modifier: NONE, + x: 18, y: 0, fg: Green, bg: Rgb(80, 73, 69), underline: Reset, modifier: BOLD, + x: 24, y: 0, fg: Yellow, bg: Rgb(80, 73, 69), underline: Reset, modifier: NONE, + x: 25, y: 0, fg: Reset, bg: Rgb(80, 73, 69), underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 1, fg: Yellow, bg: Reset, underline: Reset, modifier: NONE, + x: 8, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + ] +} diff --git a/src/snapshots/gitu__tests__show.snap b/src/snapshots/gitu__tests__show.snap new file mode 100644 index 0000000000..c8e2da0dde --- /dev/null +++ b/src/snapshots/gitu__tests__show.snap @@ -0,0 +1,43 @@ +--- +source: src/lib.rs +expression: "redact_hashes(terminal, dir)" +--- +Buffer { + area: Rect { x: 0, y: 0, width: 60, height: 20 }, + content: [ + " commit ________________________________________ ", + " Author: CI ", + " Date: Sun, 18 Feb 2024 14:00:00 +0100 ", + " ", + " add secondfile ", + " ", + " Commit body goes here ", + " ", + " secondfile ", + "🢒@@ -0,0 +1 @@ ", + " +This should be visible ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 0, fg: Rgb(216, 166, 87), bg: Reset, underline: Reset, modifier: NONE, + x: 48, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 8, fg: Rgb(211, 134, 155), bg: Reset, underline: Reset, modifier: BOLD, + x: 11, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 9, fg: Reset, bg: Rgb(80, 73, 69), underline: Reset, modifier: NONE, + x: 1, y: 9, fg: Rgb(125, 174, 163), bg: Rgb(80, 73, 69), underline: Reset, modifier: NONE, + x: 14, y: 9, fg: Reset, bg: Rgb(80, 73, 69), underline: Reset, modifier: NONE, + x: 0, y: 10, fg: Reset, bg: Rgb(42, 40, 39), underline: Reset, modifier: NONE, + x: 1, y: 10, fg: Rgb(169, 182, 101), bg: Rgb(42, 40, 39), underline: Reset, modifier: NONE, + x: 24, y: 10, fg: Reset, bg: Rgb(42, 40, 39), underline: Reset, modifier: NONE, + x: 0, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + ] +}