From 5ba657cabefafaa35bea4887d6502a3a107750d7 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Tue, 18 May 2021 00:21:05 +0200 Subject: [PATCH] file tree of a commit (#715) --- Cargo.lock | 47 ++++++++++ asyncgit/Cargo.toml | 3 +- asyncgit/src/sync/mod.rs | 2 + asyncgit/src/sync/tree.rs | 112 +++++++++++++++++++++++ src/app.rs | 17 +++- src/components/inspect_commit.rs | 22 ++++- src/components/mod.rs | 2 + src/components/revision_files.rs | 150 +++++++++++++++++++++++++++++++ src/keys.rs | 6 +- src/queue.rs | 2 + src/strings.rs | 12 +++ src/tabs/revlog.rs | 20 ++++- vim_style_key_config.ron | 2 + 13 files changed, 390 insertions(+), 7 deletions(-) create mode 100644 asyncgit/src/sync/tree.rs create mode 100644 src/components/revision_files.rs diff --git a/Cargo.lock b/Cargo.lock index c5915ce0..b82bb97c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.40" @@ -50,6 +59,7 @@ dependencies = [ "git2", "invalidstring", "log", + "pretty_assertions", "rayon-core", "scopetime", "serial_test", @@ -252,6 +262,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "ctor" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "debugid" version = "0.7.2" @@ -261,6 +281,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + [[package]] name = "dirs-next" version = "2.0.0" @@ -727,6 +753,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi", +] + [[package]] name = "parking_lot" version = "0.11.1" @@ -788,6 +823,18 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "pretty_assertions" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b" +dependencies = [ + "ansi_term", + "ctor", + "diff", + "output_vt100", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index b1872a65..18c9e12e 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -27,4 +27,5 @@ easy-cast = "0.4" [dev-dependencies] tempfile = "3.2" invalidstring = { path = "../invalidstring", version = "0.1" } -serial_test = "0.5.1" \ No newline at end of file +serial_test = "0.5.1" +pretty_assertions = "0.7" \ No newline at end of file diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 838724d6..8b3ea52c 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -24,6 +24,7 @@ mod stash; mod state; pub mod status; mod tags; +mod tree; pub mod utils; pub use blame::{blame_file, BlameHunk, FileBlame}; @@ -64,6 +65,7 @@ pub use stash::{ }; pub use state::{repo_state, RepoState}; pub use tags::{get_tags, CommitTags, Tags}; +pub use tree::{tree_file_content, tree_files, TreeFile}; pub use utils::{ get_head, get_head_tuple, is_bare_repo, is_repo, stage_add_all, stage_add_file, stage_addremoved, Head, diff --git a/asyncgit/src/sync/tree.rs b/asyncgit/src/sync/tree.rs new file mode 100644 index 00000000..53eaec5e --- /dev/null +++ b/asyncgit/src/sync/tree.rs @@ -0,0 +1,112 @@ +use super::{utils::bytes2string, CommitId}; +use crate::{error::Result, sync::utils::repo}; +use git2::{Oid, Repository, Tree}; +use scopetime::scope_time; +use std::path::{Path, PathBuf}; + +/// `tree_files` returns a list of `FileTree` +#[derive(Debug, PartialEq)] +pub struct TreeFile { + /// path of this file + pub path: PathBuf, + /// unix filemode + pub filemode: i32, + // internal object id + id: Oid, +} + +/// +pub fn tree_files( + repo_path: &str, + commit: CommitId, +) -> Result> { + scope_time!("tree_files"); + + let repo = repo(repo_path)?; + + let commit = repo.find_commit(commit.into())?; + let tree = commit.tree()?; + + let mut files: Vec = Vec::new(); + + tree_recurse(&repo, &PathBuf::from("./"), &tree, &mut files)?; + + Ok(files) +} + +/// +pub fn tree_file_content( + repo_path: &str, + file: &TreeFile, +) -> Result { + scope_time!("tree_file_content"); + + let repo = repo(repo_path)?; + + let blob = repo.find_blob(file.id)?; + let content = String::from_utf8(blob.content().into())?; + + Ok(content) +} + +/// +fn tree_recurse( + repo: &Repository, + path: &Path, + tree: &Tree, + out: &mut Vec, +) -> Result<()> { + out.reserve(tree.len()); + + for e in tree { + let path = path.join(bytes2string(e.name_bytes())?); + match e.kind() { + Some(git2::ObjectType::Blob) => { + let id = e.id(); + let filemode = e.filemode(); + out.push(TreeFile { path, filemode, id }); + } + Some(git2::ObjectType::Tree) => { + let obj = e.to_object(repo)?; + let tree = obj.peel_to_tree()?; + tree_recurse(repo, &path, &tree, out)?; + } + Some(_) | None => (), + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sync::tests::{repo_init, write_commit_file}; + use pretty_assertions::{assert_eq, assert_ne}; + + #[test] + fn test_smoke() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + let c1 = + write_commit_file(&repo, "test.txt", "content", "c1"); + + let files = tree_files(repo_path, c1).unwrap(); + + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, PathBuf::from("./test.txt")); + + let c2 = + write_commit_file(&repo, "test.txt", "content2", "c2"); + + let content = + tree_file_content(repo_path, &files[0]).unwrap(); + assert_eq!(&content, "content"); + + let files_c2 = tree_files(repo_path, c2).unwrap(); + + assert_eq!(files_c2.len(), 1); + assert_ne!(files_c2[0], files[0]); + } +} diff --git a/src/app.rs b/src/app.rs index 9131b373..f6bcf605 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,8 @@ use crate::{ ExternalEditorComponent, HelpComponent, InspectCommitComponent, MsgComponent, PullComponent, PushComponent, PushTagsComponent, RenameBranchComponent, - ResetComponent, StashMsgComponent, TagCommitComponent, + ResetComponent, RevisionFilesComponent, StashMsgComponent, + TagCommitComponent, }, input::{Input, InputEvent, InputState}, keys::{KeyConfig, SharedKeyConfig}, @@ -45,6 +46,7 @@ pub struct App { stashmsg_popup: StashMsgComponent, inspect_commit_popup: InspectCommitComponent, external_editor_popup: ExternalEditorComponent, + revision_files_popup: RevisionFilesComponent, push_popup: PushComponent, push_tags_popup: PushTagsComponent, pull_popup: PullComponent, @@ -101,6 +103,12 @@ impl App { theme.clone(), key_config.clone(), ), + revision_files_popup: RevisionFilesComponent::new( + &queue, + sender, + theme.clone(), + key_config.clone(), + ), stashmsg_popup: StashMsgComponent::new( queue.clone(), theme.clone(), @@ -386,6 +394,7 @@ impl App { create_branch_popup, rename_branch_popup, select_branch_popup, + revision_files_popup, help, revlog, status_tab, @@ -565,6 +574,10 @@ impl App { InternalEvent::StatusLastFileMoved => { self.status_tab.last_file_moved()?; } + InternalEvent::OpenFileTree(c) => { + self.revision_files_popup.open(c)?; + flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS) + } }; Ok(flags) @@ -682,6 +695,7 @@ impl App { || self.pull_popup.is_visible() || self.select_branch_popup.is_visible() || self.rename_branch_popup.is_visible() + || self.revision_files_popup.is_visible() } fn draw_popups( @@ -709,6 +723,7 @@ impl App { self.select_branch_popup.draw(f, size)?; self.create_branch_popup.draw(f, size)?; self.rename_branch_popup.draw(f, size)?; + self.revision_files_popup.draw(f, size)?; self.push_popup.draw(f, size)?; self.push_tags_popup.draw(f, size)?; self.pull_popup.draw(f, size)?; diff --git a/src/components/inspect_commit.rs b/src/components/inspect_commit.rs index 9a1bdf39..8d3fb39b 100644 --- a/src/components/inspect_commit.rs +++ b/src/components/inspect_commit.rs @@ -4,7 +4,10 @@ use super::{ DrawableComponent, EventState, }; use crate::{ - accessors, keys::SharedKeyConfig, queue::Queue, strings, + accessors, + keys::SharedKeyConfig, + queue::{InternalEvent, Queue}, + strings, ui::style::SharedTheme, }; use anyhow::Result; @@ -22,6 +25,7 @@ use tui::{ }; pub struct InspectCommitComponent { + queue: Queue, commit_id: Option, tags: Option, diff: DiffComponent, @@ -98,6 +102,14 @@ impl Component for InspectCommitComponent { true, self.diff.focused() || force_all, )); + + out.push(CommandInfo::new( + strings::commands::inspect_file_tree( + &self.key_config, + ), + true, + true, + )); } visibility_blocking(self) @@ -124,6 +136,13 @@ impl Component for InspectCommitComponent { { self.details.focus(true); self.diff.focus(false); + } else if e == self.key_config.open_file_tree { + if let Some(commit) = self.commit_id { + self.queue.borrow_mut().push_back( + InternalEvent::OpenFileTree(commit), + ); + self.hide(); + } } else if e == self.key_config.focus_left { self.hide(); } @@ -162,6 +181,7 @@ impl InspectCommitComponent { key_config: SharedKeyConfig, ) -> Self { Self { + queue: queue.clone(), details: CommitDetailsComponent::new( queue, sender, diff --git a/src/components/mod.rs b/src/components/mod.rs index 09db51e0..fb8c88eb 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -18,6 +18,7 @@ mod push; mod push_tags; mod rename_branch; mod reset; +mod revision_files; mod stashmsg; mod tag_commit; mod textinput; @@ -42,6 +43,7 @@ pub use push::PushComponent; pub use push_tags::PushTagsComponent; pub use rename_branch::RenameBranchComponent; pub use reset::ResetComponent; +pub use revision_files::RevisionFilesComponent; pub use stashmsg::StashMsgComponent; pub use tag_commit::TagCommitComponent; pub use textinput::{InputType, TextInputComponent}; diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs new file mode 100644 index 00000000..78a7d111 --- /dev/null +++ b/src/components/revision_files.rs @@ -0,0 +1,150 @@ +use super::{ + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, EventState, +}; +use crate::{ + keys::SharedKeyConfig, + queue::Queue, + strings, + ui::{self, style::SharedTheme}, +}; +use anyhow::Result; +use asyncgit::{ + sync::{self, CommitId, TreeFile}, + AsyncNotification, CWD, +}; +use crossbeam_channel::Sender; +use crossterm::event::Event; +use tui::{ + backend::Backend, layout::Rect, text::Span, widgets::Clear, Frame, +}; + +pub struct RevisionFilesComponent { + title: String, + theme: SharedTheme, + // queue: Queue, + files: Vec, + revision: Option, + visible: bool, + key_config: SharedKeyConfig, + current_height: std::cell::Cell, +} + +impl RevisionFilesComponent { + /// + pub fn new( + _queue: &Queue, + _sender: &Sender, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + title: String::new(), + theme, + files: Vec::new(), + revision: None, + // queue: queue.clone(), + visible: false, + key_config, + current_height: std::cell::Cell::new(0), + } + } + + /// + pub fn open(&mut self, commit: CommitId) -> Result<()> { + self.files = sync::tree_files(CWD, commit)?; + self.revision = Some(commit); + self.title = format!( + "File Tree at {}", + self.revision + .map(|c| c.get_short_string()) + .unwrap_or_default() + ); + self.show()?; + + Ok(()) + } +} + +impl DrawableComponent for RevisionFilesComponent { + fn draw( + &self, + f: &mut Frame, + area: Rect, + ) -> Result<()> { + if self.is_visible() { + let items = self.files.iter().map(|f| { + Span::styled( + f.path.to_string_lossy(), + self.theme.text(true, false), + ) + }); + + f.render_widget(Clear, area); + ui::draw_list( + f, + area, + &self.title, + items, + true, + &self.theme, + ); + + self.current_height.set(area.height.into()); + } + + Ok(()) + } +} + +impl Component for RevisionFilesComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + out.push( + CommandInfo::new( + strings::commands::close_popup(&self.key_config), + true, + true, + ) + .order(1), + ); + } + + visibility_blocking(self) + } + + fn event( + &mut self, + event: crossterm::event::Event, + ) -> Result { + if self.is_visible() { + if let Event::Key(key) = event { + if key == self.key_config.exit_popup { + self.hide(); + } + + return Ok(EventState::Consumed); + } + } + + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } +} diff --git a/src/keys.rs b/src/keys.rs index c8ba4fa1..7c4e46d4 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -71,6 +71,7 @@ pub struct KeyConfig { pub delete_branch: KeyEvent, pub merge_branch: KeyEvent, pub push: KeyEvent, + pub open_file_tree: KeyEvent, pub force_push: KeyEvent, pub pull: KeyEvent, pub abort_merge: KeyEvent, @@ -127,12 +128,13 @@ impl Default for KeyConfig { create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::empty()}, rename_branch: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty()}, select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()}, - delete_branch: KeyEvent{code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, - merge_branch: KeyEvent{code: KeyCode::Char('m'), modifiers: KeyModifiers::empty()}, + delete_branch: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, + merge_branch: KeyEvent { code: KeyCode::Char('m'), modifiers: KeyModifiers::empty()}, push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()}, force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT}, pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT}, + open_file_tree: KeyEvent { code: KeyCode::Char('F'), modifiers: KeyModifiers::SHIFT}, } } } diff --git a/src/queue.rs b/src/queue.rs index 423bc1b0..c761598a 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -76,6 +76,8 @@ pub enum InternalEvent { Pull(String), /// PushTags, + /// + OpenFileTree(CommitId), } /// diff --git a/src/strings.rs b/src/strings.rs index 8436389a..6ddf3e5e 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -864,6 +864,18 @@ pub mod commands { CMD_GROUP_LOG, ) } + pub fn inspect_file_tree( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Files [{}]", + key_config.get_hint(key_config.open_file_tree), + ), + "inspect file tree at specific revision", + CMD_GROUP_LOG, + ) + } pub fn tag_commit_confirm_msg( key_config: &SharedKeyConfig, ) -> CommandText { diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index 898d8837..616bbcdf 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -249,6 +249,16 @@ impl Component for Revlog { .borrow_mut() .push_back(InternalEvent::SelectBranch); return Ok(EventState::Consumed); + } else if k == self.key_config.open_file_tree { + return self.selected_commit().map_or( + Ok(EventState::NotConsumed), + |id| { + self.queue.borrow_mut().push_back( + InternalEvent::OpenFileTree(id), + ); + Ok(EventState::Consumed) + }, + ); } } } @@ -280,7 +290,7 @@ impl Component for Revlog { out.push(CommandInfo::new( strings::commands::log_tag_commit(&self.key_config), - true, + self.selected_commit().is_some(), self.visible || force_all, )); @@ -294,7 +304,7 @@ impl Component for Revlog { out.push(CommandInfo::new( strings::commands::copy_hash(&self.key_config), - true, + self.selected_commit().is_some(), self.visible || force_all, )); @@ -304,6 +314,12 @@ impl Component for Revlog { self.visible || force_all, )); + out.push(CommandInfo::new( + strings::commands::inspect_file_tree(&self.key_config), + self.selected_commit().is_some(), + self.visible || force_all, + )); + visibility_blocking(self) } diff --git a/vim_style_key_config.ron b/vim_style_key_config.ron index 0a64ae4d..29b85991 100644 --- a/vim_style_key_config.ron +++ b/vim_style_key_config.ron @@ -79,6 +79,8 @@ force_push: ( code: Char('P'), modifiers: ( bits: 1,),), pull: ( code: Char('f'), modifiers: ( bits: 0,),), + open_file_tree: ( code: Char('F'), modifiers: ( bits: 1,),), + //removed in 0.11 //tab_toggle_reverse_windows: ( code: BackTab, modifiers: ( bits: 1,),), )