commit details in log (#107)

This commit is contained in:
Stephan Dilly 2020-06-06 19:24:48 +02:00 committed by GitHub
parent 3dc1119142
commit 5bfdb40a14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 790 additions and 77 deletions

View File

@ -10,20 +10,28 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Get version
id: get_version
run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\//}
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: clippy
- name: Build
run: cargo build
- name: Run tests
run: make test
- name: Run clippy
run: |
rustup component add clippy
cargo clean
make clippy
- name: Setup MUSL

View File

@ -13,19 +13,29 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
rust: [stable]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: clippy
- name: Build Debug
run: cargo build
run: |
rustc --version
cargo build
- name: Run tests
run: make test
- name: Run clippy
run: |
rustup component add clippy
cargo clean
make clippy
- name: Build Release
@ -35,9 +45,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
target: x86_64-unknown-linux-musl
- name: Setup MUSL
run: |
rustup target add x86_64-unknown-linux-musl
sudo apt-get -qq install musl-tools
- name: Build Debug
run: cargo build --target=x86_64-unknown-linux-musl
@ -51,7 +67,10 @@ jobs:
steps:
- uses: actions/checkout@master
- name: Install Rust
run: rustup update stable && rustup default stable && rustup component add rustfmt
uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: rustfmt
- run: cargo fmt -- --check
sec:

View File

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- New `Stage all [a]`/`Unstage all [a]` in changes lists ([#82](https://github.com/extrawurst/gitui/issues/82))
- add `-d`, `--directory` options to set working directory ([#73](https://github.com/extrawurst/gitui/issues/73))
- commit detail view in revlog ([#80](https://github.com/extrawurst/gitui/issues/80))
### Fixed
- app closes when staging invalid file/path ([#108](https://github.com/extrawurst/gitui/issues/108))

View File

@ -29,6 +29,7 @@ fmt:
cargo fmt -- --check
clippy:
touch src/main.rs
cargo clean -p gitui -p asyncgit -p scopetime
cargo clippy --all-features

View File

@ -45,7 +45,7 @@ presentation slides: https://github.com/extrawurst/gitui-presentation
# known limitations
* no support for [bare repositories](https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server)
* no support for [bare repositories](https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server) (see [#100](https://github.com/extrawurst/gitui/issues/100))
* [core.hooksPath](https://git-scm.com/docs/githooks) config not supported
* revert/reset hunk in working dir (see [#11](https://github.com/extrawurst/gitui/issues/11))
@ -89,9 +89,7 @@ see [releases](https://github.com/extrawurst/gitui/releases)
### requirements
install `rust`/`cargo`: https://www.rust-lang.org/tools/install
min rust version: `1.42`
install **latest** `rust`/`cargo`: https://www.rust-lang.org/tools/install
### cargo install

View File

@ -0,0 +1,101 @@
use crate::{
error::Result, sync, AsyncNotification, StatusItem, CWD,
};
use crossbeam_channel::Sender;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
};
use sync::CommitId;
type ResultType = Vec<StatusItem>;
struct Request<R, A>(R, A);
///
pub struct AsyncCommitFiles {
current: Arc<Mutex<Option<Request<CommitId, ResultType>>>>,
sender: Sender<AsyncNotification>,
pending: Arc<AtomicUsize>,
}
impl AsyncCommitFiles {
///
pub fn new(sender: &Sender<AsyncNotification>) -> Self {
Self {
current: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicUsize::new(0)),
}
}
///
pub fn current(
&mut self,
) -> Result<Option<(CommitId, ResultType)>> {
let c = self.current.lock()?;
if let Some(c) = c.as_ref() {
Ok(Some((c.0, c.1.clone())))
} else {
Ok(None)
}
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn fetch(&mut self, id: CommitId) -> Result<()> {
if self.is_pending() {
return Ok(());
}
log::trace!("request: {}", id.to_string());
{
let current = self.current.lock()?;
if let Some(ref c) = *current {
if c.0 == id {
return Ok(());
}
}
}
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.fetch_add(1, Ordering::Relaxed);
Self::fetch_helper(id, arc_current)
.expect("failed to fetch");
arc_pending.fetch_sub(1, Ordering::Relaxed);
sender
.send(AsyncNotification::CommitFiles)
.expect("error sending");
});
Ok(())
}
fn fetch_helper(
id: CommitId,
arc_current: Arc<
Mutex<Option<Request<CommitId, ResultType>>>,
>,
) -> Result<()> {
let res = sync::get_commit_files(CWD, id)?;
{
let mut last = arc_current.lock()?;
*last = Some(Request(id, res));
}
Ok(())
}
}

View File

@ -6,6 +6,7 @@
#![deny(clippy::result_unwrap_used)]
#![deny(clippy::panic)]
mod commit_files;
mod diff;
mod error;
mod revlog;
@ -13,6 +14,7 @@ mod status;
pub mod sync;
pub use crate::{
commit_files::AsyncCommitFiles,
diff::{AsyncDiff, DiffParams},
revlog::{AsyncLog, FetchStatus},
status::{AsyncStatus, StatusParams},
@ -27,7 +29,7 @@ use std::{
};
/// this type is used to communicate events back through the channel
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum AsyncNotification {
///
Status,
@ -35,6 +37,8 @@ pub enum AsyncNotification {
Diff,
///
Log,
///
CommitFiles,
}
/// current working director `./`

View File

@ -41,10 +41,10 @@ static SLEEP_BACKGROUND: Duration = Duration::from_millis(1000);
impl AsyncLog {
///
pub fn new(sender: Sender<AsyncNotification>) -> Self {
pub fn new(sender: &Sender<AsyncNotification>) -> Self {
Self {
current: Arc::new(Mutex::new(Vec::new())),
sender,
sender: sender.clone(),
pending: Arc::new(AtomicBool::new(false)),
background: Arc::new(AtomicBool::new(false)),
}

View File

@ -0,0 +1,99 @@
use super::{utils::repo, CommitId};
use crate::error::Result;
use git2::Signature;
use scopetime::scope_time;
///
#[derive(Debug, PartialEq)]
pub struct CommitSignature {
///
pub name: String,
///
pub email: String,
/// time in secs since Unix epoch
pub time: i64,
}
impl CommitSignature {
/// convert from git2-rs `Signature`
pub fn from(s: Signature<'_>) -> Self {
Self {
name: s.name().unwrap_or("").to_string(),
email: s.email().unwrap_or("").to_string(),
time: s.when().seconds(),
}
}
}
///
pub struct CommitMessage {
/// first line
pub subject: String,
/// remaining lines if more than one
pub body: Option<String>,
}
impl CommitMessage {
pub fn from(s: &str) -> Self {
if let Some(idx) = s.find('\n') {
let (first, rest) = s.split_at(idx);
Self {
subject: first.to_string(),
body: if rest.is_empty() {
None
} else {
Some(rest.to_string())
},
}
} else {
Self {
subject: s.to_string(),
body: None,
}
}
}
}
///
pub struct CommitDetails {
///
pub author: CommitSignature,
/// committer when differs to `author` otherwise None
pub committer: Option<CommitSignature>,
///
pub message: Option<CommitMessage>,
///
pub hash: String,
}
///
pub fn get_commit_details(
repo_path: &str,
id: CommitId,
) -> Result<CommitDetails> {
scope_time!("get_commit_details");
let repo = repo(repo_path)?;
let commit = repo.find_commit(id.into())?;
let author = CommitSignature::from(commit.author());
let committer = CommitSignature::from(commit.committer());
let committer = if author == committer {
None
} else {
Some(committer)
};
let message = commit.message().map(|m| CommitMessage::from(m));
let details = CommitDetails {
author,
committer,
message,
hash: id.to_string(),
};
Ok(details)
}

View File

@ -0,0 +1,82 @@
use super::{utils::repo, CommitId};
use crate::{error::Result, StatusItem, StatusItemType};
use git2::DiffDelta;
use scopetime::scope_time;
/// get all files that are part of a commit
pub fn get_commit_files(
repo_path: &str,
id: CommitId,
) -> Result<Vec<StatusItem>> {
scope_time!("get_commit_files");
let repo = repo(repo_path)?;
let commit = repo.find_commit(id.into())?;
let commit_tree = commit.tree()?;
let parent = if commit.parent_count() > 0 {
Some(repo.find_commit(commit.parent_id(0)?)?.tree()?)
} else {
None
};
let diff = repo.diff_tree_to_tree(
parent.as_ref(),
Some(&commit_tree),
None,
)?;
let mut res = Vec::new();
diff.foreach(
&mut |delta: DiffDelta<'_>, _progress| {
res.push(StatusItem {
path: delta
.new_file()
.path()
.map(|p| p.to_str().unwrap_or("").to_string())
.unwrap_or_default(),
status: StatusItemType::from(delta.status()),
});
true
},
None,
None,
None,
)?;
Ok(res)
}
#[cfg(test)]
mod tests {
use super::get_commit_files;
use crate::{
sync::{commit, stage_add_file, tests::repo_init, CommitId},
StatusItemType,
};
use std::{fs::File, io::Write, path::Path};
#[test]
fn test_smoke() {
let file_path = Path::new("file1.txt");
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(&root.join(file_path))
.unwrap()
.write_all(b"test file1 content")
.unwrap();
stage_add_file(repo_path, file_path).unwrap();
let id = commit(repo_path, "commit msg").unwrap();
let diff =
get_commit_files(repo_path, CommitId::new(id)).unwrap();
assert_eq!(diff.len(), 1);
assert_eq!(diff[0].status, StatusItemType::New);
}
}

View File

@ -25,6 +25,12 @@ impl ToString for CommitId {
}
}
impl Into<Oid> for CommitId {
fn into(self) -> Oid {
self.0
}
}
///
#[derive(Debug)]
pub struct CommitInfo {

View File

@ -1,5 +1,7 @@
//! sync git api
mod commit_details;
mod commit_files;
mod commits_info;
pub mod diff;
mod hooks;
@ -12,6 +14,8 @@ pub mod status;
mod tags;
pub mod utils;
pub use commit_details::{get_commit_details, CommitDetails};
pub use commit_files::get_commit_files;
pub use commits_info::{get_commits_info, CommitId, CommitInfo};
pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult};
pub use hunks::{stage_hunk, unstage_hunk};

View File

@ -1,7 +1,7 @@
//! sync git api for fetching a status
use crate::{error::Error, error::Result, sync::utils};
use git2::{Status, StatusOptions, StatusShow};
use git2::{Delta, Status, StatusOptions, StatusShow};
use scopetime::scope_time;
use std::path::Path;
@ -36,13 +36,25 @@ impl From<Status> for StatusItemType {
}
}
impl From<Delta> for StatusItemType {
fn from(d: Delta) -> Self {
match d {
Delta::Added => StatusItemType::New,
Delta::Deleted => StatusItemType::Deleted,
Delta::Renamed => StatusItemType::Renamed,
Delta::Typechange => StatusItemType::Typechange,
_ => StatusItemType::Modified,
}
}
}
///
#[derive(Default, Clone, Hash, PartialEq, Debug)]
#[derive(Clone, Hash, PartialEq, Debug)]
pub struct StatusItem {
///
pub path: String,
///
pub status: Option<StatusItemType>,
pub status: StatusItemType,
}
///
@ -125,7 +137,7 @@ pub fn get_status_new(
res.push(StatusItem {
path,
status: Some(StatusItemType::from(status)),
status: StatusItemType::from(status),
});
}

View File

@ -190,12 +190,11 @@ impl App {
self.status_tab.update_git(ev)?;
self.stashing_tab.update_git(ev)?;
self.revlog.update_git(ev)?;
match ev {
AsyncNotification::Diff => (),
AsyncNotification::Log => self.revlog.update()?,
if let AsyncNotification::Status = ev {
//TODO: is that needed?
AsyncNotification::Status => self.update_commands(),
self.update_commands()
}
Ok(())

View File

@ -92,17 +92,15 @@ impl ChangesComponent {
if let Some(tree_item) = self.selection() {
if self.is_working_dir {
if let FileTreeItemKind::File(i) = tree_item.kind {
if let Some(status) = i.status {
let path = Path::new(i.path.as_str());
match status {
StatusItemType::Deleted => {
sync::stage_addremoved(CWD, path)?
}
_ => sync::stage_add_file(CWD, path)?,
};
let path = Path::new(i.path.as_str());
match i.status {
StatusItemType::Deleted => {
sync::stage_addremoved(CWD, path)?
}
_ => sync::stage_add_file(CWD, path)?,
};
return Ok(true);
}
return Ok(true);
} else {
//TODO: check if we can handle the one file case with it aswell
sync::stage_add_all(

View File

@ -0,0 +1,279 @@
use super::{
dialog_paragraph, utils::time_to_string, DrawableComponent,
};
use crate::{strings, ui::style::Theme};
use anyhow::Result;
use asyncgit::{
sync::{self, CommitDetails, CommitId},
AsyncCommitFiles, AsyncNotification, StatusItem, CWD,
};
use crossbeam_channel::Sender;
use std::borrow::Cow;
use sync::Tags;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::Modifier,
widgets::Text,
Frame,
};
pub struct CommitDetailsComponent {
data: Option<CommitDetails>,
tags: Vec<String>,
files: Option<Vec<StatusItem>>,
theme: Theme,
git_commit_files: AsyncCommitFiles,
}
impl DrawableComponent for CommitDetailsComponent {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
rect: Rect,
) -> Result<()> {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(8),
Constraint::Min(10),
Constraint::Length(12),
]
.as_ref(),
)
.split(rect);
f.render_widget(
dialog_paragraph(
strings::commit::DETAILS_INFO_TITLE,
self.get_text_info().iter(),
),
chunks[0],
);
f.render_widget(
dialog_paragraph(
strings::commit::DETAILS_MESSAGE_TITLE,
self.get_text_message().iter(),
)
.wrap(true),
chunks[1],
);
let files_loading = self.files.is_none();
let files_count = self.files.as_ref().map_or(0, Vec::len);
let txt = self
.files
.as_ref()
.map_or(vec![], |f| self.get_text_files(f));
let title = if files_loading {
strings::commit::DETAILS_FILES_LOADING_TITLE.to_string()
} else {
format!(
"{} {}",
strings::commit::DETAILS_FILES_TITLE,
files_count
)
};
f.render_widget(
dialog_paragraph(title.as_str(), txt.iter()),
chunks[2],
);
Ok(())
}
}
impl CommitDetailsComponent {
///
pub fn new(
sender: &Sender<AsyncNotification>,
theme: &Theme,
) -> Self {
Self {
theme: *theme,
data: None,
tags: Vec::new(),
files: None,
git_commit_files: AsyncCommitFiles::new(sender),
}
}
///
pub fn set_commit(
&mut self,
id: Option<CommitId>,
tags: &Tags,
) -> Result<()> {
self.data = if let Some(id) = id {
sync::get_commit_details(CWD, id).ok()
} else {
None
};
self.tags.clear();
self.files = None;
if let Some(id) = id {
if let Some(tags) = tags.get(&id) {
self.tags.extend(tags.clone());
}
if let Some((fetched_id, res)) =
self.git_commit_files.current()?
{
if fetched_id == id {
self.files = Some(res);
} else {
self.git_commit_files.fetch(id)?;
}
} else {
self.git_commit_files.fetch(id)?;
}
}
Ok(())
}
fn get_text_message(&self) -> Vec<Text> {
if let Some(ref data) = self.data {
if let Some(ref message) = data.message {
let mut res = vec![Text::Styled(
Cow::from(message.subject.clone()),
self.theme
.text(true, false)
.modifier(Modifier::BOLD),
)];
if let Some(ref body) = message.body {
res.push(Text::Styled(
Cow::from(body),
self.theme.text(true, false),
));
}
return res;
}
}
vec![]
}
fn get_text_files<'a>(
&self,
files: &'a [StatusItem],
) -> Vec<Text<'a>> {
let new_line = Text::Raw(Cow::from("\n"));
let mut res = Vec::with_capacity(files.len());
for file in files {
res.push(Text::Styled(
Cow::from(file.path.as_str()),
self.theme.text(true, false),
));
res.push(new_line.clone());
}
res
}
fn get_text_info(&self) -> Vec<Text> {
let new_line = Text::Raw(Cow::from("\n"));
if let Some(ref data) = self.data {
let mut res = vec![
Text::Styled(
Cow::from(strings::commit::DETAILS_AUTHOR),
self.theme.text(false, false),
),
Text::Styled(
Cow::from(format!(
"{} <{}>",
data.author.name, data.author.email
)),
self.theme.text(true, false),
),
new_line.clone(),
Text::Styled(
Cow::from(strings::commit::DETAILS_DATE),
self.theme.text(false, false),
),
Text::Styled(
Cow::from(time_to_string(
data.author.time,
false,
)),
self.theme.text(true, false),
),
new_line.clone(),
];
if let Some(ref committer) = data.committer {
res.extend(vec![
Text::Styled(
Cow::from(strings::commit::DETAILS_COMMITTER),
self.theme.text(false, false),
),
Text::Styled(
Cow::from(format!(
"{} <{}>",
committer.name, committer.email
)),
self.theme.text(true, false),
),
new_line.clone(),
Text::Styled(
Cow::from(strings::commit::DETAILS_DATE),
self.theme.text(false, false),
),
Text::Styled(
Cow::from(time_to_string(
committer.time,
false,
)),
self.theme.text(true, false),
),
new_line.clone(),
]);
}
res.extend(vec![
Text::Styled(
Cow::from(strings::commit::DETAILS_SHA),
self.theme.text(false, false),
),
Text::Styled(
Cow::from(data.hash.clone()),
self.theme.text(true, false),
),
new_line.clone(),
]);
if !self.tags.is_empty() {
res.push(Text::Styled(
Cow::from(strings::commit::DETAILS_TAGS),
self.theme.text(false, false),
));
for tag in &self.tags {
res.push(Text::Styled(
Cow::from(tag),
self.theme.text(true, false),
));
}
}
res
} else {
vec![]
}
}
///
pub fn any_work_pending(&self) -> bool {
self.git_commit_files.is_pending()
}
}

View File

@ -30,7 +30,7 @@ pub struct CommitList {
count_total: usize,
items: ItemBatch,
scroll_state: (Instant, f32),
tags: Tags,
tags: Option<Tags>,
current_size: (u16, u16),
scroll_top: usize,
theme: Theme,
@ -44,7 +44,7 @@ impl CommitList {
selection: 0,
count_total: 0,
scroll_state: (Instant::now(), 0_f32),
tags: Tags::new(),
tags: None,
current_size: (0, 0),
scroll_top: 0,
theme: *theme,
@ -79,13 +79,24 @@ impl CommitList {
}
///
pub const fn tags(&self) -> &Tags {
&self.tags
pub fn tags(&self) -> Option<&Tags> {
self.tags.as_ref()
}
///
pub fn has_tags(&self) -> bool {
self.tags.is_some()
}
///
pub fn clear(&mut self) {
self.tags = None;
self.items.clear();
}
///
pub fn set_tags(&mut self, tags: Tags) {
self.tags = tags;
self.tags = Some(tags);
}
///
@ -225,7 +236,9 @@ impl CommitList {
.take(height)
.enumerate()
{
let tag = if let Some(tags) = self.tags.get(&e.id) {
let tags = if let Some(tags) =
self.tags.as_ref().and_then(|t| t.get(&e.id))
{
Some(tags.join(" "))
} else {
None
@ -235,7 +248,7 @@ impl CommitList {
e,
idx + self.scroll_top == selection,
&mut txt,
tag,
tags,
&self.theme,
);
}

View File

@ -138,13 +138,9 @@ impl FileTreeComponent {
format!("{} {}{}", status_char, indent_str, file)
};
let status = status_item
.status
.unwrap_or(StatusItemType::Modified);
Some(Text::Styled(
Cow::from(txt),
theme.item(status, selected),
theme.item(status_item.status, selected),
))
}
@ -175,17 +171,13 @@ impl FileTreeComponent {
}
}
fn item_status_char(item_type: Option<StatusItemType>) -> char {
if let Some(item_type) = item_type {
match item_type {
StatusItemType::Modified => 'M',
StatusItemType::New => '+',
StatusItemType::Deleted => '-',
StatusItemType::Renamed => 'R',
_ => ' ',
}
} else {
' '
fn item_status_char(item_type: StatusItemType) -> char {
match item_type {
StatusItemType::Modified => 'M',
StatusItemType::New => '+',
StatusItemType::Deleted => '-',
StatusItemType::Renamed => 'R',
_ => ' ',
}
}
}

View File

@ -157,7 +157,7 @@ impl Component for HelpComponent {
}
impl HelpComponent {
pub fn new(theme: &Theme) -> Self {
pub const fn new(theme: &Theme) -> Self {
Self {
cmds: vec![],
visible: false,

View File

@ -1,6 +1,7 @@
mod changes;
mod command;
mod commit;
mod commit_details;
mod commitlist;
mod diff;
mod filetree;
@ -14,6 +15,7 @@ use anyhow::Result;
pub use changes::ChangesComponent;
pub use command::{CommandInfo, CommandText};
pub use commit::CommitComponent;
pub use commit_details::CommitDetailsComponent;
pub use commitlist::CommitList;
use crossterm::event::Event;
pub use diff::DiffComponent;

View File

@ -242,13 +242,14 @@ impl Index<usize> for FileTreeItems {
#[cfg(test)]
mod tests {
use super::*;
use asyncgit::StatusItemType;
fn string_vec_to_status(items: &[&str]) -> Vec<StatusItem> {
items
.iter()
.map(|a| StatusItem {
path: String::from(*a),
status: None,
status: StatusItemType::Modified,
})
.collect::<Vec<_>>()
}

View File

@ -1,5 +1,5 @@
use super::time_to_string;
use asyncgit::sync::{CommitId, CommitInfo};
use chrono::prelude::*;
use std::slice::Iter;
static SLICE_OFFSET_RELOAD_THRESHOLD: usize = 100;
@ -14,18 +14,12 @@ pub struct LogEntry {
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,
));
let hash = c.id.to_string().chars().take(7).collect();
Self {
author: c.author,
msg: c.message,
time: time.format("%Y-%m-%d %H:%M:%S").to_string(),
time: time_to_string(c.time, true),
hash_short: hash,
id: c.id,
}

View File

@ -1,3 +1,19 @@
use chrono::{DateTime, Local, NaiveDateTime, Utc};
pub mod filetree;
pub mod logitems;
pub mod statustree;
/// helper func to convert unix time since epoch to formated time string in local timezone
pub fn time_to_string(secs: i64, short: bool) -> String {
let time = DateTime::<Local>::from(DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(secs, 0),
Utc,
));
time.format(if short {
"%Y-%m-%d"
} else {
"%Y-%m-%d %H:%M:%S"
})
.to_string()
}

View File

@ -325,13 +325,14 @@ impl StatusTree {
#[cfg(test)]
mod tests {
use super::*;
use asyncgit::StatusItemType;
fn string_vec_to_status(items: &[&str]) -> Vec<StatusItem> {
items
.iter()
.map(|a| StatusItem {
path: String::from(*a),
status: None,
status: StatusItemType::Modified,
})
.collect::<Vec<_>>()
}

View File

@ -60,3 +60,4 @@ pub const STASH_APPLY: KeyEvent = no_mod(KeyCode::Enter);
pub const STASH_DROP: KeyEvent =
with_mod(KeyCode::Char('D'), KeyModifiers::SHIFT);
pub const CMD_BAR_TOGGLE: KeyEvent = no_mod(KeyCode::Char('.'));
pub const LOG_COMMIT_DETAILS: KeyEvent = no_mod(KeyCode::Enter);

View File

@ -28,6 +28,19 @@ pub static HELP_TITLE: &str = "Help: all commands";
pub static STASHING_FILES_TITLE: &str = "Files to Stash";
pub static STASHING_OPTIONS_TITLE: &str = "Options";
pub mod commit {
pub static DETAILS_AUTHOR: &str = "Author: ";
pub static DETAILS_COMMITTER: &str = "Committer: ";
pub static DETAILS_SHA: &str = "SHA: ";
pub static DETAILS_DATE: &str = "Date: ";
pub static DETAILS_TAGS: &str = "Tags: ";
pub static DETAILS_INFO_TITLE: &str = "Info";
pub static DETAILS_MESSAGE_TITLE: &str = "Message";
pub static DETAILS_FILES_TITLE: &str = "Files:";
pub static DETAILS_FILES_LOADING_TITLE: &str = "Files: loading";
}
pub mod order {
pub static NAV: i8 = 1;
}
@ -41,6 +54,7 @@ pub mod commands {
static CMD_GROUP_COMMIT: &str = "Commit";
static CMD_GROUP_STASHING: &str = "Stashing";
static CMD_GROUP_STASHES: &str = "Stashes";
static CMD_GROUP_LOG: &str = "Log";
///
pub static TOGGLE_TABS: CommandText = CommandText::new(
@ -233,4 +247,11 @@ pub mod commands {
"drop selected stash",
CMD_GROUP_STASHES,
);
///
pub static LOG_DETAILS_OPEN: CommandText = CommandText::new(
"Details [enter]",
"open details of selected commit",
CMD_GROUP_LOG,
);
}

View File

@ -1,24 +1,32 @@
use crate::{
components::{
visibility_blocking, CommandBlocking, CommandInfo,
CommitList, Component, DrawableComponent,
CommitDetailsComponent, CommitList, Component,
DrawableComponent,
},
strings,
keys, strings,
ui::style::Theme,
};
use anyhow::Result;
use asyncgit::{sync, AsyncLog, AsyncNotification, FetchStatus, CWD};
use crossbeam_channel::Sender;
use crossterm::event::Event;
use tui::{backend::Backend, layout::Rect, Frame};
use strings::commands;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
Frame,
};
const SLICE_SIZE: usize = 1200;
///
pub struct Revlog {
commit_details: CommitDetailsComponent,
list: CommitList,
git_log: AsyncLog,
visible: bool,
details_open: bool,
}
impl Revlog {
@ -28,15 +36,20 @@ impl Revlog {
theme: &Theme,
) -> Self {
Self {
commit_details: CommitDetailsComponent::new(
sender, theme,
),
list: CommitList::new(strings::LOG_TITLE, theme),
git_log: AsyncLog::new(sender.clone()),
git_log: AsyncLog::new(sender),
visible: false,
details_open: false,
}
}
///
pub fn any_work_pending(&self) -> bool {
self.git_log.is_pending()
|| self.commit_details.any_work_pending()
}
///
@ -55,9 +68,32 @@ impl Revlog {
self.fetch_commits()?;
}
if self.list.tags().is_empty() {
if !self.list.has_tags() || log_changed {
self.list.set_tags(sync::get_tags(CWD)?);
}
if self.details_open {
self.commit_details.set_commit(
self.list.selected_entry().map(|e| e.id),
self.list.tags().expect("tags"),
)?;
}
}
Ok(())
}
///
pub fn update_git(
&mut self,
ev: AsyncNotification,
) -> Result<()> {
if self.visible {
match ev {
AsyncNotification::CommitFiles
| AsyncNotification::Log => self.update()?,
_ => (),
}
}
Ok(())
@ -87,7 +123,23 @@ impl DrawableComponent for Revlog {
f: &mut Frame<B>,
area: Rect,
) -> Result<()> {
self.list.draw(f, area)?;
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage(60),
Constraint::Percentage(40),
]
.as_ref(),
)
.split(area);
if self.details_open {
self.list.draw(f, chunks[0])?;
self.commit_details.draw(f, chunks[1])?;
} else {
self.list.draw(f, area)?;
}
Ok(())
}
@ -96,13 +148,16 @@ impl DrawableComponent for Revlog {
impl Component for Revlog {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.visible {
let needs_update = self.list.event(ev)?;
let event_used = self.list.event(ev)?;
if needs_update {
if event_used {
self.update()?;
return Ok(true);
} else if let Event::Key(keys::LOG_COMMIT_DETAILS) = ev {
self.details_open = !self.details_open;
self.update()?;
return Ok(true);
}
return Ok(needs_update);
}
Ok(false)
@ -117,6 +172,12 @@ impl Component for Revlog {
self.list.commands(out, force_all);
}
out.push(CommandInfo::new(
commands::LOG_DETAILS_OPEN,
true,
self.visible,
));
visibility_blocking(self)
}
@ -131,7 +192,7 @@ impl Component for Revlog {
fn show(&mut self) -> Result<()> {
self.visible = true;
self.list.items().clear();
self.list.clear();
self.update()?;
Ok(())