file tree of a commit (#715)

This commit is contained in:
Stephan Dilly 2021-05-18 00:21:05 +02:00 committed by GitHub
parent fce294066c
commit 5ba657cabe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 390 additions and 7 deletions

47
Cargo.lock generated
View file

@ -26,6 +26,15 @@ dependencies = [
"version_check", "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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.40" version = "1.0.40"
@ -50,6 +59,7 @@ dependencies = [
"git2", "git2",
"invalidstring", "invalidstring",
"log", "log",
"pretty_assertions",
"rayon-core", "rayon-core",
"scopetime", "scopetime",
"serial_test", "serial_test",
@ -252,6 +262,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "ctor"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
dependencies = [
"quote",
"syn",
]
[[package]] [[package]]
name = "debugid" name = "debugid"
version = "0.7.2" version = "0.7.2"
@ -261,6 +281,12 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "diff"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
[[package]] [[package]]
name = "dirs-next" name = "dirs-next"
version = "2.0.0" version = "2.0.0"
@ -727,6 +753,15 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "output_vt100"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.1" version = "0.11.1"
@ -788,6 +823,18 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 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]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.19" version = "0.5.19"

View file

@ -27,4 +27,5 @@ easy-cast = "0.4"
[dev-dependencies] [dev-dependencies]
tempfile = "3.2" tempfile = "3.2"
invalidstring = { path = "../invalidstring", version = "0.1" } invalidstring = { path = "../invalidstring", version = "0.1" }
serial_test = "0.5.1" serial_test = "0.5.1"
pretty_assertions = "0.7"

View file

@ -24,6 +24,7 @@ mod stash;
mod state; mod state;
pub mod status; pub mod status;
mod tags; mod tags;
mod tree;
pub mod utils; pub mod utils;
pub use blame::{blame_file, BlameHunk, FileBlame}; pub use blame::{blame_file, BlameHunk, FileBlame};
@ -64,6 +65,7 @@ pub use stash::{
}; };
pub use state::{repo_state, RepoState}; pub use state::{repo_state, RepoState};
pub use tags::{get_tags, CommitTags, Tags}; pub use tags::{get_tags, CommitTags, Tags};
pub use tree::{tree_file_content, tree_files, TreeFile};
pub use utils::{ pub use utils::{
get_head, get_head_tuple, is_bare_repo, is_repo, stage_add_all, get_head, get_head_tuple, is_bare_repo, is_repo, stage_add_all,
stage_add_file, stage_addremoved, Head, stage_add_file, stage_addremoved, Head,

112
asyncgit/src/sync/tree.rs Normal file
View file

@ -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<Vec<TreeFile>> {
scope_time!("tree_files");
let repo = repo(repo_path)?;
let commit = repo.find_commit(commit.into())?;
let tree = commit.tree()?;
let mut files: Vec<TreeFile> = Vec::new();
tree_recurse(&repo, &PathBuf::from("./"), &tree, &mut files)?;
Ok(files)
}
///
pub fn tree_file_content(
repo_path: &str,
file: &TreeFile,
) -> Result<String> {
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<TreeFile>,
) -> 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]);
}
}

View file

@ -8,7 +8,8 @@ use crate::{
ExternalEditorComponent, HelpComponent, ExternalEditorComponent, HelpComponent,
InspectCommitComponent, MsgComponent, PullComponent, InspectCommitComponent, MsgComponent, PullComponent,
PushComponent, PushTagsComponent, RenameBranchComponent, PushComponent, PushTagsComponent, RenameBranchComponent,
ResetComponent, StashMsgComponent, TagCommitComponent, ResetComponent, RevisionFilesComponent, StashMsgComponent,
TagCommitComponent,
}, },
input::{Input, InputEvent, InputState}, input::{Input, InputEvent, InputState},
keys::{KeyConfig, SharedKeyConfig}, keys::{KeyConfig, SharedKeyConfig},
@ -45,6 +46,7 @@ pub struct App {
stashmsg_popup: StashMsgComponent, stashmsg_popup: StashMsgComponent,
inspect_commit_popup: InspectCommitComponent, inspect_commit_popup: InspectCommitComponent,
external_editor_popup: ExternalEditorComponent, external_editor_popup: ExternalEditorComponent,
revision_files_popup: RevisionFilesComponent,
push_popup: PushComponent, push_popup: PushComponent,
push_tags_popup: PushTagsComponent, push_tags_popup: PushTagsComponent,
pull_popup: PullComponent, pull_popup: PullComponent,
@ -101,6 +103,12 @@ impl App {
theme.clone(), theme.clone(),
key_config.clone(), key_config.clone(),
), ),
revision_files_popup: RevisionFilesComponent::new(
&queue,
sender,
theme.clone(),
key_config.clone(),
),
stashmsg_popup: StashMsgComponent::new( stashmsg_popup: StashMsgComponent::new(
queue.clone(), queue.clone(),
theme.clone(), theme.clone(),
@ -386,6 +394,7 @@ impl App {
create_branch_popup, create_branch_popup,
rename_branch_popup, rename_branch_popup,
select_branch_popup, select_branch_popup,
revision_files_popup,
help, help,
revlog, revlog,
status_tab, status_tab,
@ -565,6 +574,10 @@ impl App {
InternalEvent::StatusLastFileMoved => { InternalEvent::StatusLastFileMoved => {
self.status_tab.last_file_moved()?; self.status_tab.last_file_moved()?;
} }
InternalEvent::OpenFileTree(c) => {
self.revision_files_popup.open(c)?;
flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS)
}
}; };
Ok(flags) Ok(flags)
@ -682,6 +695,7 @@ impl App {
|| self.pull_popup.is_visible() || self.pull_popup.is_visible()
|| self.select_branch_popup.is_visible() || self.select_branch_popup.is_visible()
|| self.rename_branch_popup.is_visible() || self.rename_branch_popup.is_visible()
|| self.revision_files_popup.is_visible()
} }
fn draw_popups<B: Backend>( fn draw_popups<B: Backend>(
@ -709,6 +723,7 @@ impl App {
self.select_branch_popup.draw(f, size)?; self.select_branch_popup.draw(f, size)?;
self.create_branch_popup.draw(f, size)?; self.create_branch_popup.draw(f, size)?;
self.rename_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_popup.draw(f, size)?;
self.push_tags_popup.draw(f, size)?; self.push_tags_popup.draw(f, size)?;
self.pull_popup.draw(f, size)?; self.pull_popup.draw(f, size)?;

View file

@ -4,7 +4,10 @@ use super::{
DrawableComponent, EventState, DrawableComponent, EventState,
}; };
use crate::{ use crate::{
accessors, keys::SharedKeyConfig, queue::Queue, strings, accessors,
keys::SharedKeyConfig,
queue::{InternalEvent, Queue},
strings,
ui::style::SharedTheme, ui::style::SharedTheme,
}; };
use anyhow::Result; use anyhow::Result;
@ -22,6 +25,7 @@ use tui::{
}; };
pub struct InspectCommitComponent { pub struct InspectCommitComponent {
queue: Queue,
commit_id: Option<CommitId>, commit_id: Option<CommitId>,
tags: Option<CommitTags>, tags: Option<CommitTags>,
diff: DiffComponent, diff: DiffComponent,
@ -98,6 +102,14 @@ impl Component for InspectCommitComponent {
true, true,
self.diff.focused() || force_all, self.diff.focused() || force_all,
)); ));
out.push(CommandInfo::new(
strings::commands::inspect_file_tree(
&self.key_config,
),
true,
true,
));
} }
visibility_blocking(self) visibility_blocking(self)
@ -124,6 +136,13 @@ impl Component for InspectCommitComponent {
{ {
self.details.focus(true); self.details.focus(true);
self.diff.focus(false); 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 { } else if e == self.key_config.focus_left {
self.hide(); self.hide();
} }
@ -162,6 +181,7 @@ impl InspectCommitComponent {
key_config: SharedKeyConfig, key_config: SharedKeyConfig,
) -> Self { ) -> Self {
Self { Self {
queue: queue.clone(),
details: CommitDetailsComponent::new( details: CommitDetailsComponent::new(
queue, queue,
sender, sender,

View file

@ -18,6 +18,7 @@ mod push;
mod push_tags; mod push_tags;
mod rename_branch; mod rename_branch;
mod reset; mod reset;
mod revision_files;
mod stashmsg; mod stashmsg;
mod tag_commit; mod tag_commit;
mod textinput; mod textinput;
@ -42,6 +43,7 @@ pub use push::PushComponent;
pub use push_tags::PushTagsComponent; pub use push_tags::PushTagsComponent;
pub use rename_branch::RenameBranchComponent; pub use rename_branch::RenameBranchComponent;
pub use reset::ResetComponent; pub use reset::ResetComponent;
pub use revision_files::RevisionFilesComponent;
pub use stashmsg::StashMsgComponent; pub use stashmsg::StashMsgComponent;
pub use tag_commit::TagCommitComponent; pub use tag_commit::TagCommitComponent;
pub use textinput::{InputType, TextInputComponent}; pub use textinput::{InputType, TextInputComponent};

View file

@ -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<TreeFile>,
revision: Option<CommitId>,
visible: bool,
key_config: SharedKeyConfig,
current_height: std::cell::Cell<usize>,
}
impl RevisionFilesComponent {
///
pub fn new(
_queue: &Queue,
_sender: &Sender<AsyncNotification>,
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<B: Backend>(
&self,
f: &mut Frame<B>,
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<CommandInfo>,
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<EventState> {
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(())
}
}

View file

@ -71,6 +71,7 @@ pub struct KeyConfig {
pub delete_branch: KeyEvent, pub delete_branch: KeyEvent,
pub merge_branch: KeyEvent, pub merge_branch: KeyEvent,
pub push: KeyEvent, pub push: KeyEvent,
pub open_file_tree: KeyEvent,
pub force_push: KeyEvent, pub force_push: KeyEvent,
pub pull: KeyEvent, pub pull: KeyEvent,
pub abort_merge: KeyEvent, pub abort_merge: KeyEvent,
@ -127,12 +128,13 @@ impl Default for KeyConfig {
create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::empty()}, create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::empty()},
rename_branch: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty()}, rename_branch: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty()},
select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()}, select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()},
delete_branch: KeyEvent{code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, delete_branch: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT},
merge_branch: KeyEvent{code: KeyCode::Char('m'), modifiers: KeyModifiers::empty()}, merge_branch: KeyEvent { code: KeyCode::Char('m'), modifiers: KeyModifiers::empty()},
push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()}, push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()},
force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT}, force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT},
pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()},
abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT}, abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT},
open_file_tree: KeyEvent { code: KeyCode::Char('F'), modifiers: KeyModifiers::SHIFT},
} }
} }
} }

View file

@ -76,6 +76,8 @@ pub enum InternalEvent {
Pull(String), Pull(String),
/// ///
PushTags, PushTags,
///
OpenFileTree(CommitId),
} }
/// ///

View file

@ -864,6 +864,18 @@ pub mod commands {
CMD_GROUP_LOG, 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( pub fn tag_commit_confirm_msg(
key_config: &SharedKeyConfig, key_config: &SharedKeyConfig,
) -> CommandText { ) -> CommandText {

View file

@ -249,6 +249,16 @@ impl Component for Revlog {
.borrow_mut() .borrow_mut()
.push_back(InternalEvent::SelectBranch); .push_back(InternalEvent::SelectBranch);
return Ok(EventState::Consumed); 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( out.push(CommandInfo::new(
strings::commands::log_tag_commit(&self.key_config), strings::commands::log_tag_commit(&self.key_config),
true, self.selected_commit().is_some(),
self.visible || force_all, self.visible || force_all,
)); ));
@ -294,7 +304,7 @@ impl Component for Revlog {
out.push(CommandInfo::new( out.push(CommandInfo::new(
strings::commands::copy_hash(&self.key_config), strings::commands::copy_hash(&self.key_config),
true, self.selected_commit().is_some(),
self.visible || force_all, self.visible || force_all,
)); ));
@ -304,6 +314,12 @@ impl Component for Revlog {
self.visible || force_all, 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) visibility_blocking(self)
} }

View file

@ -79,6 +79,8 @@
force_push: ( code: Char('P'), modifiers: ( bits: 1,),), force_push: ( code: Char('P'), modifiers: ( bits: 1,),),
pull: ( code: Char('f'), modifiers: ( bits: 0,),), pull: ( code: Char('f'), modifiers: ( bits: 0,),),
open_file_tree: ( code: Char('F'), modifiers: ( bits: 1,),),
//removed in 0.11 //removed in 0.11
//tab_toggle_reverse_windows: ( code: BackTab, modifiers: ( bits: 1,),), //tab_toggle_reverse_windows: ( code: BackTab, modifiers: ( bits: 1,),),
) )