mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
parent
364c79cfd5
commit
b7eed4361e
14 changed files with 718 additions and 73 deletions
|
|
@ -77,7 +77,7 @@ pub enum AsyncNotification {
|
|||
Fetch,
|
||||
}
|
||||
|
||||
/// current working director `./`
|
||||
/// current working directory `./`
|
||||
pub static CWD: &str = "./";
|
||||
|
||||
/// helper function to calculate the hash of an arbitrary type that implements the `Hash` trait
|
||||
|
|
|
|||
189
asyncgit/src/sync/blame.rs
Normal file
189
asyncgit/src/sync/blame.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
//! Sync git API for fetching a file blame
|
||||
|
||||
use super::{utils, CommitId};
|
||||
use crate::{error::Result, sync::get_commit_info};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
|
||||
/// A `BlameHunk` contains all the information that will be shown to the user.
|
||||
#[derive(Clone, Hash, Debug, PartialEq, Eq)]
|
||||
pub struct BlameHunk {
|
||||
///
|
||||
pub commit_id: CommitId,
|
||||
///
|
||||
pub author: String,
|
||||
///
|
||||
pub time: i64,
|
||||
/// `git2::BlameHunk::final_start_line` returns 1-based indices, but
|
||||
/// `start_line` is 0-based because the `Vec` storing the lines starts at
|
||||
/// index 0.
|
||||
pub start_line: usize,
|
||||
///
|
||||
pub end_line: usize,
|
||||
}
|
||||
|
||||
/// A `BlameFile` represents as a collection of hunks. This resembles `git2`’s
|
||||
/// API.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct FileBlame {
|
||||
///
|
||||
pub path: String,
|
||||
///
|
||||
pub lines: Vec<(Option<BlameHunk>, String)>,
|
||||
}
|
||||
|
||||
///
|
||||
pub fn blame_file(
|
||||
repo_path: &str,
|
||||
file_path: &str,
|
||||
commit_id: &str,
|
||||
) -> Result<FileBlame> {
|
||||
let repo = utils::repo(repo_path)?;
|
||||
|
||||
let spec = format!("{}:{}", commit_id, file_path);
|
||||
let blame = repo.blame_file(Path::new(file_path), None)?;
|
||||
let object = repo.revparse_single(&spec)?;
|
||||
let blob = repo.find_blob(object.id())?;
|
||||
let reader = BufReader::new(blob.content());
|
||||
|
||||
let lines: Vec<(Option<BlameHunk>, String)> = reader
|
||||
.lines()
|
||||
.enumerate()
|
||||
.map(|(i, line)| {
|
||||
// Line indices in a `FileBlame` are 1-based.
|
||||
let corresponding_hunk = blame.get_line(i + 1);
|
||||
|
||||
if let Some(hunk) = corresponding_hunk {
|
||||
let commit_id = CommitId::new(hunk.final_commit_id());
|
||||
// Line indices in a `BlameHunk` are 1-based.
|
||||
let start_line =
|
||||
hunk.final_start_line().saturating_sub(1);
|
||||
let end_line =
|
||||
start_line.saturating_add(hunk.lines_in_hunk());
|
||||
|
||||
if let Ok(commit_info) =
|
||||
get_commit_info(repo_path, &commit_id)
|
||||
{
|
||||
let hunk = BlameHunk {
|
||||
commit_id,
|
||||
author: commit_info.author.clone(),
|
||||
time: commit_info.time,
|
||||
start_line,
|
||||
end_line,
|
||||
};
|
||||
|
||||
return (
|
||||
Some(hunk),
|
||||
line.unwrap_or_else(|_| "".into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(None, line.unwrap_or_else(|_| "".into()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let file_blame = FileBlame {
|
||||
path: file_path.into(),
|
||||
lines,
|
||||
};
|
||||
|
||||
Ok(file_blame)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::error::Result;
|
||||
use crate::sync::{
|
||||
blame_file, commit, stage_add_file, tests::repo_init_empty,
|
||||
BlameHunk,
|
||||
};
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::Write,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_blame() -> Result<()> {
|
||||
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();
|
||||
|
||||
assert!(matches!(
|
||||
blame_file(&repo_path, "foo", "HEAD"),
|
||||
Err(_)
|
||||
));
|
||||
|
||||
File::create(&root.join(file_path))?
|
||||
.write_all(b"line 1\n")?;
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
commit(repo_path, "first commit")?;
|
||||
|
||||
let blame = blame_file(&repo_path, "foo", "HEAD")?;
|
||||
|
||||
assert!(matches!(
|
||||
blame.lines.as_slice(),
|
||||
[(
|
||||
Some(BlameHunk {
|
||||
author,
|
||||
start_line: 0,
|
||||
end_line: 1,
|
||||
..
|
||||
}),
|
||||
line
|
||||
)] if author == "name" && line == "line 1"
|
||||
));
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&root.join(file_path))?;
|
||||
|
||||
file.write(b"line 2\n")?;
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
commit(repo_path, "second commit")?;
|
||||
|
||||
let blame = blame_file(&repo_path, "foo", "HEAD")?;
|
||||
|
||||
assert!(matches!(
|
||||
blame.lines.as_slice(),
|
||||
[
|
||||
(
|
||||
Some(BlameHunk {
|
||||
start_line: 0,
|
||||
end_line: 1,
|
||||
..
|
||||
}),
|
||||
first_line
|
||||
),
|
||||
(
|
||||
Some(BlameHunk {
|
||||
author,
|
||||
start_line: 1,
|
||||
end_line: 2,
|
||||
..
|
||||
}),
|
||||
second_line
|
||||
)
|
||||
] if author == "name" && first_line == "line 1" && second_line == "line 2"
|
||||
));
|
||||
|
||||
file.write(b"line 3\n")?;
|
||||
|
||||
let blame = blame_file(&repo_path, "foo", "HEAD")?;
|
||||
|
||||
assert_eq!(blame.lines.len(), 2);
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
commit(repo_path, "third commit")?;
|
||||
|
||||
let blame = blame_file(&repo_path, "foo", "HEAD")?;
|
||||
|
||||
assert_eq!(blame.lines.len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,26 @@ pub fn get_commits_info(
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_commit_info(
|
||||
repo_path: &str,
|
||||
commit_id: &CommitId,
|
||||
) -> Result<CommitInfo> {
|
||||
scope_time!("get_commit_info");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let commit = repo.find_commit((*commit_id).into())?;
|
||||
let author = commit.author();
|
||||
|
||||
Ok(CommitInfo {
|
||||
message: commit.message().unwrap_or("").into(),
|
||||
author: author.name().unwrap_or("<unknown>").into(),
|
||||
time: commit.time().seconds(),
|
||||
id: CommitId(commit.id()),
|
||||
})
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_message(
|
||||
c: &Commit,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
//TODO: remove once we have this activated on the toplevel
|
||||
#![deny(clippy::expect_used)]
|
||||
|
||||
pub mod blame;
|
||||
pub mod branch;
|
||||
mod commit;
|
||||
mod commit_details;
|
||||
|
|
@ -24,6 +25,7 @@ pub mod status;
|
|||
mod tags;
|
||||
pub mod utils;
|
||||
|
||||
pub use blame::{blame_file, BlameHunk, FileBlame};
|
||||
pub use branch::{
|
||||
branch_compare_upstream, checkout_branch, config_is_pull_rebase,
|
||||
create_branch, delete_branch, get_branch_remote,
|
||||
|
|
@ -37,7 +39,9 @@ pub use commit_details::{
|
|||
get_commit_details, CommitDetails, CommitMessage,
|
||||
};
|
||||
pub use commit_files::get_commit_files;
|
||||
pub use commits_info::{get_commits_info, CommitId, CommitInfo};
|
||||
pub use commits_info::{
|
||||
get_commit_info, get_commits_info, CommitId, CommitInfo,
|
||||
};
|
||||
pub use diff::get_diff_commit;
|
||||
pub use hooks::{
|
||||
hooks_commit_msg, hooks_post_commit, hooks_pre_commit, HookResult,
|
||||
|
|
|
|||
110
src/app.rs
110
src/app.rs
|
|
@ -2,8 +2,8 @@ use crate::{
|
|||
accessors,
|
||||
cmdbar::CommandBar,
|
||||
components::{
|
||||
event_pump, BranchListComponent, CommandBlocking,
|
||||
CommandInfo, CommitComponent, Component,
|
||||
event_pump, BlameFileComponent, BranchListComponent,
|
||||
CommandBlocking, CommandInfo, CommitComponent, Component,
|
||||
CreateBranchComponent, DrawableComponent,
|
||||
ExternalEditorComponent, HelpComponent,
|
||||
InspectCommitComponent, MsgComponent, PullComponent,
|
||||
|
|
@ -41,6 +41,7 @@ pub struct App {
|
|||
msg: MsgComponent,
|
||||
reset: ResetComponent,
|
||||
commit: CommitComponent,
|
||||
blame_file_popup: BlameFileComponent,
|
||||
stashmsg_popup: StashMsgComponent,
|
||||
inspect_commit_popup: InspectCommitComponent,
|
||||
external_editor_popup: ExternalEditorComponent,
|
||||
|
|
@ -93,6 +94,11 @@ impl App {
|
|||
theme.clone(),
|
||||
key_config.clone(),
|
||||
),
|
||||
blame_file_popup: BlameFileComponent::new(
|
||||
&strings::blame_title(&key_config),
|
||||
theme.clone(),
|
||||
key_config.clone(),
|
||||
),
|
||||
stashmsg_popup: StashMsgComponent::new(
|
||||
queue.clone(),
|
||||
theme.clone(),
|
||||
|
|
@ -363,6 +369,7 @@ impl App {
|
|||
msg,
|
||||
reset,
|
||||
commit,
|
||||
blame_file_popup,
|
||||
stashmsg_popup,
|
||||
inspect_commit_popup,
|
||||
external_editor_popup,
|
||||
|
|
@ -488,48 +495,9 @@ impl App {
|
|||
) -> Result<NeedsUpdate> {
|
||||
let mut flags = NeedsUpdate::empty();
|
||||
match ev {
|
||||
InternalEvent::ConfirmedAction(action) => match action {
|
||||
Action::Reset(r) => {
|
||||
if self.status_tab.reset(&r) {
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
}
|
||||
Action::StashDrop(_) | Action::StashPop(_) => {
|
||||
if self.stashlist_tab.action_confirmed(&action) {
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
}
|
||||
Action::ResetHunk(path, hash) => {
|
||||
sync::reset_hunk(CWD, &path, hash)?;
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
Action::ResetLines(path, lines) => {
|
||||
sync::discard_lines(CWD, &path, &lines)?;
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
Action::DeleteBranch(branch_ref) => {
|
||||
if let Err(e) =
|
||||
sync::delete_branch(CWD, &branch_ref)
|
||||
{
|
||||
self.queue.borrow_mut().push_back(
|
||||
InternalEvent::ShowErrorMsg(
|
||||
e.to_string(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
self.select_branch_popup.update_branches()?;
|
||||
}
|
||||
}
|
||||
Action::ForcePush(branch, force) => self
|
||||
.queue
|
||||
.borrow_mut()
|
||||
.push_back(InternalEvent::Push(branch, force)),
|
||||
Action::PullMerge { rebase, .. } => {
|
||||
self.pull_popup.try_conflict_free_merge(rebase);
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
},
|
||||
InternalEvent::ConfirmedAction(action) => {
|
||||
self.process_confirmed_action(action, &mut flags)?;
|
||||
}
|
||||
InternalEvent::ConfirmAction(action) => {
|
||||
self.reset.open(action)?;
|
||||
flags.insert(NeedsUpdate::COMMANDS);
|
||||
|
|
@ -548,6 +516,10 @@ impl App {
|
|||
InternalEvent::TagCommit(id) => {
|
||||
self.tag_commit_popup.open(id)?;
|
||||
}
|
||||
InternalEvent::BlameFile(path) => {
|
||||
self.blame_file_popup.open(&path)?;
|
||||
flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS)
|
||||
}
|
||||
InternalEvent::CreateBranch => {
|
||||
self.create_branch_popup.open()?;
|
||||
}
|
||||
|
|
@ -586,6 +558,54 @@ impl App {
|
|||
Ok(flags)
|
||||
}
|
||||
|
||||
fn process_confirmed_action(
|
||||
&mut self,
|
||||
action: Action,
|
||||
flags: &mut NeedsUpdate,
|
||||
) -> Result<()> {
|
||||
match action {
|
||||
Action::Reset(r) => {
|
||||
if self.status_tab.reset(&r) {
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
}
|
||||
Action::StashDrop(_) | Action::StashPop(_) => {
|
||||
if self.stashlist_tab.action_confirmed(&action) {
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
}
|
||||
Action::ResetHunk(path, hash) => {
|
||||
sync::reset_hunk(CWD, &path, hash)?;
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
Action::ResetLines(path, lines) => {
|
||||
sync::discard_lines(CWD, &path, &lines)?;
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
Action::DeleteBranch(branch_ref) => {
|
||||
if let Err(e) = sync::delete_branch(CWD, &branch_ref)
|
||||
{
|
||||
self.queue.borrow_mut().push_back(
|
||||
InternalEvent::ShowErrorMsg(e.to_string()),
|
||||
)
|
||||
} else {
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
self.select_branch_popup.update_branches()?;
|
||||
}
|
||||
}
|
||||
Action::ForcePush(branch, force) => self
|
||||
.queue
|
||||
.borrow_mut()
|
||||
.push_back(InternalEvent::Push(branch, force)),
|
||||
Action::PullMerge { rebase, .. } => {
|
||||
self.pull_popup.try_conflict_free_merge(rebase);
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commands(&self, force_all: bool) -> Vec<CommandInfo> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
|
|
@ -637,6 +657,7 @@ impl App {
|
|||
|| self.msg.is_visible()
|
||||
|| self.stashmsg_popup.is_visible()
|
||||
|| self.inspect_commit_popup.is_visible()
|
||||
|| self.blame_file_popup.is_visible()
|
||||
|| self.external_editor_popup.is_visible()
|
||||
|| self.tag_commit_popup.is_visible()
|
||||
|| self.create_branch_popup.is_visible()
|
||||
|
|
@ -666,6 +687,7 @@ impl App {
|
|||
self.stashmsg_popup.draw(f, size)?;
|
||||
self.help.draw(f, size)?;
|
||||
self.inspect_commit_popup.draw(f, size)?;
|
||||
self.blame_file_popup.draw(f, size)?;
|
||||
self.external_editor_popup.draw(f, size)?;
|
||||
self.tag_commit_popup.draw(f, size)?;
|
||||
self.select_branch_popup.draw(f, size)?;
|
||||
|
|
|
|||
372
src/components/blame_file.rs
Normal file
372
src/components/blame_file.rs
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
use super::{
|
||||
utils, visibility_blocking, CommandBlocking, CommandInfo,
|
||||
Component, DrawableComponent,
|
||||
};
|
||||
use crate::{
|
||||
components::{utils::string_width_align, ScrollType},
|
||||
keys::SharedKeyConfig,
|
||||
strings,
|
||||
ui::style::SharedTheme,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use asyncgit::{
|
||||
sync::{blame_file, BlameHunk, FileBlame},
|
||||
CWD,
|
||||
};
|
||||
use crossterm::event::Event;
|
||||
use std::convert::TryInto;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Rect},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Cell, Clear, Row, Table, TableState},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub struct BlameFileComponent {
|
||||
title: String,
|
||||
theme: SharedTheme,
|
||||
visible: bool,
|
||||
path: Option<String>,
|
||||
file_blame: Option<FileBlame>,
|
||||
table_state: std::cell::Cell<TableState>,
|
||||
key_config: SharedKeyConfig,
|
||||
current_height: std::cell::Cell<usize>,
|
||||
}
|
||||
|
||||
static COMMIT_ID: &str = "HEAD";
|
||||
static NO_COMMIT_ID: &str = "0000000";
|
||||
static NO_AUTHOR: &str = "<no author>";
|
||||
static AUTHOR_WIDTH: usize = 20;
|
||||
|
||||
fn get_author_width(width: usize) -> usize {
|
||||
(width.saturating_sub(19) / 3).max(3).min(AUTHOR_WIDTH)
|
||||
}
|
||||
|
||||
const fn number_of_digits(number: usize) -> usize {
|
||||
let mut rest = number;
|
||||
let mut result = 0;
|
||||
|
||||
while rest > 0 {
|
||||
rest /= 10;
|
||||
result += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
impl DrawableComponent for BlameFileComponent {
|
||||
fn draw<B: Backend>(
|
||||
&self,
|
||||
f: &mut Frame<B>,
|
||||
area: Rect,
|
||||
) -> Result<()> {
|
||||
if self.is_visible() {
|
||||
let path: &str = self
|
||||
.path
|
||||
.as_deref()
|
||||
.unwrap_or("<no path for blame available>");
|
||||
|
||||
let title = if self.file_blame.is_some() {
|
||||
format!("{} -- {} -- {}", self.title, path, COMMIT_ID)
|
||||
} else {
|
||||
format!(
|
||||
"{} -- {} -- <no blame available>",
|
||||
self.title, path
|
||||
)
|
||||
};
|
||||
|
||||
let rows = self.get_rows(area.width.into());
|
||||
let author_width = get_author_width(area.width.into());
|
||||
let constraints = [
|
||||
// commit id
|
||||
Constraint::Length(7),
|
||||
// commit date
|
||||
Constraint::Length(10),
|
||||
// commit author
|
||||
Constraint::Length(author_width.try_into()?),
|
||||
// line number and vertical bar
|
||||
Constraint::Length(
|
||||
(self.get_line_number_width().saturating_add(1))
|
||||
.try_into()?,
|
||||
),
|
||||
// the source code line
|
||||
Constraint::Min(0),
|
||||
];
|
||||
|
||||
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);
|
||||
|
||||
self.table_state.set(table_state);
|
||||
self.current_height.set(area.height.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for BlameFileComponent {
|
||||
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::scroll(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.order(1),
|
||||
);
|
||||
}
|
||||
|
||||
visibility_blocking(self)
|
||||
}
|
||||
|
||||
fn event(
|
||||
&mut self,
|
||||
event: crossterm::event::Event,
|
||||
) -> Result<bool> {
|
||||
if self.is_visible() {
|
||||
if let Event::Key(key) = event {
|
||||
if key == self.key_config.exit_popup {
|
||||
self.hide();
|
||||
} else if key == self.key_config.move_up {
|
||||
self.move_selection(ScrollType::Up);
|
||||
} else if key == self.key_config.move_down {
|
||||
self.move_selection(ScrollType::Down);
|
||||
} else if key == self.key_config.shift_up
|
||||
|| key == self.key_config.home
|
||||
{
|
||||
self.move_selection(ScrollType::Home);
|
||||
} else if key == self.key_config.shift_down
|
||||
|| key == self.key_config.end
|
||||
{
|
||||
self.move_selection(ScrollType::End);
|
||||
} else if key == self.key_config.page_down {
|
||||
self.move_selection(ScrollType::PageDown);
|
||||
} else if key == self.key_config.page_up {
|
||||
self.move_selection(ScrollType::PageUp);
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.visible = false
|
||||
}
|
||||
|
||||
fn show(&mut self) -> Result<()> {
|
||||
self.visible = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BlameFileComponent {
|
||||
///
|
||||
pub fn new(
|
||||
title: &str,
|
||||
theme: SharedTheme,
|
||||
key_config: SharedKeyConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: String::from(title),
|
||||
theme,
|
||||
visible: false,
|
||||
path: None,
|
||||
file_blame: None,
|
||||
table_state: std::cell::Cell::new(TableState::default()),
|
||||
key_config,
|
||||
current_height: std::cell::Cell::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn open(&mut self, path: &str) -> Result<()> {
|
||||
self.path = Some(path.into());
|
||||
self.file_blame = blame_file(CWD, path, COMMIT_ID).ok();
|
||||
self.table_state.get_mut().select(Some(0));
|
||||
|
||||
self.show()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
fn get_rows(&self, width: usize) -> Vec<Row> {
|
||||
if let Some(ref file_blame) = self.file_blame {
|
||||
file_blame
|
||||
.lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (blame_hunk, line))| {
|
||||
self.get_line_blame(
|
||||
width,
|
||||
i,
|
||||
(blame_hunk.as_ref(), line.as_ref()),
|
||||
file_blame,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
fn get_line_blame(
|
||||
&self,
|
||||
width: usize,
|
||||
line_number: usize,
|
||||
hunk_and_line: (Option<&BlameHunk>, &str),
|
||||
file_blame: &FileBlame,
|
||||
) -> Row {
|
||||
let (hunk_for_line, line) = hunk_and_line;
|
||||
|
||||
let show_metadata = if line_number == 0 {
|
||||
true
|
||||
} else {
|
||||
let hunk_for_previous_line =
|
||||
&file_blame.lines[line_number - 1];
|
||||
|
||||
match (hunk_for_previous_line, hunk_for_line) {
|
||||
((Some(previous), _), Some(current)) => {
|
||||
previous.commit_id != current.commit_id
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
};
|
||||
|
||||
let mut cells = if show_metadata {
|
||||
self.get_metadata_for_line_blame(width, hunk_for_line)
|
||||
} else {
|
||||
vec![Cell::from(""), Cell::from(""), Cell::from("")]
|
||||
};
|
||||
|
||||
let line_number_width = self.get_line_number_width();
|
||||
cells.push(
|
||||
// U+2502 is BOX DRAWINGS LIGHT VERTICAL.
|
||||
Cell::from(format!(
|
||||
"{:>line_number_width$}\u{2502}",
|
||||
line_number,
|
||||
line_number_width = line_number_width,
|
||||
))
|
||||
.style(self.theme.text(true, false)),
|
||||
);
|
||||
cells.push(
|
||||
Cell::from(String::from(line))
|
||||
.style(self.theme.text(true, false)),
|
||||
);
|
||||
|
||||
Row::new(cells)
|
||||
}
|
||||
|
||||
fn get_metadata_for_line_blame(
|
||||
&self,
|
||||
width: usize,
|
||||
blame_hunk: Option<&BlameHunk>,
|
||||
) -> Vec<Cell> {
|
||||
let commit_hash = blame_hunk.map_or_else(
|
||||
|| NO_COMMIT_ID.into(),
|
||||
|hunk| hunk.commit_id.get_short_string(),
|
||||
);
|
||||
let author_width = get_author_width(width);
|
||||
let truncated_author: String = blame_hunk.map_or_else(
|
||||
|| NO_AUTHOR.into(),
|
||||
|hunk| string_width_align(&hunk.author, author_width),
|
||||
);
|
||||
let author = format!(
|
||||
"{:author_width$}",
|
||||
truncated_author,
|
||||
author_width = AUTHOR_WIDTH
|
||||
);
|
||||
let time = blame_hunk.map_or_else(
|
||||
|| "".into(),
|
||||
|hunk| utils::time_to_string(hunk.time, true),
|
||||
);
|
||||
|
||||
vec![
|
||||
Cell::from(commit_hash)
|
||||
.style(self.theme.commit_hash(false)),
|
||||
Cell::from(time).style(self.theme.commit_time(false)),
|
||||
Cell::from(author).style(self.theme.commit_author(false)),
|
||||
]
|
||||
}
|
||||
|
||||
fn get_max_line_number(&self) -> usize {
|
||||
self.file_blame
|
||||
.as_ref()
|
||||
.map_or(0, |file_blame| file_blame.lines.len() - 1)
|
||||
}
|
||||
|
||||
fn get_line_number_width(&self) -> usize {
|
||||
let max_line_number = self.get_max_line_number();
|
||||
|
||||
number_of_digits(max_line_number)
|
||||
}
|
||||
|
||||
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_line_number();
|
||||
|
||||
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;
|
||||
|
||||
table_state.select(Some(new_selection));
|
||||
self.table_state.set(table_state);
|
||||
|
||||
needs_update
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use super::utils::logitems::{ItemBatch, LogEntry};
|
||||
use crate::{
|
||||
components::{
|
||||
CommandBlocking, CommandInfo, Component, DrawableComponent,
|
||||
ScrollType,
|
||||
utils::string_width_align, CommandBlocking, CommandInfo,
|
||||
Component, DrawableComponent, ScrollType,
|
||||
},
|
||||
keys::SharedKeyConfig,
|
||||
strings,
|
||||
|
|
@ -22,7 +22,6 @@ use tui::{
|
|||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
const ELEMENTS_PER_LINE: usize = 10;
|
||||
|
||||
|
|
@ -383,29 +382,6 @@ impl Component for CommitList {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn string_width_align(s: &str, width: usize) -> String {
|
||||
static POSTFIX: &str = "..";
|
||||
|
||||
let len = UnicodeWidthStr::width(s);
|
||||
let width_wo_postfix = width.saturating_sub(POSTFIX.len());
|
||||
|
||||
if (len >= width_wo_postfix && len <= width)
|
||||
|| (len <= width_wo_postfix)
|
||||
{
|
||||
format!("{:w$}", s, w = width)
|
||||
} else {
|
||||
let mut s = s.to_string();
|
||||
s.truncate(find_truncate_point(&s, width_wo_postfix));
|
||||
format!("{}{}", s, POSTFIX)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn find_truncate_point(s: &str, chars: usize) -> usize {
|
||||
s.chars().take(chars).map(char::len_utf8).sum()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -388,6 +388,11 @@ impl Component for FileTreeComponent {
|
|||
)
|
||||
.order(order::NAV),
|
||||
);
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::blame_file(&self.key_config),
|
||||
self.selection_file().is_some(),
|
||||
self.focused || force_all,
|
||||
));
|
||||
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
|
|
@ -395,7 +400,20 @@ impl Component for FileTreeComponent {
|
|||
fn event(&mut self, ev: Event) -> Result<bool> {
|
||||
if self.focused {
|
||||
if let Event::Key(e) = ev {
|
||||
return if e == self.key_config.move_down {
|
||||
return if e == self.key_config.blame {
|
||||
match (&self.queue, self.selection_file()) {
|
||||
(Some(queue), Some(status_item)) => {
|
||||
queue.borrow_mut().push_back(
|
||||
InternalEvent::BlameFile(
|
||||
status_item.path,
|
||||
),
|
||||
);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
_ => Ok(false),
|
||||
}
|
||||
} else if e == self.key_config.move_down {
|
||||
Ok(self.move_selection(MoveSelection::Down))
|
||||
} else if e == self.key_config.move_up {
|
||||
Ok(self.move_selection(MoveSelection::Up))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
mod blame_file;
|
||||
mod branchlist;
|
||||
mod changes;
|
||||
mod command;
|
||||
|
|
@ -22,6 +23,7 @@ mod tag_commit;
|
|||
mod textinput;
|
||||
mod utils;
|
||||
|
||||
pub use blame_file::BlameFileComponent;
|
||||
pub use branchlist::BranchListComponent;
|
||||
pub use changes::ChangesComponent;
|
||||
pub use command::{CommandInfo, CommandText};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use chrono::{DateTime, Local, NaiveDateTime, Utc};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub mod filetree;
|
||||
pub mod logitems;
|
||||
|
|
@ -34,3 +35,26 @@ pub fn time_to_string(secs: i64, short: bool) -> String {
|
|||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn string_width_align(s: &str, width: usize) -> String {
|
||||
static POSTFIX: &str = "..";
|
||||
|
||||
let len = UnicodeWidthStr::width(s);
|
||||
let width_wo_postfix = width.saturating_sub(POSTFIX.len());
|
||||
|
||||
if (len >= width_wo_postfix && len <= width)
|
||||
|| (len <= width_wo_postfix)
|
||||
{
|
||||
format!("{:w$}", s, w = width)
|
||||
} else {
|
||||
let mut s = s.to_string();
|
||||
s.truncate(find_truncate_point(&s, width_wo_postfix));
|
||||
format!("{}{}", s, POSTFIX)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn find_truncate_point(s: &str, chars: usize) -> usize {
|
||||
s.chars().take(chars).map(char::len_utf8).sum()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ pub struct KeyConfig {
|
|||
pub shift_up: KeyEvent,
|
||||
pub shift_down: KeyEvent,
|
||||
pub enter: KeyEvent,
|
||||
pub blame: KeyEvent,
|
||||
pub edit_file: KeyEvent,
|
||||
pub status_stage_all: KeyEvent,
|
||||
pub status_reset_item: KeyEvent,
|
||||
|
|
@ -103,6 +104,7 @@ impl Default for KeyConfig {
|
|||
shift_up: KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::SHIFT},
|
||||
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::empty()},
|
||||
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},
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ pub enum InternalEvent {
|
|||
///
|
||||
TagCommit(CommitId),
|
||||
///
|
||||
BlameFile(String),
|
||||
///
|
||||
CreateBranch,
|
||||
///
|
||||
RenameBranch(String, String),
|
||||
|
|
|
|||
|
|
@ -170,6 +170,9 @@ pub fn confirm_msg_force_push(
|
|||
pub fn log_title(_key_config: &SharedKeyConfig) -> String {
|
||||
"Commit".to_string()
|
||||
}
|
||||
pub fn blame_title(_key_config: &SharedKeyConfig) -> String {
|
||||
"Blame".to_string()
|
||||
}
|
||||
pub fn tag_commit_popup_title(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
|
|
@ -817,6 +820,16 @@ pub mod commands {
|
|||
CMD_GROUP_LOG,
|
||||
)
|
||||
}
|
||||
pub fn blame_file(key_config: &SharedKeyConfig) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Blame [{}]",
|
||||
key_config.get_hint(key_config.blame),
|
||||
),
|
||||
"open blame view of selected file",
|
||||
CMD_GROUP_LOG,
|
||||
)
|
||||
}
|
||||
pub fn log_tag_commit(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@
|
|||
shift_down: ( code: Char('J'), modifiers: ( bits: 1,),),
|
||||
|
||||
enter: ( code: Enter, modifiers: ( bits: 0,),),
|
||||
blame: ( code: Char('b'), modifiers: ( bits: 0,),),
|
||||
|
||||
edit_file: ( code: Char('I'), modifiers: ( bits: 1,),),
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue