Add popup for file history (#841)

This commit is contained in:
Christoph Rüßler 2022-01-30 18:50:50 +01:00 committed by GitHub
parent 3e72539ca1
commit b622ceef94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 713 additions and 79 deletions

View file

@ -78,6 +78,8 @@ pub enum AsyncGitNotification {
///
Log,
///
FileLog,
///
CommitFiles,
///
Tags,

View file

@ -4,7 +4,6 @@ use crate::{
AsyncGitNotification,
};
use crossbeam_channel::Sender;
use git2::Oid;
use scopetime::scope_time;
use std::{
sync::{
@ -29,6 +28,7 @@ pub enum FetchStatus {
///
pub struct AsyncLog {
current: Arc<Mutex<Vec<CommitId>>>,
current_head: Arc<Mutex<Option<CommitId>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicBool>,
background: Arc<AtomicBool>,
@ -50,6 +50,7 @@ impl AsyncLog {
Self {
repo,
current: Arc::new(Mutex::new(Vec::new())),
current_head: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicBool::new(false)),
background: Arc::new(AtomicBool::new(false)),
@ -95,20 +96,16 @@ impl AsyncLog {
}
///
fn current_head(&self) -> Result<CommitId> {
Ok(self
.current
.lock()?
.first()
.map_or(Oid::zero().into(), |f| *f))
fn current_head(&self) -> Result<Option<CommitId>> {
Ok(*self.current_head.lock()?)
}
///
fn head_changed(&self) -> Result<bool> {
if let Ok(head) = repo(&self.repo)?.head() {
if let Some(head) = head.target() {
return Ok(head != self.current_head()?.into());
}
return Ok(
head.target() != self.current_head()?.map(Into::into)
);
}
Ok(false)
}
@ -132,15 +129,20 @@ impl AsyncLog {
let arc_pending = Arc::clone(&self.pending);
let arc_background = Arc::clone(&self.background);
let filter = self.filter.clone();
let repo = self.repo.clone();
let repo_path = self.repo.clone();
self.pending.store(true, Ordering::Relaxed);
if let Ok(head) = repo(&self.repo)?.head() {
*self.current_head.lock()? =
head.target().map(CommitId::new);
}
rayon_core::spawn(move || {
scope_time!("async::revlog");
Self::fetch_helper(
&repo,
&repo_path,
&arc_current,
&arc_background,
&sender,
@ -195,6 +197,7 @@ impl AsyncLog {
fn clear(&mut self) -> Result<()> {
self.current.lock()?.clear();
*self.current_head.lock()? = None;
Ok(())
}

View file

@ -1,4 +1,4 @@
use std::cmp::Ordering;
//! Functions for getting infos about files in commits
use super::{stash::is_stash_commit, CommitId, RepoPath};
use crate::{
@ -6,6 +6,7 @@ use crate::{
};
use git2::{Diff, DiffOptions, Repository};
use scopetime::scope_time;
use std::cmp::Ordering;
/// get all files that are part of a commit
pub fn get_commit_files(
@ -42,6 +43,7 @@ pub fn get_commit_files(
Ok(res)
}
/// get diff of two arbitrary commits
#[allow(clippy::needless_pass_by_value)]
pub fn get_compare_commits_diff(
repo: &Repository,
@ -80,8 +82,8 @@ pub fn get_compare_commits_diff(
Ok(diff)
}
#[allow(clippy::redundant_pub_crate)]
pub(crate) fn get_commit_diff<'a>(
/// get diff of a commit to its first parent
pub fn get_commit_diff<'a>(
repo_path: &RepoPath,
repo: &'a Repository,
id: CommitId,

View file

@ -1,5 +1,6 @@
use super::CommitId;
use crate::error::Result;
use crate::sync::RepoPath;
use crate::{error::Result, sync::commit_files::get_commit_diff};
use git2::{Commit, Oid, Repository};
use std::{
cmp::Ordering,
@ -34,6 +35,29 @@ pub type LogWalkerFilter = Arc<
Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
>;
///
pub fn diff_contains_file(
repo_path: RepoPath,
file_path: String,
) -> LogWalkerFilter {
Arc::new(Box::new(
move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let diff = get_commit_diff(
&repo_path,
repo,
*commit_id,
Some(file_path.clone()),
)?;
let contains_file = diff.deltas().len() > 0;
Ok(contains_file)
},
))
}
///
pub struct LogWalker<'a> {
commits: BinaryHeap<TimeOrderedCommit<'a>>,
@ -111,8 +135,8 @@ mod tests {
use crate::error::Result;
use crate::sync::RepoPath;
use crate::sync::{
commit, commit_files::get_commit_diff, get_commits_info,
stage_add_file, tests::repo_init_empty,
commit, get_commits_info, stage_add_file,
tests::repo_init_empty,
};
use pretty_assertions::assert_eq;
use std::{fs::File, io::Write, path::Path};
@ -201,24 +225,12 @@ mod tests {
let _third_commit_id = commit(&repo_path, "commit3").unwrap();
let repo_path_clone = repo_path.clone();
let diff_contains_baz = move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let diff = get_commit_diff(
&repo_path_clone,
&repo,
*commit_id,
Some("baz".into()),
)?;
let contains_file = diff.deltas().len() > 0;
Ok(contains_file)
};
let diff_contains_baz =
diff_contains_file(repo_path_clone, "baz".into());
let mut items = Vec::new();
let mut walker = LogWalker::new(&repo, 100)?
.filter(Some(Arc::new(Box::new(diff_contains_baz))));
.filter(Some(diff_contains_baz));
walker.read(&mut items).unwrap();
assert_eq!(items.len(), 1);
@ -229,25 +241,12 @@ mod tests {
assert_eq!(items.len(), 0);
let repo_path_clone = repo_path.clone();
let diff_contains_bar = move |repo: &Repository,
commit_id: &CommitId|
-> Result<bool> {
let diff = get_commit_diff(
&repo_path_clone,
&repo,
*commit_id,
Some("bar".into()),
)?;
let contains_file = diff.deltas().len() > 0;
Ok(contains_file)
};
let diff_contains_bar =
diff_contains_file(repo_path, "bar".into());
let mut items = Vec::new();
let mut walker = LogWalker::new(&repo, 100)?
.filter(Some(Arc::new(Box::new(diff_contains_bar))));
.filter(Some(diff_contains_bar));
walker.read(&mut items).unwrap();
assert_eq!(items.len(), 0);

View file

@ -7,7 +7,7 @@ pub mod blame;
pub mod branch;
mod commit;
mod commit_details;
mod commit_files;
pub mod commit_files;
mod commit_revert;
mod commits_info;
mod config;
@ -60,7 +60,7 @@ pub use hooks::{
};
pub use hunks::{reset_hunk, stage_hunk, unstage_hunk};
pub use ignore::add_to_ignore;
pub use logwalker::{LogWalker, LogWalkerFilter};
pub use logwalker::{diff_contains_file, LogWalker, LogWalkerFilter};
pub use merge::{
abort_pending_rebase, abort_pending_state,
continue_pending_rebase, merge_branch, merge_commit, merge_msg,

View file

@ -7,11 +7,11 @@ use crate::{
CommitComponent, CompareCommitsComponent, Component,
ConfirmComponent, CreateBranchComponent, DrawableComponent,
ExternalEditorComponent, FetchComponent, FileFindPopup,
HelpComponent, InspectCommitComponent, MsgComponent,
OptionsPopupComponent, PullComponent, PushComponent,
PushTagsComponent, RenameBranchComponent, RevisionFilesPopup,
SharedOptions, StashMsgComponent, TagCommitComponent,
TagListComponent,
FileRevlogComponent, HelpComponent, InspectCommitComponent,
MsgComponent, OptionsPopupComponent, PullComponent,
PushComponent, PushTagsComponent, RenameBranchComponent,
RevisionFilesPopup, SharedOptions, StashMsgComponent,
TagCommitComponent, TagListComponent,
},
input::{Input, InputEvent, InputState},
keys::{KeyConfig, SharedKeyConfig},
@ -51,6 +51,7 @@ pub struct App {
reset: ConfirmComponent,
commit: CommitComponent,
blame_file_popup: BlameFileComponent,
file_revlog_popup: FileRevlogComponent,
stashmsg_popup: StashMsgComponent,
inspect_commit_popup: InspectCommitComponent,
compare_commits_popup: CompareCommitsComponent,
@ -122,6 +123,13 @@ impl App {
theme.clone(),
key_config.clone(),
),
file_revlog_popup: FileRevlogComponent::new(
&repo,
&queue,
sender,
theme.clone(),
key_config.clone(),
),
revision_files_popup: RevisionFilesPopup::new(
repo.clone(),
&queue,
@ -419,6 +427,7 @@ impl App {
self.stashing_tab.update_git(ev)?;
self.revlog.update_git(ev)?;
self.blame_file_popup.update_git(ev)?;
self.file_revlog_popup.update_git(ev)?;
self.inspect_commit_popup.update_git(ev)?;
self.compare_commits_popup.update_git(ev)?;
self.push_popup.update_git(ev)?;
@ -451,6 +460,7 @@ impl App {
|| self.stashing_tab.anything_pending()
|| self.files_tab.anything_pending()
|| self.blame_file_popup.any_work_pending()
|| self.file_revlog_popup.any_work_pending()
|| self.inspect_commit_popup.any_work_pending()
|| self.compare_commits_popup.any_work_pending()
|| self.input.is_state_changing()
@ -483,6 +493,7 @@ impl App {
reset,
commit,
blame_file_popup,
file_revlog_popup,
stashmsg_popup,
inspect_commit_popup,
compare_commits_popup,
@ -516,6 +527,7 @@ impl App {
inspect_commit_popup,
compare_commits_popup,
blame_file_popup,
file_revlog_popup,
external_editor_popup,
tag_commit_popup,
select_branch_popup,
@ -630,6 +642,7 @@ impl App {
self.status_tab.update_diff()?;
self.inspect_commit_popup.update_diff()?;
self.compare_commits_popup.update_diff()?;
self.file_revlog_popup.update_diff()?;
}
if flags.contains(NeedsUpdate::COMMANDS) {
self.update_commands();
@ -695,6 +708,11 @@ impl App {
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::OpenFileRevlog(path) => {
self.file_revlog_popup.open(&path)?;
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::CreateBranch => {
self.create_branch_popup.open()?;
}

View file

@ -635,15 +635,15 @@ impl DrawableComponent for DiffComponent {
Block::default()
.title(Span::styled(
title.as_str(),
self.theme.title(self.focused),
self.theme.title(self.focused()),
))
.borders(Borders::ALL)
.border_style(self.theme.block(self.focused)),
.border_style(self.theme.block(self.focused())),
),
r,
);
if self.focused {
if self.focused() {
self.scroll.draw(f, r, &self.theme);
}
@ -660,14 +660,14 @@ impl Component for DiffComponent {
out.push(CommandInfo::new(
strings::commands::scroll(&self.key_config),
self.can_scroll(),
self.focused,
self.focused(),
));
out.push(
CommandInfo::new(
strings::commands::diff_home_end(&self.key_config),
self.can_scroll(),
self.focused,
self.focused(),
)
.hidden(),
);
@ -676,17 +676,17 @@ impl Component for DiffComponent {
out.push(CommandInfo::new(
strings::commands::diff_hunk_remove(&self.key_config),
self.selected_hunk.is_some(),
self.focused && self.is_stage(),
self.focused() && self.is_stage(),
));
out.push(CommandInfo::new(
strings::commands::diff_hunk_add(&self.key_config),
self.selected_hunk.is_some(),
self.focused && !self.is_stage(),
self.focused() && !self.is_stage(),
));
out.push(CommandInfo::new(
strings::commands::diff_hunk_revert(&self.key_config),
self.selected_hunk.is_some(),
self.focused && !self.is_stage(),
self.focused() && !self.is_stage(),
));
out.push(CommandInfo::new(
strings::commands::diff_lines_revert(
@ -694,13 +694,13 @@ impl Component for DiffComponent {
),
//TODO: only if any modifications are selected
true,
self.focused && !self.is_stage(),
self.focused() && !self.is_stage(),
));
out.push(CommandInfo::new(
strings::commands::diff_lines_stage(&self.key_config),
//TODO: only if any modifications are selected
true,
self.focused && !self.is_stage(),
self.focused() && !self.is_stage(),
));
out.push(CommandInfo::new(
strings::commands::diff_lines_unstage(
@ -708,14 +708,14 @@ impl Component for DiffComponent {
),
//TODO: only if any modifications are selected
true,
self.focused && self.is_stage(),
self.focused() && self.is_stage(),
));
}
out.push(CommandInfo::new(
strings::commands::copy(&self.key_config),
true,
self.focused,
self.focused(),
));
CommandBlocking::PassingOn
@ -723,7 +723,7 @@ impl Component for DiffComponent {
#[allow(clippy::cognitive_complexity)]
fn event(&mut self, ev: Event) -> Result<EventState> {
if self.focused {
if self.focused() {
if let Event::Key(e) = ev {
return if e == self.key_config.keys.move_down {
self.move_selection(ScrollType::Down);

View file

@ -0,0 +1,531 @@
use super::utils::logitems::ItemBatch;
use super::visibility_blocking;
use crate::{
components::{
event_pump, CommandBlocking, CommandInfo, Component,
DiffComponent, DrawableComponent, EventState, ScrollType,
},
keys::SharedKeyConfig,
queue::{InternalEvent, NeedsUpdate, Queue},
strings,
ui::{draw_scrollbar, style::SharedTheme},
};
use anyhow::Result;
use asyncgit::{
sync::{
diff::DiffOptions, diff_contains_file, get_commits_info,
CommitId, RepoPathRef,
},
AsyncDiff, AsyncGitNotification, AsyncLog, DiffParams, DiffType,
FetchStatus,
};
use chrono::{DateTime, Local};
use crossbeam_channel::Sender;
use crossterm::event::Event;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
text::{Span, Spans, Text},
widgets::{Block, Borders, Cell, Clear, Row, Table, TableState},
Frame,
};
const SLICE_SIZE: usize = 1200;
///
pub struct FileRevlogComponent {
git_log: Option<AsyncLog>,
git_diff: AsyncDiff,
theme: SharedTheme,
queue: Queue,
sender: Sender<AsyncGitNotification>,
diff: DiffComponent,
visible: bool,
repo_path: RepoPathRef,
file_path: Option<String>,
table_state: std::cell::Cell<TableState>,
items: ItemBatch,
count_total: usize,
key_config: SharedKeyConfig,
current_width: std::cell::Cell<usize>,
current_height: std::cell::Cell<usize>,
}
impl FileRevlogComponent {
///
pub fn new(
repo_path: &RepoPathRef,
queue: &Queue,
sender: &Sender<AsyncGitNotification>,
theme: SharedTheme,
key_config: SharedKeyConfig,
) -> Self {
Self {
theme: theme.clone(),
queue: queue.clone(),
sender: sender.clone(),
diff: DiffComponent::new(
repo_path.clone(),
queue.clone(),
theme,
key_config.clone(),
true,
),
git_log: None,
git_diff: AsyncDiff::new(
repo_path.borrow().clone(),
sender,
),
visible: false,
repo_path: repo_path.clone(),
file_path: None,
table_state: std::cell::Cell::new(TableState::default()),
items: ItemBatch::default(),
count_total: 0,
key_config,
current_width: std::cell::Cell::new(0),
current_height: std::cell::Cell::new(0),
}
}
fn components_mut(&mut self) -> Vec<&mut dyn Component> {
vec![&mut self.diff]
}
///
pub fn open(&mut self, file_path: &str) -> Result<()> {
self.file_path = Some(file_path.into());
let filter = diff_contains_file(
self.repo_path.borrow().clone(),
file_path.into(),
);
self.git_log = Some(AsyncLog::new(
self.repo_path.borrow().clone(),
&self.sender,
Some(filter),
));
self.table_state.get_mut().select(Some(0));
self.show()?;
self.update()?;
Ok(())
}
///
pub fn any_work_pending(&self) -> bool {
self.git_diff.is_pending()
|| self
.git_log
.as_ref()
.map_or(false, AsyncLog::is_pending)
}
///
pub fn update(&mut self) -> Result<()> {
if let Some(ref mut git_log) = self.git_log {
let log_changed =
git_log.fetch()? == FetchStatus::Started;
let table_state = self.table_state.take();
let start = table_state.selected().unwrap_or(0);
self.table_state.set(table_state);
if self.items.needs_data(start, git_log.count()?)
|| log_changed
{
self.fetch_commits()?;
}
}
Ok(())
}
///
pub fn update_git(
&mut self,
event: AsyncGitNotification,
) -> Result<()> {
if self.visible {
match event {
AsyncGitNotification::CommitFiles
| AsyncGitNotification::Log => self.update()?,
AsyncGitNotification::Diff => self.update_diff()?,
_ => (),
}
}
Ok(())
}
pub fn update_diff(&mut self) -> Result<()> {
if self.is_visible() {
if let Some(commit_id) = self.selected_commit() {
if let Some(file_path) = &self.file_path {
let diff_params = DiffParams {
path: file_path.clone(),
diff_type: DiffType::Commit(commit_id),
options: DiffOptions::default(),
};
if let Some((params, last)) =
self.git_diff.last()?
{
if params == diff_params {
self.diff.update(
file_path.to_string(),
false,
last,
);
return Ok(());
}
}
self.git_diff.request(diff_params)?;
self.diff.clear(true);
return Ok(());
}
}
self.diff.clear(false);
}
Ok(())
}
fn fetch_commits(&mut self) -> Result<()> {
if let Some(git_log) = &mut self.git_log {
let table_state = self.table_state.take();
let start = table_state.selected().unwrap_or(0);
let commits = get_commits_info(
&self.repo_path.borrow(),
&git_log.get_slice(start, SLICE_SIZE)?,
self.current_width.get() as usize,
);
if let Ok(commits) = commits {
self.items.set_items(start, commits);
}
self.table_state.set(table_state);
self.count_total = git_log.count()?;
}
Ok(())
}
fn selected_commit(&self) -> Option<CommitId> {
let table_state = self.table_state.take();
let commit_id = table_state.selected().and_then(|selected| {
self.items
.iter()
.nth(selected)
.as_ref()
.map(|entry| entry.id)
});
self.table_state.set(table_state);
commit_id
}
fn can_focus_diff(&self) -> bool {
self.selected_commit().is_some()
}
fn get_title(&self) -> String {
self.file_path.as_ref().map_or(
"<no history available>".into(),
|file_path| {
strings::file_log_title(&self.key_config, file_path)
},
)
}
fn get_rows(&self, now: DateTime<Local>) -> Vec<Row> {
self.items
.iter()
.map(|entry| {
let spans = Spans::from(vec![
Span::styled(
entry.hash_short.to_string(),
self.theme.commit_hash(false),
),
Span::raw(" "),
Span::styled(
entry.time_to_string(now),
self.theme.commit_time(false),
),
Span::raw(" "),
Span::styled(
entry.author.to_string(),
self.theme.commit_author(false),
),
]);
let mut text = Text::from(spans);
text.extend(Text::raw(entry.msg.to_string()));
let cells = vec![Cell::from(""), Cell::from(text)];
Row::new(cells).height(2)
})
.collect()
}
fn get_max_selection(&mut self) -> usize {
self.git_log.as_mut().map_or(0, |log| {
log.count().unwrap_or(0).saturating_sub(1)
})
}
fn move_selection(&mut self, scroll_type: ScrollType) -> bool {
let mut table_state = self.table_state.take();
let old_selection = table_state.selected().unwrap_or(0);
let max_selection = self.get_max_selection();
let new_selection = match scroll_type {
ScrollType::Up => old_selection.saturating_sub(1),
ScrollType::Down => {
old_selection.saturating_add(1).min(max_selection)
}
ScrollType::Home => 0,
ScrollType::End => max_selection,
ScrollType::PageUp => old_selection.saturating_sub(
self.current_height.get().saturating_sub(2),
),
ScrollType::PageDown => old_selection
.saturating_add(
self.current_height.get().saturating_sub(2),
)
.min(max_selection),
};
let needs_update = new_selection != old_selection;
if needs_update {
self.queue.push(InternalEvent::Update(NeedsUpdate::DIFF));
}
table_state.select(Some(new_selection));
self.table_state.set(table_state);
needs_update
}
fn draw_revlog<B: Backend>(&self, f: &mut Frame<B>, area: Rect) {
let constraints = [
// type of change: (A)dded, (M)odified, (D)eleted
Constraint::Length(1),
// commit details
Constraint::Percentage(100),
];
let now = Local::now();
let title = self.get_title();
let rows = self.get_rows(now);
let table = Table::new(rows)
.widths(&constraints)
.column_spacing(1)
.highlight_style(self.theme.text(true, true))
.block(
Block::default()
.borders(Borders::ALL)
.title(Span::styled(
title,
self.theme.title(true),
))
.border_style(self.theme.block(true)),
);
let mut table_state = self.table_state.take();
f.render_widget(Clear, area);
f.render_stateful_widget(table, area, &mut table_state);
draw_scrollbar(
f,
area,
&self.theme,
self.count_total,
table_state.selected().unwrap_or(0),
);
self.table_state.set(table_state);
self.current_width.set(area.width.into());
self.current_height.set(area.height.into());
}
}
impl DrawableComponent for FileRevlogComponent {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
area: Rect,
) -> Result<()> {
if self.visible {
let percentages = if self.diff.focused() {
(30, 70)
} else {
(50, 50)
};
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage(percentages.0),
Constraint::Percentage(percentages.1),
]
.as_ref(),
)
.split(area);
f.render_widget(Clear, area);
self.draw_revlog(f, chunks[0]);
self.diff.draw(f, chunks[1])?;
}
Ok(())
}
}
impl Component for FileRevlogComponent {
fn event(&mut self, event: Event) -> Result<EventState> {
if self.is_visible() {
if event_pump(
event,
self.components_mut().as_mut_slice(),
)?
.is_consumed()
{
return Ok(EventState::Consumed);
}
if let Event::Key(key) = event {
if key == self.key_config.keys.exit_popup {
self.hide();
return Ok(EventState::Consumed);
} else if key == self.key_config.keys.focus_right
&& self.can_focus_diff()
{
self.diff.focus(true);
return Ok(EventState::Consumed);
} else if key == self.key_config.keys.focus_left {
if self.diff.focused() {
self.diff.focus(false);
} else {
self.hide();
}
return Ok(EventState::Consumed);
} else if key == self.key_config.keys.enter {
self.hide();
return self.selected_commit().map_or(
Ok(EventState::NotConsumed),
|id| {
self.queue.push(
InternalEvent::InspectCommit(
id, None,
),
);
Ok(EventState::Consumed)
},
);
}
let selection_changed =
if key == self.key_config.keys.move_up {
self.move_selection(ScrollType::Up)
} else if key == self.key_config.keys.move_down {
self.move_selection(ScrollType::Down)
} else if key == self.key_config.keys.shift_up
|| key == self.key_config.keys.home
{
self.move_selection(ScrollType::Home)
} else if key == self.key_config.keys.shift_down
|| key == self.key_config.keys.end
{
self.move_selection(ScrollType::End)
} else if key == self.key_config.keys.page_up {
self.move_selection(ScrollType::PageUp)
} else if key == self.key_config.keys.page_down {
self.move_selection(ScrollType::PageDown)
} else {
false
};
return Ok(selection_changed.into());
}
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)
}
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),
);
out.push(
CommandInfo::new(
strings::commands::log_details_toggle(
&self.key_config,
),
true,
self.selected_commit().is_some(),
)
.order(1),
);
out.push(CommandInfo::new(
strings::commands::diff_focus_right(&self.key_config),
self.can_focus_diff(),
!self.diff.focused(),
));
out.push(CommandInfo::new(
strings::commands::diff_focus_left(&self.key_config),
true,
self.diff.focused(),
));
}
visibility_blocking(self)
}
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

@ -12,6 +12,7 @@ mod diff;
mod externaleditor;
mod fetch;
mod file_find_popup;
mod file_revlog;
mod help;
mod inspect_commit;
mod msg;
@ -45,6 +46,7 @@ pub use diff::DiffComponent;
pub use externaleditor::ExternalEditorComponent;
pub use fetch::FetchComponent;
pub use file_find_popup::FileFindPopup;
pub use file_revlog::FileRevlogComponent;
pub use help::HelpComponent;
pub use inspect_commit::InspectCommitComponent;
pub use msg::MsgComponent;

View file

@ -132,13 +132,17 @@ impl RevisionFilesComponent {
}
fn blame(&self) -> bool {
self.tree.selected_file().map_or(false, |file| {
self.queue.push(InternalEvent::BlameFile(
file.full_path_str()
.strip_prefix("./")
.unwrap_or_default()
.to_string(),
));
self.selected_file_path().map_or(false, |path| {
self.queue.push(InternalEvent::BlameFile(path));
true
})
}
fn file_history(&self) -> bool {
self.selected_file_path().map_or(false, |path| {
self.queue.push(InternalEvent::OpenFileRevlog(path));
true
})
}
@ -157,15 +161,24 @@ impl RevisionFilesComponent {
}
}
fn selected_file(&self) -> Option<String> {
fn selected_file_path_with_prefix(&self) -> Option<String> {
self.tree
.selected_file()
.map(|file| file.full_path_str().to_string())
}
fn selected_file_path(&self) -> Option<String> {
self.tree.selected_file().map(|file| {
file.full_path_str()
.strip_prefix("./")
.unwrap_or_default()
.to_string()
})
}
fn selection_changed(&mut self) {
//TODO: retrieve TreeFile from tree datastructure
if let Some(file) = self.selected_file() {
if let Some(file) = self.selected_file_path_with_prefix() {
log::info!("selected: {:?}", file);
let path = Path::new(&file);
if let Some(item) =
@ -280,6 +293,16 @@ impl Component for RevisionFilesComponent {
self.tree.selected_file().is_some(),
true,
));
out.push(
CommandInfo::new(
strings::commands::open_file_history(
&self.key_config,
),
self.tree.selected_file().is_some(),
true,
)
.order(order::RARE_ACTION),
);
tree_nav_cmds(&self.tree, &self.key_config, out);
} else {
self.current_file.commands(out, force_all);
@ -304,6 +327,11 @@ impl Component for RevisionFilesComponent {
self.hide();
return Ok(EventState::Consumed);
}
} else if key == self.key_config.keys.file_history {
if self.file_history() {
self.hide();
return Ok(EventState::Consumed);
}
} else if key == self.key_config.keys.move_right {
if is_tree_focused {
self.focus = Focus::File;
@ -324,7 +352,9 @@ impl Component for RevisionFilesComponent {
return Ok(EventState::Consumed);
}
} else if key == self.key_config.keys.edit_file {
if let Some(file) = self.selected_file() {
if let Some(file) =
self.selected_file_path_with_prefix()
{
//Note: switch to status tab so its clear we are
// not altering a file inside a revision here
self.queue.push(InternalEvent::TabSwitchStatus);

View file

@ -398,6 +398,16 @@ impl Component for StatusTreeComponent {
)
.order(order::RARE_ACTION),
);
out.push(
CommandInfo::new(
strings::commands::open_file_history(
&self.key_config,
),
self.selection_file().is_some(),
self.focused || force_all,
)
.order(order::RARE_ACTION),
);
CommandBlocking::PassingOn
}
@ -416,6 +426,19 @@ impl Component for StatusTreeComponent {
}
_ => Ok(EventState::NotConsumed),
}
} else if e == self.key_config.keys.file_history {
match (&self.queue, self.selection_file()) {
(Some(queue), Some(status_item)) => {
queue.push(
InternalEvent::OpenFileRevlog(
status_item.path,
),
);
Ok(EventState::Consumed)
}
_ => Ok(EventState::NotConsumed),
}
} else if e == self.key_config.keys.move_down {
Ok(self
.move_selection(MoveSelection::Down)

View file

@ -39,6 +39,7 @@ pub struct KeysList {
pub shift_down: KeyEvent,
pub enter: KeyEvent,
pub blame: KeyEvent,
pub file_history: KeyEvent,
pub edit_file: KeyEvent,
pub status_stage_all: KeyEvent,
pub status_reset_item: KeyEvent,
@ -116,6 +117,7 @@ impl Default for KeysList {
shift_down: KeyEvent { code: KeyCode::Down, modifiers: KeyModifiers::SHIFT},
enter: KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::empty()},
blame: KeyEvent { code: KeyCode::Char('B'), modifiers: KeyModifiers::SHIFT},
file_history: KeyEvent { code: KeyCode::Char('H'), modifiers: KeyModifiers::SHIFT},
edit_file: KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::empty()},
status_stage_all: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::empty()},
status_reset_item: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT},

View file

@ -44,6 +44,7 @@ pub struct KeysListFile {
pub enter: Option<KeyEvent>,
pub blame: Option<KeyEvent>,
pub edit_file: Option<KeyEvent>,
pub file_history: Option<KeyEvent>,
pub status_stage_all: Option<KeyEvent>,
pub status_reset_item: Option<KeyEvent>,
pub status_ignore_file: Option<KeyEvent>,
@ -130,6 +131,7 @@ impl KeysListFile {
enter: self.enter.unwrap_or(default.enter),
blame: self.blame.unwrap_or(default.blame),
edit_file: self.edit_file.unwrap_or(default.edit_file),
file_history: self.file_history.unwrap_or(default.file_history),
status_stage_all: self.status_stage_all.unwrap_or(default.status_stage_all),
status_reset_item: self.status_reset_item.unwrap_or(default.status_reset_item),
status_ignore_file: self.status_ignore_file.unwrap_or(default.status_ignore_file),

View file

@ -81,6 +81,8 @@ pub enum InternalEvent {
///
BlameFile(String),
///
OpenFileRevlog(String),
///
CreateBranch,
///
RenameBranch(String, String),

View file

@ -269,6 +269,12 @@ pub fn confirm_msg_force_push(
pub fn log_title(_key_config: &SharedKeyConfig) -> String {
"Commit".to_string()
}
pub fn file_log_title(
_key_config: &SharedKeyConfig,
file_path: &str,
) -> String {
format!("Commits for file {}", file_path)
}
pub fn blame_title(_key_config: &SharedKeyConfig) -> String {
"Blame".to_string()
}
@ -1054,6 +1060,18 @@ pub mod commands {
CMD_GROUP_LOG,
)
}
pub fn open_file_history(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"History [{}]",
key_config.get_hint(key_config.keys.file_history),
),
"open history of selected file",
CMD_GROUP_LOG,
)
}
pub fn log_tag_commit(
key_config: &SharedKeyConfig,
) -> CommandText {