mirror of
https://github.com/extrawurst/gitui.git
synced 2024-12-26 18:43:37 +03:00
commit details in log (#107)
This commit is contained in:
parent
3dc1119142
commit
5bfdb40a14
12
.github/workflows/cd.yml
vendored
12
.github/workflows/cd.yml
vendored
@ -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
|
||||
|
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@ -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:
|
||||
|
@ -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))
|
||||
|
1
Makefile
1
Makefile
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
101
asyncgit/src/commit_files.rs
Normal file
101
asyncgit/src/commit_files.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -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 `./`
|
||||
|
@ -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)),
|
||||
}
|
||||
|
99
asyncgit/src/sync/commit_details.rs
Normal file
99
asyncgit/src/sync/commit_details.rs
Normal 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)
|
||||
}
|
82
asyncgit/src/sync/commit_files.rs
Normal file
82
asyncgit/src/sync/commit_files.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -25,6 +25,12 @@ impl ToString for CommitId {
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Oid> for CommitId {
|
||||
fn into(self) -> Oid {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Debug)]
|
||||
pub struct CommitInfo {
|
||||
|
@ -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};
|
||||
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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(())
|
||||
|
@ -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(
|
||||
|
279
src/components/commit_details.rs
Normal file
279
src/components/commit_details.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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',
|
||||
_ => ' ',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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<_>>()
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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<_>>()
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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(())
|
||||
|
Loading…
Reference in New Issue
Block a user