mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
Add popup for file history (#841)
This commit is contained in:
parent
3e72539ca1
commit
b622ceef94
15 changed files with 713 additions and 79 deletions
|
|
@ -78,6 +78,8 @@ pub enum AsyncGitNotification {
|
|||
///
|
||||
Log,
|
||||
///
|
||||
FileLog,
|
||||
///
|
||||
CommitFiles,
|
||||
///
|
||||
Tags,
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
28
src/app.rs
28
src/app.rs
|
|
@ -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()?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
531
src/components/file_revlog.rs
Normal file
531
src/components/file_revlog.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ pub enum InternalEvent {
|
|||
///
|
||||
BlameFile(String),
|
||||
///
|
||||
OpenFileRevlog(String),
|
||||
///
|
||||
CreateBranch,
|
||||
///
|
||||
RenameBranch(String, String),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue