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",
]
[[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"

View file

@ -27,4 +27,5 @@ easy-cast = "0.4"
[dev-dependencies]
tempfile = "3.2"
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;
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,

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,
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<B: Backend>(
@ -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)?;

View file

@ -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<CommitId>,
tags: Option<CommitTags>,
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,

View file

@ -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};

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 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},
}
}
}

View file

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

View file

@ -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 {

View file

@ -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)
}

View file

@ -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,),),
)