diff --git a/asyncgit/src/sync/commits_info.rs b/asyncgit/src/sync/commits_info.rs index f37811b9..5c1b22a7 100644 --- a/asyncgit/src/sync/commits_info.rs +++ b/asyncgit/src/sync/commits_info.rs @@ -3,6 +3,28 @@ use crate::error::Result; use git2::{Commit, Error, Oid}; use scopetime::scope_time; +/// identifies a single commit +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct CommitId(Oid); + +impl CommitId { + /// create new CommitId + pub fn new(id: Oid) -> Self { + Self(id) + } + + /// + pub(crate) fn get_oid(self) -> Oid { + self.0 + } +} + +impl ToString for CommitId { + fn to_string(&self) -> String { + self.0.to_string() + } +} + /// #[derive(Debug)] pub struct CommitInfo { @@ -13,7 +35,7 @@ pub struct CommitInfo { /// pub author: String, /// - pub hash: String, + pub id: CommitId, } /// @@ -44,7 +66,7 @@ pub fn get_commits_info( message, author, time: c.time().seconds(), - hash: c.id().to_string(), + id: CommitId(c.id()), } }) .collect::>(); diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 52c5ab9c..e7c90942 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -11,14 +11,14 @@ pub mod status; mod tags; pub mod utils; -pub use commits_info::{get_commits_info, CommitInfo}; +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}; pub use logwalker::LogWalker; pub use reset::{ reset_stage, reset_workdir_file, reset_workdir_folder, }; -pub use stash::stash_save; +pub use stash::{get_stashes, stash_apply, stash_drop, stash_save}; pub use tags::{get_tags, Tags}; pub use utils::{ commit, stage_add_all, stage_add_file, stage_addremoved, diff --git a/asyncgit/src/sync/stash.rs b/asyncgit/src/sync/stash.rs index 088f8d75..f856b105 100644 --- a/asyncgit/src/sync/stash.rs +++ b/asyncgit/src/sync/stash.rs @@ -1,39 +1,71 @@ -use super::utils::repo; -use crate::error::Result; -use git2::{Oid, StashFlags}; +use super::{utils::repo, CommitId}; +use crate::error::{Error, Result}; +use git2::{Oid, Repository, StashFlags}; use scopetime::scope_time; /// -#[allow(dead_code)] -pub struct StashItem { - pub msg: String, - index: usize, - id: Oid, -} - -/// -#[allow(dead_code)] -pub struct StashItems(Vec); - -/// -#[allow(dead_code)] -pub fn get_stashes(repo_path: &str) -> Result { +pub fn get_stashes(repo_path: &str) -> Result> { scope_time!("get_stashes"); let mut repo = repo(repo_path)?; let mut list = Vec::new(); - repo.stash_foreach(|index, msg, id| { - list.push(StashItem { - msg: msg.to_string(), - index, - id: *id, - }); + repo.stash_foreach(|_index, _msg, id| { + list.push(*id); true })?; - Ok(StashItems(list)) + Ok(list) +} + +/// +pub fn stash_drop(repo_path: &str, stash_id: CommitId) -> Result<()> { + scope_time!("stash_drop"); + + let mut repo = repo(repo_path)?; + + let index = get_stash_index(&mut repo, stash_id.get_oid())?; + + repo.stash_drop(index)?; + + Ok(()) +} + +/// +pub fn stash_apply( + repo_path: &str, + stash_id: CommitId, +) -> Result<()> { + scope_time!("stash_apply"); + + let mut repo = repo(repo_path)?; + + let index = get_stash_index(&mut repo, stash_id.get_oid())?; + + repo.stash_apply(index, None)?; + + Ok(()) +} + +fn get_stash_index( + repo: &mut Repository, + stash_id: Oid, +) -> Result { + let mut idx = None; + + repo.stash_foreach(|index, _msg, id| { + if *id == stash_id { + idx = Some(index); + false + } else { + true + } + })?; + + idx.ok_or_else(|| { + Error::Generic("stash commit not found".to_string()) + }) } /// @@ -66,7 +98,10 @@ pub fn stash_save( #[cfg(test)] mod tests { use super::*; - use crate::sync::tests::{get_statuses, repo_init}; + use crate::sync::{ + get_commits_info, + tests::{get_statuses, repo_init}, + }; use std::{fs::File, io::Write}; #[test] @@ -80,10 +115,7 @@ mod tests { false ); - assert_eq!( - get_stashes(repo_path).unwrap().0.is_empty(), - true - ); + assert_eq!(get_stashes(repo_path).unwrap().is_empty(), true); } #[test] @@ -117,9 +149,28 @@ mod tests { let res = get_stashes(repo_path)?; - assert_eq!(res.0.len(), 1); - assert_eq!(res.0[0].msg, "On master: foo"); - assert_eq!(res.0[0].index, 0); + assert_eq!(res.len(), 1); + + let infos = + get_commits_info(repo_path, &[res[0]], 100).unwrap(); + + assert_eq!(infos[0].message, "On master: foo"); + + Ok(()) + } + + #[test] + fn test_stash_nothing_untracked() -> Result<()> { + 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("foo.txt"))? + .write_all(b"test\nfoo")?; + + assert!( + stash_save(repo_path, Some("foo"), false, false).is_err() + ); Ok(()) } diff --git a/asyncgit/src/sync/tags.rs b/asyncgit/src/sync/tags.rs index ae84257f..4fa09fde 100644 --- a/asyncgit/src/sync/tags.rs +++ b/asyncgit/src/sync/tags.rs @@ -1,17 +1,17 @@ -use super::utils::repo; +use super::{utils::repo, CommitId}; use crate::error::Result; use scopetime::scope_time; use std::collections::HashMap; /// hashmap of tag target commit hash to tag names -pub type Tags = HashMap>; +pub type Tags = HashMap>; /// returns `Tags` type filled with all tags found in repo pub fn get_tags(repo_path: &str) -> Result { scope_time!("get_tags"); let mut res = Tags::new(); - let mut adder = |key: String, value: String| { + let mut adder = |key, value: String| { if let Some(key) = res.get_mut(&key) { key.push(value) } else { @@ -26,9 +26,8 @@ pub fn get_tags(repo_path: &str) -> Result { let obj = repo.revparse_single(name)?; if let Some(tag) = obj.as_tag() { - let target_hash = tag.target_id().to_string(); let tag_name = String::from(name); - adder(target_hash, tag_name); + adder(CommitId::new(tag.target_id()), tag_name); } } } @@ -70,7 +69,7 @@ mod tests { repo.tag("b", &target, &sig, "", false).unwrap(); assert_eq!( - get_tags(repo_path).unwrap()[&head_id.to_string()], + get_tags(repo_path).unwrap()[&CommitId::new(head_id)], vec!["a", "b"] ); } diff --git a/src/app.rs b/src/app.rs index af740666..a56d6d91 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,9 +6,9 @@ use crate::{ ResetComponent, StashMsgComponent, }, keys, - queue::{InternalEvent, NeedsUpdate, Queue}, + queue::{Action, InternalEvent, NeedsUpdate, Queue}, strings, - tabs::{Revlog, Stashing, Status}, + tabs::{Revlog, StashList, Stashing, Status}, ui::style::Theme, }; use anyhow::{anyhow, Result}; @@ -39,6 +39,7 @@ pub struct App { revlog: Revlog, status_tab: Status, stashing_tab: Stashing, + stashlist_tab: StashList, queue: Queue, theme: Theme, } @@ -66,6 +67,7 @@ impl App { revlog: Revlog::new(&sender, &theme), status_tab: Status::new(&sender, &queue, &theme), stashing_tab: Stashing::new(&sender, &queue, &theme), + stashlist_tab: StashList::new(&queue, &theme), queue, theme, } @@ -95,6 +97,7 @@ impl App { 0 => self.status_tab.draw(f, chunks_main[1])?, 1 => self.revlog.draw(f, chunks_main[1])?, 2 => self.stashing_tab.draw(f, chunks_main[1])?, + 3 => self.stashlist_tab.draw(f, chunks_main[1])?, _ => return Err(anyhow!("unknown tab")), }; @@ -158,6 +161,7 @@ impl App { self.status_tab.update()?; self.revlog.update()?; self.stashing_tab.update()?; + self.stashlist_tab.update()?; Ok(()) } @@ -207,7 +211,8 @@ impl App { help, revlog, status_tab, - stashing_tab + stashing_tab, + stashlist_tab ] ); @@ -226,6 +231,7 @@ impl App { &mut self.status_tab, &mut self.revlog, &mut self.stashing_tab, + &mut self.stashlist_tab, ] } @@ -234,16 +240,21 @@ impl App { { let tabs = self.get_tabs(); new_tab %= tabs.len(); + } + self.set_tab(new_tab) + } - for (i, t) in tabs.into_iter().enumerate() { - if new_tab == i { - t.show()?; - } else { - t.hide(); - } + fn set_tab(&mut self, tab: usize) -> Result<()> { + let tabs = self.get_tabs(); + for (i, t) in tabs.into_iter().enumerate() { + if tab == i { + t.show()?; + } else { + t.hide(); } } - self.tab = new_tab; + + self.tab = tab; Ok(()) } @@ -275,27 +286,20 @@ impl App { ) -> Result { let mut flags = NeedsUpdate::empty(); match ev { - InternalEvent::ResetItem(reset_item) => { - if reset_item.is_folder { - if sync::reset_workdir_folder( - CWD, - reset_item.path.as_str(), - ) - .is_ok() - { + InternalEvent::ConfirmedAction(action) => match action { + Action::Reset(r) => { + if Status::reset(&r) { flags.insert(NeedsUpdate::ALL); } - } else if sync::reset_workdir_file( - CWD, - reset_item.path.as_str(), - ) - .is_ok() - { - flags.insert(NeedsUpdate::ALL); } - } - InternalEvent::ConfirmResetItem(reset_item) => { - self.reset.open_for_path(reset_item)?; + Action::StashDrop(s) => { + if StashList::drop(s) { + flags.insert(NeedsUpdate::ALL); + } + } + }, + InternalEvent::ConfirmAction(action) => { + self.reset.open(action)?; flags.insert(NeedsUpdate::COMMANDS); } InternalEvent::AddHunk(hash) => { @@ -315,13 +319,16 @@ impl App { } InternalEvent::ShowErrorMsg(msg) => { self.msg.show_msg(msg.as_str())?; - flags.insert(NeedsUpdate::ALL); + flags + .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); } InternalEvent::Update(u) => flags.insert(u), InternalEvent::OpenCommit => self.commit.show()?, - InternalEvent::PopupStashing(_opts) => { + InternalEvent::PopupStashing(opts) => { + self.stashmsg_popup.options(opts); self.stashmsg_popup.show()? } + InternalEvent::TabSwitch => self.set_tab(0)?, }; Ok(flags) @@ -382,6 +389,7 @@ impl App { Ok(()) } + //TODO: make this dynamic fn draw_tabs(&self, f: &mut Frame, r: Rect) { f.render_widget( Tabs::default() @@ -390,6 +398,7 @@ impl App { strings::TAB_STATUS, strings::TAB_LOG, strings::TAB_STASHING, + "Stashes", ]) .style(Style::default()) .highlight_style( diff --git a/src/components/changes.rs b/src/components/changes.rs index 259ffba7..c2adc9ea 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -6,7 +6,7 @@ use super::{ use crate::{ components::{CommandInfo, Component}, keys, - queue::{InternalEvent, NeedsUpdate, Queue, ResetItem}, + queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem}, strings, ui::style::Theme, }; @@ -112,10 +112,12 @@ impl ChangesComponent { let is_folder = matches!(tree_item.kind, FileTreeItemKind::Path(_)); self.queue.borrow_mut().push_back( - InternalEvent::ConfirmResetItem(ResetItem { - path: tree_item.info.full_path, - is_folder, - }), + InternalEvent::ConfirmAction(Action::Reset( + ResetItem { + path: tree_item.info.full_path, + is_folder, + }, + )), ); return true; diff --git a/src/components/command.rs b/src/components/command.rs index 22be5078..a6268bfa 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -41,6 +41,7 @@ pub struct CommandInfo { pub enabled: bool, /// will show up in the quick bar pub quick_bar: bool, + /// available in current app state pub available: bool, /// used to order commands in quickbar diff --git a/src/tabs/revlog/mod.rs b/src/components/commitlist.rs similarity index 62% rename from src/tabs/revlog/mod.rs rename to src/components/commitlist.rs index abde8257..68e7d01b 100644 --- a/src/tabs/revlog/mod.rs +++ b/src/components/commitlist.rs @@ -1,18 +1,16 @@ -mod utils; - +use super::utils::logitems::{ItemBatch, LogEntry}; use crate::{ components::{ CommandBlocking, CommandInfo, Component, DrawableComponent, ScrollType, }, keys, - strings::{self, commands}, + strings::commands, ui::calc_scroll_top, ui::style::Theme, }; use anyhow::Result; -use asyncgit::{sync, AsyncLog, AsyncNotification, FetchStatus, CWD}; -use crossbeam_channel::Sender; +use asyncgit::sync; use crossterm::event::Event; use std::{borrow::Cow, cmp, convert::TryFrom, time::Instant}; use sync::Tags; @@ -22,18 +20,15 @@ use tui::{ widgets::{Block, Borders, Paragraph, Text}, Frame, }; -use utils::{ItemBatch, LogEntry}; -const SLICE_SIZE: usize = 1200; const ELEMENTS_PER_LINE: usize = 10; /// -pub struct Revlog { +pub struct CommitList { + title: String, selection: usize, count_total: usize, items: ItemBatch, - git_log: AsyncLog, - visible: bool, scroll_state: (Instant, f32), tags: Tags, current_size: (u16, u16), @@ -41,76 +36,63 @@ pub struct Revlog { theme: Theme, } -impl Revlog { +impl CommitList { /// - pub fn new( - sender: &Sender, - theme: &Theme, - ) -> Self { + pub fn new(title: &str, theme: &Theme) -> Self { Self { items: ItemBatch::default(), - git_log: AsyncLog::new(sender.clone()), selection: 0, count_total: 0, - visible: false, scroll_state: (Instant::now(), 0_f32), tags: Tags::new(), current_size: (0, 0), scroll_top: 0, theme: *theme, + title: String::from(title), } } /// - pub fn any_work_pending(&self) -> bool { - self.git_log.is_pending() + pub fn items(&mut self) -> &mut ItemBatch { + &mut self.items } - fn selection_max(&self) -> usize { + /// + pub fn selection(&self) -> usize { + self.selection + } + + /// + pub fn current_size(&self) -> (u16, u16) { + self.current_size + } + + /// + pub fn set_count_total(&mut self, total: usize) { + self.count_total = total; + } + + /// + pub fn selection_max(&self) -> usize { self.count_total.saturating_sub(1) } /// - pub fn update(&mut self) -> Result<()> { - if self.visible { - let log_changed = - self.git_log.fetch()? == FetchStatus::Started; - - self.count_total = self.git_log.count()?; - - if self - .items - .needs_data(self.selection, self.selection_max()) - || log_changed - { - self.fetch_commits()?; - } - - if self.tags.is_empty() { - self.tags = sync::get_tags(CWD)?; - } - } - - Ok(()) + pub fn tags(&self) -> &Tags { + &self.tags } - fn fetch_commits(&mut self) -> Result<()> { - let want_min = self.selection.saturating_sub(SLICE_SIZE / 2); - - let commits = sync::get_commits_info( - CWD, - &self.git_log.get_slice(want_min, SLICE_SIZE)?, - self.current_size.0.into(), - ); - - if let Ok(commits) = commits { - self.items.set_items(want_min, commits); - } - - Ok(()) + /// + pub fn set_tags(&mut self, tags: Tags) { + self.tags = tags; } - fn move_selection(&mut self, scroll: ScrollType) -> Result<()> { + /// + pub fn selected_entry(&self) -> Option<&LogEntry> { + self.items.iter().nth(self.selection) + } + + fn move_selection(&mut self, scroll: ScrollType) -> Result { self.update_scroll_speed(); #[allow(clippy::cast_possible_truncation)] @@ -120,7 +102,7 @@ impl Revlog { let page_offset = usize::from(self.current_size.1).saturating_sub(1); - self.selection = match scroll { + let new_selection = match scroll { ScrollType::Up => { self.selection.saturating_sub(speed_int) } @@ -137,12 +119,14 @@ impl Revlog { ScrollType::End => self.selection_max(), }; - self.selection = - cmp::min(self.selection, self.selection_max()); + let new_selection = + cmp::min(new_selection, self.selection_max()); - self.update()?; + let needs_update = new_selection != self.selection; - Ok(()) + self.selection = new_selection; + + Ok(needs_update) } fn update_scroll_speed(&mut self) { @@ -184,7 +168,7 @@ impl Revlog { // commit hash txt.push(Text::Styled( - Cow::from(&e.hash[0..7]), + Cow::from(e.hash_short.as_str()), theme.commit_hash(selected), )); @@ -238,7 +222,7 @@ impl Revlog { .take(height) .enumerate() { - let tag = if let Some(tags) = self.tags.get(&e.hash) { + let tag = if let Some(tags) = self.tags.get(&e.id) { Some(tags.join(" ")) } else { None @@ -261,7 +245,7 @@ impl Revlog { } } -impl DrawableComponent for Revlog { +impl DrawableComponent for CommitList { fn draw( &mut self, f: &mut Frame, @@ -283,7 +267,7 @@ impl DrawableComponent for Revlog { let title = format!( "{} {}/{}", - strings::LOG_TITLE, + self.title, self.count_total.saturating_sub(self.selection), self.count_total, ); @@ -305,38 +289,32 @@ impl DrawableComponent for Revlog { } } -impl Component for Revlog { +impl Component for CommitList { fn event(&mut self, ev: Event) -> Result { - if self.visible { - if let Event::Key(k) = ev { - return match k { - keys::MOVE_UP => { - self.move_selection(ScrollType::Up)?; - Ok(true) - } - keys::MOVE_DOWN => { - self.move_selection(ScrollType::Down)?; - Ok(true) - } - keys::SHIFT_UP | keys::HOME => { - self.move_selection(ScrollType::Home)?; - Ok(true) - } - keys::SHIFT_DOWN | keys::END => { - self.move_selection(ScrollType::End)?; - Ok(true) - } - keys::PAGE_UP => { - self.move_selection(ScrollType::PageUp)?; - Ok(true) - } - keys::PAGE_DOWN => { - self.move_selection(ScrollType::PageDown)?; - Ok(true) - } - _ => Ok(false), - }; - } + if let Event::Key(k) = ev { + let selection_changed = match k { + keys::MOVE_UP => { + self.move_selection(ScrollType::Up)? + } + keys::MOVE_DOWN => { + self.move_selection(ScrollType::Down)? + } + keys::SHIFT_UP | keys::HOME => { + self.move_selection(ScrollType::Home)? + } + keys::SHIFT_DOWN | keys::END => { + self.move_selection(ScrollType::End)? + } + keys::PAGE_UP => { + self.move_selection(ScrollType::PageUp)? + } + keys::PAGE_DOWN => { + self.move_selection(ScrollType::PageDown)? + } + _ => false, + }; + + return Ok(selection_changed); } Ok(false) @@ -345,35 +323,13 @@ impl Component for Revlog { fn commands( &self, out: &mut Vec, - force_all: bool, + _force_all: bool, ) -> CommandBlocking { out.push(CommandInfo::new( commands::SCROLL, - self.visible, - self.visible || force_all, + self.selected_entry().is_some(), + true, )); - - if self.visible { - CommandBlocking::Blocking - } else { - CommandBlocking::PassingOn - } - } - - fn is_visible(&self) -> bool { - self.visible - } - - fn hide(&mut self) { - self.visible = false; - self.git_log.set_background(); - } - - fn show(&mut self) -> Result<()> { - self.visible = true; - self.items.clear(); - self.update()?; - - Ok(()) + CommandBlocking::PassingOn } } diff --git a/src/components/mod.rs b/src/components/mod.rs index c30072f3..6df5946a 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,6 +1,7 @@ mod changes; mod command; mod commit; +mod commitlist; mod diff; mod filetree; mod help; @@ -13,6 +14,7 @@ use anyhow::Result; pub use changes::ChangesComponent; pub use command::{CommandInfo, CommandText}; pub use commit::CommitComponent; +pub use commitlist::CommitList; use crossterm::event::Event; pub use diff::DiffComponent; pub use filetree::FileTreeComponent; diff --git a/src/components/reset.rs b/src/components/reset.rs index 61cbfeeb..fff1307e 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{ components::dialog_paragraph, - queue::{InternalEvent, Queue, ResetItem}, + queue::{Action, InternalEvent, Queue}, strings, ui, ui::style::Theme, }; @@ -21,7 +21,7 @@ use tui::{ /// pub struct ResetComponent { - target: Option, + target: Option, visible: bool, queue: Queue, theme: Theme, @@ -34,16 +34,17 @@ impl DrawableComponent for ResetComponent { _rect: Rect, ) -> Result<()> { if self.visible { - let mut txt = Vec::new(); - txt.push(Text::Styled( - Cow::from(strings::RESET_MSG), + let (title, msg) = self.get_text(); + + let txt = vec![Text::Styled( + Cow::from(msg), self.theme.text_danger(), - )); + )]; let area = ui::centered_rect(30, 20, f.size()); f.render_widget(Clear, area); f.render_widget( - dialog_paragraph(strings::RESET_TITLE, txt.iter()), + dialog_paragraph(title, txt.iter()), area, ); } @@ -120,20 +121,37 @@ impl ResetComponent { } } /// - pub fn open_for_path(&mut self, item: ResetItem) -> Result<()> { - self.target = Some(item); + pub fn open(&mut self, a: Action) -> Result<()> { + self.target = Some(a); self.show()?; Ok(()) } /// pub fn confirm(&mut self) { - if let Some(target) = self.target.take() { + if let Some(a) = self.target.take() { self.queue .borrow_mut() - .push_back(InternalEvent::ResetItem(target)); + .push_back(InternalEvent::ConfirmedAction(a)); } self.hide(); } + + fn get_text(&self) -> (&str, &str) { + if let Some(ref a) = self.target { + return match a { + Action::Reset(_) => ( + strings::CONFIRM_TITLE_RESET, + strings::CONFIRM_MSG_RESET, + ), + Action::StashDrop(_) => ( + strings::CONFIRM_TITLE_STASHDROP, + strings::CONFIRM_MSG_STASHDROP, + ), + }; + } + + ("", "") + } } diff --git a/src/components/stashmsg.rs b/src/components/stashmsg.rs index 938fe5e8..a6350887 100644 --- a/src/components/stashmsg.rs +++ b/src/components/stashmsg.rs @@ -56,7 +56,7 @@ impl Component for StashMsgComponent { if let Event::Key(e) = ev { if let KeyCode::Enter = e.code { - if sync::stash_save( + match sync::stash_save( CWD, if self.input.get_text().is_empty() { None @@ -65,15 +65,31 @@ impl Component for StashMsgComponent { }, self.options.stash_untracked, self.options.keep_index, - ) - .is_ok() - { - self.input.clear(); - self.hide(); + ) { + Ok(()) => { + self.input.clear(); + self.hide(); - self.queue.borrow_mut().push_back( - InternalEvent::Update(NeedsUpdate::ALL), - ); + self.queue.borrow_mut().push_back( + InternalEvent::Update( + NeedsUpdate::ALL, + ), + ); + } + Err(e) => { + self.hide(); + log::error!( + "e: {} (options: {:?})", + e, + self.options + ); + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg(format!( + "stash error:\n{}\noptions:\n{:?}", + e, self.options + )), + ); + } } } @@ -112,4 +128,9 @@ impl StashMsgComponent { ), } } + + /// + pub fn options(&mut self, options: StashingOptions) { + self.options = options; + } } diff --git a/src/tabs/revlog/utils.rs b/src/components/utils/logitems.rs similarity index 87% rename from src/tabs/revlog/utils.rs rename to src/components/utils/logitems.rs index 73b815a0..d6d8c9f1 100644 --- a/src/tabs/revlog/utils.rs +++ b/src/components/utils/logitems.rs @@ -1,15 +1,15 @@ -use asyncgit::sync::CommitInfo; +use asyncgit::sync::{CommitId, CommitInfo}; use chrono::prelude::*; use std::slice::Iter; static SLICE_OFFSET_RELOAD_THRESHOLD: usize = 100; -#[derive(Default)] -pub(super) struct LogEntry { +pub struct LogEntry { pub time: String, pub author: String, pub msg: String, - pub hash: String, + pub hash_short: String, + pub id: CommitId, } impl From for LogEntry { @@ -19,18 +19,22 @@ impl From for LogEntry { 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(), - hash: c.hash, + hash_short: hash, + id: c.id, } } } /// #[derive(Default)] -pub(super) struct ItemBatch { +pub struct ItemBatch { index_offset: usize, items: Vec, } diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index 606de183..2971c997 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -1,2 +1,3 @@ pub mod filetree; +pub mod logitems; pub mod statustree; diff --git a/src/keys.rs b/src/keys.rs index 6aa589ed..50df78cd 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -46,3 +46,6 @@ pub const STASHING_TOGGLE_UNTRACKED: KeyEvent = no_mod(KeyCode::Char('u')); pub const STASHING_TOGGLE_INDEX: KeyEvent = no_mod(KeyCode::Char('i')); +pub const STASH_APPLY: KeyEvent = no_mod(KeyCode::Enter); +pub const STASH_DROP: KeyEvent = + with_mod(KeyCode::Char('D'), KeyModifiers::SHIFT); diff --git a/src/queue.rs b/src/queue.rs index 929b8cef..d0d0f19c 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,4 +1,5 @@ use crate::tabs::StashingOptions; +use asyncgit::sync::CommitId; use bitflags::bitflags; use std::{cell::RefCell, collections::VecDeque, rc::Rc}; @@ -22,12 +23,18 @@ pub struct ResetItem { pub is_folder: bool, } +/// +pub enum Action { + Reset(ResetItem), + StashDrop(CommitId), +} + /// pub enum InternalEvent { /// - ConfirmResetItem(ResetItem), + ConfirmAction(Action), /// - ResetItem(ResetItem), + ConfirmedAction(Action), /// AddHunk(u64), /// @@ -38,6 +45,8 @@ pub enum InternalEvent { OpenCommit, /// PopupStashing(StashingOptions), + /// + TabSwitch, } /// diff --git a/src/strings.rs b/src/strings.rs index e8f74218..f2b0044c 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -14,10 +14,13 @@ pub static COMMIT_TITLE: &str = "Commit"; pub static COMMIT_MSG: &str = "type commit message.."; pub static STASH_POPUP_TITLE: &str = "Stash"; pub static STASH_POPUP_MSG: &str = "type name (optional)"; -pub static RESET_TITLE: &str = "Reset"; -pub static RESET_MSG: &str = "confirm file reset?"; +pub static CONFIRM_TITLE_RESET: &str = "Reset"; +pub static CONFIRM_TITLE_STASHDROP: &str = "Drop"; +pub static CONFIRM_MSG_RESET: &str = "confirm file reset?"; +pub static CONFIRM_MSG_STASHDROP: &str = "confirm stash drop?"; pub static LOG_TITLE: &str = "Commit"; +pub static STASHLIST_TITLE: &str = "Stashes"; pub static HELP_TITLE: &str = "Help: all commands"; @@ -32,6 +35,7 @@ pub mod commands { static CMD_GROUP_CHANGES: &str = "Changes"; static CMD_GROUP_COMMIT: &str = "Commit"; static CMD_GROUP_STASHING: &str = "Stashing"; + static CMD_GROUP_STASHES: &str = "Stashes"; /// pub static TOGGLE_TABS: CommandText = CommandText::new( @@ -188,4 +192,16 @@ pub mod commands { "save files to stash", CMD_GROUP_STASHING, ); + /// + pub static STASHLIST_APPLY: CommandText = CommandText::new( + "Apply [enter]", + "apply selected stash", + CMD_GROUP_STASHES, + ); + /// + pub static STASHLIST_DROP: CommandText = CommandText::new( + "Drop [D]", + "drop selected stash", + CMD_GROUP_STASHES, + ); } diff --git a/src/tabs/mod.rs b/src/tabs/mod.rs index 501f6f37..3a546fde 100644 --- a/src/tabs/mod.rs +++ b/src/tabs/mod.rs @@ -1,7 +1,9 @@ mod revlog; mod stashing; +mod stashlist; mod status; pub use revlog::Revlog; pub use stashing::{Stashing, StashingOptions}; +pub use stashlist::StashList; pub use status::Status; diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs new file mode 100644 index 00000000..0d1c7197 --- /dev/null +++ b/src/tabs/revlog.rs @@ -0,0 +1,139 @@ +use crate::{ + components::{ + visibility_blocking, CommandBlocking, CommandInfo, + CommitList, Component, DrawableComponent, + }, + 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}; + +const SLICE_SIZE: usize = 1200; + +/// +pub struct Revlog { + list: CommitList, + git_log: AsyncLog, + visible: bool, +} + +impl Revlog { + /// + pub fn new( + sender: &Sender, + theme: &Theme, + ) -> Self { + Self { + list: CommitList::new(strings::LOG_TITLE, theme), + git_log: AsyncLog::new(sender.clone()), + visible: false, + } + } + + /// + pub fn any_work_pending(&self) -> bool { + self.git_log.is_pending() + } + + /// + pub fn update(&mut self) -> Result<()> { + if self.visible { + let log_changed = + self.git_log.fetch()? == FetchStatus::Started; + + self.list.set_count_total(self.git_log.count()?); + + let selection = self.list.selection(); + let selection_max = self.list.selection_max(); + if self.list.items().needs_data(selection, selection_max) + || log_changed + { + self.fetch_commits()?; + } + + if self.list.tags().is_empty() { + self.list.set_tags(sync::get_tags(CWD)?); + } + } + + Ok(()) + } + + fn fetch_commits(&mut self) -> Result<()> { + let want_min = + self.list.selection().saturating_sub(SLICE_SIZE / 2); + + let commits = sync::get_commits_info( + CWD, + &self.git_log.get_slice(want_min, SLICE_SIZE)?, + self.list.current_size().0.into(), + ); + + if let Ok(commits) = commits { + self.list.items().set_items(want_min, commits); + } + + Ok(()) + } +} + +impl DrawableComponent for Revlog { + fn draw( + &mut self, + f: &mut Frame, + area: Rect, + ) -> Result<()> { + self.list.draw(f, area)?; + + Ok(()) + } +} + +impl Component for Revlog { + fn event(&mut self, ev: Event) -> Result { + if self.visible { + let needs_update = self.list.event(ev)?; + + if needs_update { + self.update()?; + } + + return Ok(needs_update); + } + + Ok(false) + } + + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.visible || force_all { + self.list.commands(out, force_all); + } + + visibility_blocking(self) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false; + self.git_log.set_background(); + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + self.list.items().clear(); + self.update()?; + + Ok(()) + } +} diff --git a/src/tabs/stashing.rs b/src/tabs/stashing.rs index 1930194f..5bb4b0ba 100644 --- a/src/tabs/stashing.rs +++ b/src/tabs/stashing.rs @@ -24,7 +24,7 @@ use tui::{ widgets::{Block, Borders, Paragraph, Text}, }; -#[derive(Default, Clone, Copy)] +#[derive(Default, Clone, Copy, Debug)] pub struct StashingOptions { pub stash_untracked: bool, pub keep_index: bool, @@ -171,23 +171,29 @@ impl Component for Stashing { out: &mut Vec, force_all: bool, ) -> CommandBlocking { - command_pump(out, force_all, self.components().as_slice()); + if self.visible || force_all { + command_pump( + out, + force_all, + self.components().as_slice(), + ); - out.push(CommandInfo::new( - commands::STASHING_SAVE, - self.visible && !self.index.is_empty(), - self.visible || force_all, - )); - out.push(CommandInfo::new( - commands::STASHING_TOGGLE_INDEXED, - self.visible, - self.visible || force_all, - )); - out.push(CommandInfo::new( - commands::STASHING_TOGGLE_UNTRACKED, - self.visible, - self.visible || force_all, - )); + out.push(CommandInfo::new( + commands::STASHING_SAVE, + self.visible && !self.index.is_empty(), + self.visible || force_all, + )); + out.push(CommandInfo::new( + commands::STASHING_TOGGLE_INDEXED, + self.visible, + self.visible || force_all, + )); + out.push(CommandInfo::new( + commands::STASHING_TOGGLE_UNTRACKED, + self.visible, + self.visible || force_all, + )); + } visibility_blocking(self) } diff --git a/src/tabs/stashlist.rs b/src/tabs/stashlist.rs new file mode 100644 index 00000000..1f7ffd28 --- /dev/null +++ b/src/tabs/stashlist.rs @@ -0,0 +1,152 @@ +use crate::{ + components::{ + visibility_blocking, CommandBlocking, CommandInfo, + CommitList, Component, DrawableComponent, + }, + keys, + queue::{Action, InternalEvent, Queue}, + strings, + ui::style::Theme, +}; +use anyhow::Result; +use asyncgit::sync; +use asyncgit::CWD; +use crossterm::event::Event; +use strings::commands; +use sync::CommitId; + +pub struct StashList { + list: CommitList, + visible: bool, + queue: Queue, +} + +impl StashList { + /// + pub fn new(queue: &Queue, theme: &Theme) -> Self { + Self { + visible: false, + list: CommitList::new(strings::STASHLIST_TITLE, theme), + queue: queue.clone(), + } + } + + /// + pub fn update(&mut self) -> Result<()> { + if self.visible { + let stashes = sync::get_stashes(CWD)?; + let commits = + sync::get_commits_info(CWD, stashes.as_slice(), 100)?; + + self.list.set_count_total(commits.len()); + self.list.items().set_items(0, commits); + } + + Ok(()) + } + + fn apply_stash(&mut self) { + if let Some(e) = self.list.selected_entry() { + match sync::stash_apply(CWD, e.id) { + Ok(_) => { + self.queue + .borrow_mut() + .push_back(InternalEvent::TabSwitch); + } + Err(e) => { + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg(format!( + "stash apply error:\n{}", + e, + )), + ); + } + } + } + } + + fn drop_stash(&mut self) { + if let Some(e) = self.list.selected_entry() { + self.queue.borrow_mut().push_back( + InternalEvent::ConfirmAction(Action::StashDrop(e.id)), + ); + } + } + + /// + pub fn drop(id: CommitId) -> bool { + sync::stash_drop(CWD, id).is_ok() + } +} + +impl DrawableComponent for StashList { + fn draw( + &mut self, + f: &mut tui::Frame, + rect: tui::layout::Rect, + ) -> Result<()> { + self.list.draw(f, rect)?; + + Ok(()) + } +} + +impl Component for StashList { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.visible || force_all { + self.list.commands(out, force_all); + + let selection_valid = + self.list.selected_entry().is_some(); + out.push(CommandInfo::new( + commands::STASHLIST_APPLY, + selection_valid, + true, + )); + out.push(CommandInfo::new( + commands::STASHLIST_DROP, + selection_valid, + true, + )); + } + + visibility_blocking(self) + } + + fn event(&mut self, ev: crossterm::event::Event) -> Result { + if self.visible { + if self.list.event(ev)? { + return Ok(true); + } + + if let Event::Key(k) = ev { + match k { + keys::STASH_APPLY => self.apply_stash(), + keys::STASH_DROP => self.drop_stash(), + + _ => (), + }; + } + } + + Ok(false) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false; + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + self.update()?; + Ok(()) + } +} diff --git a/src/tabs/status.rs b/src/tabs/status.rs index dc1d7377..949753f6 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -6,14 +6,15 @@ use crate::{ FileTreeItemKind, }, keys, - queue::Queue, + queue::{Queue, ResetItem}, strings, ui::style::Theme, }; use anyhow::Result; use asyncgit::{ - sync::status::StatusType, AsyncDiff, AsyncNotification, - AsyncStatus, DiffParams, StatusParams, + sync::{self, status::StatusType}, + AsyncDiff, AsyncNotification, AsyncStatus, DiffParams, + StatusParams, CWD, }; use components::{command_pump, visibility_blocking}; use crossbeam_channel::Sender; @@ -268,6 +269,16 @@ impl Status { Ok(()) } + + /// called after confirmation + pub fn reset(item: &ResetItem) -> bool { + if item.is_folder { + sync::reset_workdir_folder(CWD, item.path.as_str()) + .is_ok() + } else { + sync::reset_workdir_file(CWD, item.path.as_str()).is_ok() + } + } } impl Component for Status {