mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 00:48:35 +00:00
Log view (#41)
This commit is contained in:
parent
4df7704ef5
commit
1db1f00302
15 changed files with 818 additions and 148 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -286,6 +286,7 @@ dependencies = [
|
|||
"asyncgit",
|
||||
"backtrace",
|
||||
"bitflags",
|
||||
"chrono",
|
||||
"crossbeam-channel",
|
||||
"crossterm",
|
||||
"dirs",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ dirs = "2.0"
|
|||
crossbeam-channel = "0.4"
|
||||
scopeguard = "1.1"
|
||||
bitflags = "1.2"
|
||||
chrono = "0.4"
|
||||
backtrace = { version = "0.3" }
|
||||
scopetime = { path = "./scopetime", version = "0.1" }
|
||||
asyncgit = { path = "./asyncgit", version = "0.2" }
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -37,4 +37,4 @@ install:
|
|||
cargo install --path "."
|
||||
|
||||
install-debug:
|
||||
cargo install --features=timing --path "."
|
||||
cargo install --features=timing --path "." --offline
|
||||
|
|
@ -5,11 +5,13 @@
|
|||
#![deny(clippy::all)]
|
||||
|
||||
mod diff;
|
||||
mod revlog;
|
||||
mod status;
|
||||
pub mod sync;
|
||||
|
||||
pub use crate::{
|
||||
diff::{AsyncDiff, DiffParams},
|
||||
revlog::AsyncLog,
|
||||
status::AsyncStatus,
|
||||
sync::{
|
||||
diff::{DiffLine, DiffLineType, FileDiff},
|
||||
|
|
@ -30,6 +32,8 @@ pub enum AsyncNotification {
|
|||
Status,
|
||||
///
|
||||
Diff,
|
||||
///
|
||||
Log,
|
||||
}
|
||||
|
||||
/// current working director `./`
|
||||
|
|
|
|||
105
asyncgit/src/revlog.rs
Normal file
105
asyncgit/src/revlog.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use crate::{sync, AsyncNotification, CWD};
|
||||
use crossbeam_channel::Sender;
|
||||
use git2::Oid;
|
||||
use scopetime::scope_time;
|
||||
use std::{
|
||||
iter::FromIterator,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
use sync::{utils::repo, LogWalker};
|
||||
|
||||
///
|
||||
pub struct AsyncLog {
|
||||
current: Arc<Mutex<Vec<Oid>>>,
|
||||
sender: Sender<AsyncNotification>,
|
||||
pending: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
static LIMIT_COUNT: usize = 1000;
|
||||
|
||||
impl AsyncLog {
|
||||
///
|
||||
pub fn new(sender: Sender<AsyncNotification>) -> Self {
|
||||
Self {
|
||||
current: Arc::new(Mutex::new(Vec::new())),
|
||||
sender,
|
||||
pending: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn count(&mut self) -> usize {
|
||||
self.current.lock().unwrap().len()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_slice(
|
||||
&self,
|
||||
start_index: usize,
|
||||
amount: usize,
|
||||
) -> Vec<Oid> {
|
||||
let list = self.current.lock().unwrap();
|
||||
let list_len = list.len();
|
||||
let min = start_index.min(list_len);
|
||||
let max = min + amount;
|
||||
let max = max.min(list_len);
|
||||
Vec::from_iter(list[min..max].iter().cloned())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn fetch(&mut self) {
|
||||
if !self.is_pending() {
|
||||
self.clear();
|
||||
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
rayon_core::spawn(move || {
|
||||
arc_pending.store(true, Ordering::Relaxed);
|
||||
|
||||
scope_time!("async::revlog");
|
||||
|
||||
let mut entries = Vec::with_capacity(LIMIT_COUNT);
|
||||
let r = repo(CWD);
|
||||
let mut walker = LogWalker::new(&r);
|
||||
loop {
|
||||
entries.clear();
|
||||
let res_is_err = walker
|
||||
.read(&mut entries, LIMIT_COUNT)
|
||||
.is_err();
|
||||
|
||||
if !res_is_err {
|
||||
let mut current = arc_current.lock().unwrap();
|
||||
current.extend(entries.iter());
|
||||
}
|
||||
|
||||
if res_is_err || entries.len() <= 1 {
|
||||
break;
|
||||
} else {
|
||||
Self::notify(&sender);
|
||||
}
|
||||
}
|
||||
|
||||
arc_pending.store(false, Ordering::Relaxed);
|
||||
|
||||
Self::notify(&sender);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.current.lock().unwrap().clear();
|
||||
}
|
||||
|
||||
fn notify(sender: &Sender<AsyncNotification>) {
|
||||
sender.send(AsyncNotification::Log).expect("error sending");
|
||||
}
|
||||
}
|
||||
101
asyncgit/src/sync/commits_info.rs
Normal file
101
asyncgit/src/sync/commits_info.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use super::utils::repo;
|
||||
use git2::{Commit, Error, Oid};
|
||||
use scopetime::scope_time;
|
||||
|
||||
///
|
||||
#[derive(Debug)]
|
||||
pub struct CommitInfo {
|
||||
///
|
||||
pub message: String,
|
||||
///
|
||||
pub time: i64,
|
||||
///
|
||||
pub author: String,
|
||||
///
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_commits_info(
|
||||
repo_path: &str,
|
||||
ids: &[Oid],
|
||||
) -> Result<Vec<CommitInfo>, Error> {
|
||||
scope_time!("get_commits_info");
|
||||
|
||||
let repo = repo(repo_path);
|
||||
|
||||
let commits = ids.iter().map(|id| repo.find_commit(*id).unwrap());
|
||||
|
||||
let res = commits
|
||||
.map(|c: Commit| {
|
||||
let message = get_message(&c);
|
||||
let author = if let Some(name) = c.author().name() {
|
||||
String::from(name)
|
||||
} else {
|
||||
String::from("<unknown>")
|
||||
};
|
||||
CommitInfo {
|
||||
message,
|
||||
author,
|
||||
time: c.time().seconds(),
|
||||
hash: c.id().to_string(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn get_message(c: &Commit) -> String {
|
||||
if let Some(msg) = c.message() {
|
||||
limit_str(msg, 50)
|
||||
} else {
|
||||
String::from("<unknown>")
|
||||
}
|
||||
}
|
||||
|
||||
fn limit_str(s: &str, limit: usize) -> String {
|
||||
if let Some(first) = s.lines().next() {
|
||||
first.chars().take(limit).collect::<String>()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::get_commits_info;
|
||||
use crate::sync::{
|
||||
commit, stage_add_file, tests::repo_init_empty,
|
||||
};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Error, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_log() -> Result<(), Error> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path);
|
||||
let c1 = commit(repo_path, "commit1");
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path);
|
||||
let c2 = commit(repo_path, "commit2");
|
||||
|
||||
let res = get_commits_info(repo_path, &vec![c2, c1]).unwrap();
|
||||
|
||||
assert_eq!(res.len(), 2);
|
||||
assert_eq!(res[0].message.as_str(), "commit2");
|
||||
assert_eq!(res[0].author.as_str(), "name");
|
||||
assert_eq!(res[1].message.as_str(), "commit1");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
117
asyncgit/src/sync/logwalker.rs
Normal file
117
asyncgit/src/sync/logwalker.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
use git2::{Error, Oid, Repository, Revwalk};
|
||||
|
||||
///
|
||||
pub struct LogWalker<'a> {
|
||||
repo: &'a Repository,
|
||||
revwalk: Option<Revwalk<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> LogWalker<'a> {
|
||||
///
|
||||
pub fn new(repo: &'a Repository) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
revwalk: None,
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn read(
|
||||
&mut self,
|
||||
out: &mut Vec<Oid>,
|
||||
limit: usize,
|
||||
) -> Result<usize, Error> {
|
||||
let mut count = 0_usize;
|
||||
|
||||
if self.revwalk.is_none() {
|
||||
let mut walk = self.repo.revwalk()?;
|
||||
walk.push_head()?;
|
||||
self.revwalk = Some(walk);
|
||||
}
|
||||
|
||||
if let Some(ref mut walk) = self.revwalk {
|
||||
for id in walk {
|
||||
if let Ok(id) = id {
|
||||
out.push(id);
|
||||
count += 1;
|
||||
|
||||
if count == limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
commit, get_commits_info, stage_add_file,
|
||||
tests::repo_init_empty,
|
||||
};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Error, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_limit() -> Result<(), Error> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path);
|
||||
commit(repo_path, "commit1");
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path);
|
||||
let oid2 = commit(repo_path, "commit2");
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut walk = LogWalker::new(&repo);
|
||||
walk.read(&mut items, 1).unwrap();
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0], oid2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logwalker() -> Result<(), Error> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path);
|
||||
commit(repo_path, "commit1");
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path);
|
||||
let oid2 = commit(repo_path, "commit2");
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut walk = LogWalker::new(&repo);
|
||||
walk.read(&mut items, 100).unwrap();
|
||||
|
||||
let info = get_commits_info(repo_path, &items).unwrap();
|
||||
dbg!(&info);
|
||||
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(items[0], oid2);
|
||||
|
||||
let mut items = Vec::new();
|
||||
walk.read(&mut items, 100).unwrap();
|
||||
|
||||
assert_eq!(items.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,18 @@
|
|||
//! sync git api
|
||||
|
||||
mod commits_info;
|
||||
pub mod diff;
|
||||
mod hooks;
|
||||
mod hunks;
|
||||
mod logwalker;
|
||||
mod reset;
|
||||
pub mod status;
|
||||
pub mod utils;
|
||||
|
||||
pub use commits_info::{get_commits_info, CommitInfo};
|
||||
pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult};
|
||||
pub use hunks::{stage_hunk, unstage_hunk};
|
||||
pub use logwalker::LogWalker;
|
||||
pub use reset::{
|
||||
reset_stage, reset_workdir_file, reset_workdir_folder,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! sync git api (various methods)
|
||||
|
||||
use git2::{IndexAddOption, Repository, RepositoryOpenFlags};
|
||||
use git2::{IndexAddOption, Oid, Repository, RepositoryOpenFlags};
|
||||
use scopetime::scope_time;
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ pub fn repo(repo_path: &str) -> Repository {
|
|||
}
|
||||
|
||||
/// this does not run any git hooks
|
||||
pub fn commit(repo_path: &str, msg: &str) {
|
||||
pub fn commit(repo_path: &str, msg: &str) -> Oid {
|
||||
scope_time!("commit");
|
||||
|
||||
let repo = repo(repo_path);
|
||||
|
|
@ -59,7 +59,7 @@ pub fn commit(repo_path: &str, msg: &str) {
|
|||
&tree,
|
||||
parents.as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// add a file diff from workingdir to stage (will not add removed files see `stage_addremoved`)
|
||||
|
|
|
|||
296
src/app.rs
296
src/app.rs
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
keys,
|
||||
queue::{InternalEvent, NeedsUpdate, Queue},
|
||||
strings,
|
||||
tabs::Revlog,
|
||||
};
|
||||
use asyncgit::{
|
||||
current_tick, sync, AsyncDiff, AsyncNotification, AsyncStatus,
|
||||
|
|
@ -75,13 +76,15 @@ pub struct App {
|
|||
git_diff: AsyncDiff,
|
||||
git_status: AsyncStatus,
|
||||
current_commands: Vec<CommandInfo>,
|
||||
tab: usize,
|
||||
revlog: Revlog,
|
||||
queue: Queue,
|
||||
}
|
||||
|
||||
// public interface
|
||||
impl App {
|
||||
///
|
||||
pub fn new(sender: Sender<AsyncNotification>) -> Self {
|
||||
pub fn new(sender: &Sender<AsyncNotification>) -> Self {
|
||||
let queue = Queue::default();
|
||||
Self {
|
||||
focus: Focus::WorkDir,
|
||||
|
|
@ -105,8 +108,10 @@ impl App {
|
|||
diff: DiffComponent::new(queue.clone()),
|
||||
msg: MsgComponent::default(),
|
||||
git_diff: AsyncDiff::new(sender.clone()),
|
||||
git_status: AsyncStatus::new(sender),
|
||||
git_status: AsyncStatus::new(sender.clone()),
|
||||
current_commands: Vec::new(),
|
||||
tab: 0,
|
||||
revlog: Revlog::new(&sender),
|
||||
queue,
|
||||
}
|
||||
}
|
||||
|
|
@ -128,52 +133,19 @@ impl App {
|
|||
f.render_widget(
|
||||
Tabs::default()
|
||||
.block(Block::default().borders(Borders::BOTTOM))
|
||||
.titles(&[strings::TAB_STATUS])
|
||||
.titles(&[strings::TAB_STATUS, strings::TAB_LOG])
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.divider(strings::TAB_DIVIDER),
|
||||
.divider(strings::TAB_DIVIDER)
|
||||
.select(self.tab),
|
||||
chunks_main[0],
|
||||
);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
if self.focus == Focus::Diff {
|
||||
[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(70),
|
||||
]
|
||||
} else {
|
||||
[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]
|
||||
}
|
||||
.as_ref(),
|
||||
)
|
||||
.split(chunks_main[1]);
|
||||
|
||||
let left_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
if self.diff_target == DiffTarget::WorkingDir {
|
||||
[
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Percentage(40),
|
||||
]
|
||||
} else {
|
||||
[
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(60),
|
||||
]
|
||||
}
|
||||
.as_ref(),
|
||||
)
|
||||
.split(chunks[0]);
|
||||
|
||||
self.index_wd.draw(f, left_chunks[0]);
|
||||
self.index.draw(f, left_chunks[1]);
|
||||
self.diff.draw(f, chunks[1]);
|
||||
if self.tab == 0 {
|
||||
self.draw_status_tab(f, chunks_main[1]);
|
||||
} else {
|
||||
self.revlog.draw(f, chunks_main[1]);
|
||||
}
|
||||
|
||||
Self::draw_commands(
|
||||
f,
|
||||
|
|
@ -190,8 +162,13 @@ impl App {
|
|||
|
||||
let mut flags = NeedsUpdate::empty();
|
||||
|
||||
if Self::event_pump(ev, self.components_mut().as_mut_slice())
|
||||
{
|
||||
let event_used = if self.tab == 0 {
|
||||
Self::event_pump(ev, self.components_mut().as_mut_slice())
|
||||
} else {
|
||||
self.revlog.event(ev)
|
||||
};
|
||||
|
||||
if event_used {
|
||||
flags.insert(NeedsUpdate::COMMANDS);
|
||||
} else if let Event::Key(k) = ev {
|
||||
let new_flags = match k {
|
||||
|
|
@ -219,6 +196,10 @@ impl App {
|
|||
self.commit.show();
|
||||
NeedsUpdate::COMMANDS
|
||||
}
|
||||
keys::TAB_TOGGLE => {
|
||||
self.toggle_tabs();
|
||||
NeedsUpdate::COMMANDS
|
||||
}
|
||||
_ => NeedsUpdate::empty(),
|
||||
};
|
||||
|
||||
|
|
@ -253,6 +234,7 @@ impl App {
|
|||
match ev {
|
||||
AsyncNotification::Diff => self.update_diff(),
|
||||
AsyncNotification::Status => self.update_status(),
|
||||
AsyncNotification::Log => self.revlog.update(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +245,9 @@ impl App {
|
|||
|
||||
///
|
||||
pub fn any_work_pending(&self) -> bool {
|
||||
self.git_diff.is_pending() || self.git_status.is_pending()
|
||||
self.git_diff.is_pending()
|
||||
|| self.git_status.is_pending()
|
||||
|| self.revlog.any_work_pending()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -314,6 +298,17 @@ impl App {
|
|||
None
|
||||
}
|
||||
|
||||
fn toggle_tabs(&mut self) {
|
||||
self.tab += 1;
|
||||
self.tab %= 2;
|
||||
|
||||
if self.tab == 1 {
|
||||
self.revlog.show();
|
||||
} else {
|
||||
self.revlog.hide();
|
||||
}
|
||||
}
|
||||
|
||||
fn can_focus_diff(&self) -> bool {
|
||||
match self.focus {
|
||||
Focus::WorkDir => self.index_wd.is_file_seleted(),
|
||||
|
|
@ -401,92 +396,33 @@ impl App {
|
|||
fn commands(&self, force_all: bool) -> Vec<CommandInfo> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
for c in self.components() {
|
||||
if c.commands(&mut res, force_all)
|
||||
!= CommandBlocking::PassingOn
|
||||
&& !force_all
|
||||
{
|
||||
break;
|
||||
if self.revlog.is_visible() {
|
||||
self.revlog.commands(&mut res, force_all);
|
||||
} else {
|
||||
for c in self.components() {
|
||||
if c.commands(&mut res, force_all)
|
||||
!= CommandBlocking::PassingOn
|
||||
&& !force_all
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: move into status tab component
|
||||
self.add_commands_status_tab(
|
||||
&mut res,
|
||||
!self.any_popup_visible(),
|
||||
);
|
||||
}
|
||||
|
||||
let main_cmds_available = !self.any_popup_visible();
|
||||
|
||||
{
|
||||
{
|
||||
let focus_on_stage = self.focus == Focus::Stage;
|
||||
let focus_not_diff = self.focus != Focus::Diff;
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::STATUS_FOCUS_UNSTAGED,
|
||||
true,
|
||||
main_cmds_available
|
||||
&& focus_on_stage
|
||||
&& !focus_not_diff,
|
||||
)
|
||||
.hidden(),
|
||||
);
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::STATUS_FOCUS_STAGED,
|
||||
true,
|
||||
main_cmds_available
|
||||
&& !focus_on_stage
|
||||
&& !focus_not_diff,
|
||||
)
|
||||
.hidden(),
|
||||
);
|
||||
}
|
||||
{
|
||||
let focus_on_diff = self.focus == Focus::Diff;
|
||||
res.push(CommandInfo::new(
|
||||
commands::STATUS_FOCUS_LEFT,
|
||||
true,
|
||||
main_cmds_available && focus_on_diff,
|
||||
));
|
||||
res.push(CommandInfo::new(
|
||||
commands::STATUS_FOCUS_RIGHT,
|
||||
self.can_focus_diff(),
|
||||
main_cmds_available && !focus_on_diff,
|
||||
));
|
||||
}
|
||||
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::COMMIT_OPEN,
|
||||
!self.index.is_empty(),
|
||||
self.offer_open_commit_cmd(),
|
||||
)
|
||||
.order(-1),
|
||||
);
|
||||
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::SELECT_STAGING,
|
||||
true,
|
||||
self.focus == Focus::WorkDir,
|
||||
)
|
||||
.order(-2),
|
||||
);
|
||||
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::SELECT_UNSTAGED,
|
||||
true,
|
||||
self.focus == Focus::Stage,
|
||||
)
|
||||
.order(-2),
|
||||
);
|
||||
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::QUIT,
|
||||
true,
|
||||
main_cmds_available,
|
||||
)
|
||||
.order(100),
|
||||
);
|
||||
}
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::QUIT,
|
||||
true,
|
||||
!self.any_popup_visible(),
|
||||
)
|
||||
.order(100),
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
|
|
@ -509,6 +445,62 @@ impl App {
|
|||
false
|
||||
}
|
||||
|
||||
fn add_commands_status_tab(
|
||||
&self,
|
||||
res: &mut Vec<CommandInfo>,
|
||||
main_cmds_available: bool,
|
||||
) {
|
||||
{
|
||||
let focus_on_diff = self.focus == Focus::Diff;
|
||||
res.push(CommandInfo::new(
|
||||
commands::STATUS_FOCUS_LEFT,
|
||||
true,
|
||||
main_cmds_available && focus_on_diff,
|
||||
));
|
||||
res.push(CommandInfo::new(
|
||||
commands::STATUS_FOCUS_RIGHT,
|
||||
self.can_focus_diff(),
|
||||
main_cmds_available && !focus_on_diff,
|
||||
));
|
||||
}
|
||||
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::COMMIT_OPEN,
|
||||
!self.index.is_empty(),
|
||||
main_cmds_available && self.offer_open_commit_cmd(),
|
||||
)
|
||||
.order(-1),
|
||||
);
|
||||
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::SELECT_STATUS,
|
||||
true,
|
||||
main_cmds_available && self.focus == Focus::Diff,
|
||||
)
|
||||
.hidden(),
|
||||
);
|
||||
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::SELECT_STAGING,
|
||||
true,
|
||||
main_cmds_available && self.focus == Focus::WorkDir,
|
||||
)
|
||||
.order(-2),
|
||||
);
|
||||
|
||||
res.push(
|
||||
CommandInfo::new(
|
||||
commands::SELECT_UNSTAGED,
|
||||
true,
|
||||
main_cmds_available && self.focus == Focus::Stage,
|
||||
)
|
||||
.order(-2),
|
||||
);
|
||||
}
|
||||
|
||||
fn any_popup_visible(&self) -> bool {
|
||||
self.commit.is_visible()
|
||||
|| self.help.is_visible()
|
||||
|
|
@ -525,6 +517,52 @@ impl App {
|
|||
self.msg.draw(f, size);
|
||||
}
|
||||
|
||||
fn draw_status_tab<B: Backend>(
|
||||
&self,
|
||||
f: &mut Frame<B>,
|
||||
area: Rect,
|
||||
) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
if self.focus == Focus::Diff {
|
||||
[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(70),
|
||||
]
|
||||
} else {
|
||||
[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]
|
||||
}
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
|
||||
let left_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
if self.diff_target == DiffTarget::WorkingDir {
|
||||
[
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Percentage(40),
|
||||
]
|
||||
} else {
|
||||
[
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(60),
|
||||
]
|
||||
}
|
||||
.as_ref(),
|
||||
)
|
||||
.split(chunks[0]);
|
||||
|
||||
self.index_wd.draw(f, left_chunks[0]);
|
||||
self.index.draw(f, left_chunks[1]);
|
||||
self.diff.draw(f, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_commands<B: Backend>(
|
||||
f: &mut Frame<B>,
|
||||
r: Rect,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const fn with_mod(
|
|||
KeyEvent { code, modifiers }
|
||||
}
|
||||
|
||||
pub const TAB_TOGGLE: KeyEvent = no_mod(KeyCode::Tab);
|
||||
pub const FOCUS_WORKDIR: KeyEvent = no_mod(KeyCode::Char('1'));
|
||||
pub const FOCUS_STAGE: KeyEvent = no_mod(KeyCode::Char('2'));
|
||||
pub const FOCUS_RIGHT: KeyEvent = no_mod(KeyCode::Right);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ mod poll;
|
|||
mod queue;
|
||||
mod spinner;
|
||||
mod strings;
|
||||
mod tabs;
|
||||
mod ui;
|
||||
mod version;
|
||||
|
||||
|
|
@ -59,13 +60,13 @@ fn main() -> Result<()> {
|
|||
disable_raw_mode().unwrap();
|
||||
}
|
||||
|
||||
set_panic_handlers();
|
||||
|
||||
let mut terminal = start_terminal(io::stdout())?;
|
||||
|
||||
let (tx_git, rx_git) = unbounded();
|
||||
|
||||
let mut app = App::new(tx_git);
|
||||
|
||||
set_panic_handlers();
|
||||
let mut app = App::new(&tx_git);
|
||||
|
||||
let rx_input = poll::start_polling_thread();
|
||||
let ticker = tick(TICK_INTERVAL);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ pub static TITLE_DIFF: &str = "Diff";
|
|||
pub static TITLE_INDEX: &str = "Staged Changes [2]";
|
||||
|
||||
pub static TAB_STATUS: &str = "Status";
|
||||
pub static TAB_LOG: &str = "Log";
|
||||
pub static TAB_DIVIDER: &str = " | ";
|
||||
|
||||
pub static CMD_SPLITTER: &str = " ";
|
||||
|
|
@ -73,6 +74,12 @@ pub mod commands {
|
|||
CMD_GROUP_GENERAL,
|
||||
);
|
||||
///
|
||||
pub static SELECT_STATUS: CommandText = CommandText::new(
|
||||
"Focus Files [1,2]",
|
||||
"focus/select file tree of staged or unstaged files",
|
||||
CMD_GROUP_GENERAL,
|
||||
);
|
||||
///
|
||||
pub static SELECT_UNSTAGED: CommandText = CommandText::new(
|
||||
"Focus Unstaged [1]",
|
||||
"focus/select unstaged area",
|
||||
|
|
@ -108,18 +115,7 @@ pub mod commands {
|
|||
"revert changes in selected file or entire path",
|
||||
CMD_GROUP_CHANGES,
|
||||
);
|
||||
///
|
||||
pub static STATUS_FOCUS_UNSTAGED: CommandText = CommandText::new(
|
||||
"Unstaged [1]",
|
||||
"view changes in working dir",
|
||||
CMD_GROUP_GENERAL,
|
||||
);
|
||||
///
|
||||
pub static STATUS_FOCUS_STAGED: CommandText = CommandText::new(
|
||||
"Staged [2]",
|
||||
"view staged changes",
|
||||
CMD_GROUP_GENERAL,
|
||||
);
|
||||
|
||||
///
|
||||
pub static STATUS_FOCUS_LEFT: CommandText = CommandText::new(
|
||||
"Back [\u{2190}]", //←
|
||||
|
|
|
|||
5
src/tabs/mod.rs
Normal file
5
src/tabs/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod revlog;
|
||||
|
||||
//TODO: tab traits?
|
||||
|
||||
pub use revlog::Revlog;
|
||||
296
src/tabs/revlog.rs
Normal file
296
src/tabs/revlog.rs
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
use crate::{
|
||||
components::{CommandBlocking, CommandInfo, Component},
|
||||
keys,
|
||||
strings::commands,
|
||||
};
|
||||
use asyncgit::{sync, AsyncLog, AsyncNotification, CWD};
|
||||
use chrono::prelude::*;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossterm::event::Event;
|
||||
use std::{borrow::Cow, cmp, convert::TryFrom, time::Instant};
|
||||
use sync::CommitInfo;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Paragraph, Text},
|
||||
Frame,
|
||||
};
|
||||
|
||||
struct LogEntry {
|
||||
time: String,
|
||||
author: String,
|
||||
msg: String,
|
||||
hash: String,
|
||||
}
|
||||
|
||||
impl From<&CommitInfo> for LogEntry {
|
||||
fn from(c: &CommitInfo) -> Self {
|
||||
let time =
|
||||
DateTime::<Local>::from(DateTime::<Utc>::from_utc(
|
||||
NaiveDateTime::from_timestamp(c.time, 0),
|
||||
Utc,
|
||||
));
|
||||
Self {
|
||||
author: c.author.clone(),
|
||||
msg: c.message.clone(),
|
||||
time: time.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
hash: c.hash[0..7].to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const COLOR_SELECTION_BG: Color = Color::Blue;
|
||||
|
||||
const STYLE_HASH: Style = Style::new().fg(Color::Magenta);
|
||||
const STYLE_TIME: Style = Style::new().fg(Color::Blue);
|
||||
const STYLE_AUTHOR: Style = Style::new().fg(Color::Green);
|
||||
const STYLE_MSG: Style = Style::new().fg(Color::Reset);
|
||||
|
||||
const STYLE_HASH_SELECTED: Style =
|
||||
Style::new().fg(Color::Magenta).bg(COLOR_SELECTION_BG);
|
||||
const STYLE_TIME_SELECTED: Style =
|
||||
Style::new().fg(Color::White).bg(COLOR_SELECTION_BG);
|
||||
const STYLE_AUTHOR_SELECTED: Style =
|
||||
Style::new().fg(Color::Green).bg(COLOR_SELECTION_BG);
|
||||
const STYLE_MSG_SELECTED: Style =
|
||||
Style::new().fg(Color::Reset).bg(COLOR_SELECTION_BG);
|
||||
|
||||
static ELEMENTS_PER_LINE: usize = 8;
|
||||
static SLICE_SIZE: usize = 1000;
|
||||
static SLICE_OFFSET_RELOAD_THRESHOLD: usize = 100;
|
||||
|
||||
///
|
||||
pub struct Revlog {
|
||||
selection: usize,
|
||||
selection_max: usize,
|
||||
items: Vec<LogEntry>,
|
||||
git_log: AsyncLog,
|
||||
visible: bool,
|
||||
first_open_done: bool,
|
||||
scroll_state: (Instant, f32),
|
||||
}
|
||||
|
||||
impl Revlog {
|
||||
///
|
||||
pub fn new(sender: &Sender<AsyncNotification>) -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
git_log: AsyncLog::new(sender.clone()),
|
||||
selection: 0,
|
||||
selection_max: 0,
|
||||
visible: false,
|
||||
first_open_done: false,
|
||||
scroll_state: (Instant::now(), 0_f32),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn draw<B: Backend>(&self, f: &mut Frame<B>, area: Rect) {
|
||||
let height = area.height as usize;
|
||||
let selection = self.selection;
|
||||
let height_d2 = height as usize / 2;
|
||||
let min = selection.saturating_sub(height_d2);
|
||||
|
||||
let mut txt = Vec::new();
|
||||
for (idx, e) in self.items.iter().enumerate() {
|
||||
Self::add_entry(e, idx == selection, &mut txt);
|
||||
}
|
||||
|
||||
let title =
|
||||
format!("commit {}/{}", selection, self.selection_max);
|
||||
|
||||
f.render_widget(
|
||||
Paragraph::new(
|
||||
txt.iter()
|
||||
.skip(min * ELEMENTS_PER_LINE)
|
||||
.take(height * ELEMENTS_PER_LINE),
|
||||
)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(title.as_str()),
|
||||
)
|
||||
.alignment(Alignment::Left),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
pub fn any_work_pending(&self) -> bool {
|
||||
self.git_log.is_pending()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update(&mut self) {
|
||||
let next_idx = self.items.len();
|
||||
|
||||
let requires_more_data = next_idx
|
||||
.saturating_sub(self.selection)
|
||||
< SLICE_OFFSET_RELOAD_THRESHOLD;
|
||||
|
||||
self.selection_max = self.git_log.count().saturating_sub(1);
|
||||
|
||||
if requires_more_data {
|
||||
let commits = sync::get_commits_info(
|
||||
CWD,
|
||||
&self.git_log.get_slice(next_idx, SLICE_SIZE),
|
||||
);
|
||||
|
||||
if let Ok(commits) = commits {
|
||||
self.items.extend(commits.iter().map(LogEntry::from));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, up: bool) {
|
||||
self.update_scroll_speed();
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let speed_int = usize::try_from(self.scroll_state.1 as i64)
|
||||
.unwrap()
|
||||
.max(1);
|
||||
|
||||
if up {
|
||||
self.selection = self.selection.saturating_sub(speed_int);
|
||||
} else {
|
||||
self.selection = self.selection.saturating_add(speed_int);
|
||||
}
|
||||
|
||||
self.selection = cmp::min(self.selection, self.selection_max);
|
||||
|
||||
self.update();
|
||||
}
|
||||
|
||||
fn update_scroll_speed(&mut self) {
|
||||
const REPEATED_SCROLL_THRESHOLD_MILLIS: u128 = 300;
|
||||
const SCROLL_SPEED_START: f32 = 0.1_f32;
|
||||
const SCROLL_SPEED_MAX: f32 = 10_f32;
|
||||
const SCROLL_SPEED_MULTIPLIER: f32 = 1.05_f32;
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
let since_last_scroll =
|
||||
now.duration_since(self.scroll_state.0);
|
||||
|
||||
self.scroll_state.0 = now;
|
||||
|
||||
let speed = if since_last_scroll.as_millis()
|
||||
< REPEATED_SCROLL_THRESHOLD_MILLIS
|
||||
{
|
||||
self.scroll_state.1 * SCROLL_SPEED_MULTIPLIER
|
||||
} else {
|
||||
SCROLL_SPEED_START
|
||||
};
|
||||
|
||||
self.scroll_state.1 = speed.min(SCROLL_SPEED_MAX);
|
||||
}
|
||||
|
||||
fn add_entry<'a>(
|
||||
e: &'a LogEntry,
|
||||
selected: bool,
|
||||
txt: &mut Vec<Text<'a>>,
|
||||
) {
|
||||
let count_before = txt.len();
|
||||
|
||||
let splitter_txt = Cow::from(" ");
|
||||
let splitter = if selected {
|
||||
Text::Styled(
|
||||
splitter_txt,
|
||||
Style::new().bg(COLOR_SELECTION_BG),
|
||||
)
|
||||
} else {
|
||||
Text::Raw(splitter_txt)
|
||||
};
|
||||
|
||||
txt.push(Text::Styled(
|
||||
Cow::from(e.hash.as_str()),
|
||||
if selected {
|
||||
STYLE_HASH_SELECTED
|
||||
} else {
|
||||
STYLE_HASH
|
||||
},
|
||||
));
|
||||
txt.push(splitter.clone());
|
||||
txt.push(Text::Styled(
|
||||
Cow::from(e.time.as_str()),
|
||||
if selected {
|
||||
STYLE_TIME_SELECTED
|
||||
} else {
|
||||
STYLE_TIME
|
||||
},
|
||||
));
|
||||
txt.push(splitter.clone());
|
||||
txt.push(Text::Styled(
|
||||
Cow::from(e.author.as_str()),
|
||||
if selected {
|
||||
STYLE_AUTHOR_SELECTED
|
||||
} else {
|
||||
STYLE_AUTHOR
|
||||
},
|
||||
));
|
||||
txt.push(splitter);
|
||||
txt.push(Text::Styled(
|
||||
Cow::from(e.msg.as_str()),
|
||||
if selected {
|
||||
STYLE_MSG_SELECTED
|
||||
} else {
|
||||
STYLE_MSG
|
||||
},
|
||||
));
|
||||
txt.push(Text::Raw(Cow::from("\n")));
|
||||
|
||||
assert_eq!(txt.len() - count_before, ELEMENTS_PER_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Revlog {
|
||||
fn event(&mut self, ev: Event) -> bool {
|
||||
if let Event::Key(k) = ev {
|
||||
return match k {
|
||||
keys::MOVE_UP => {
|
||||
self.move_selection(true);
|
||||
true
|
||||
}
|
||||
keys::MOVE_DOWN => {
|
||||
self.move_selection(false);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
out.push(CommandInfo::new(
|
||||
commands::SCROLL,
|
||||
self.visible,
|
||||
self.visible || force_all,
|
||||
));
|
||||
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.visible = false;
|
||||
}
|
||||
|
||||
fn show(&mut self) {
|
||||
self.visible = true;
|
||||
|
||||
if !self.first_open_done {
|
||||
self.first_open_done = true;
|
||||
self.git_log.fetch();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue