Support stash-apply,stash-drop

This commit is contained in:
Stephan Dilly 2020-05-25 12:31:46 +02:00
parent 01a354e171
commit 63d3bf5661
21 changed files with 673 additions and 249 deletions

View file

@ -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::<Vec<_>>();

View file

@ -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,

View file

@ -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<StashItem>);
///
#[allow(dead_code)]
pub fn get_stashes(repo_path: &str) -> Result<StashItems> {
pub fn get_stashes(repo_path: &str) -> Result<Vec<Oid>> {
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<usize> {
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(())
}

View file

@ -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<String, Vec<String>>;
pub type Tags = HashMap<CommitId, Vec<String>>;
/// returns `Tags` type filled with all tags found in repo
pub fn get_tags(repo_path: &str) -> Result<Tags> {
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<Tags> {
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"]
);
}

View file

@ -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<NeedsUpdate> {
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<B: Backend>(&self, f: &mut Frame<B>, 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(

View file

@ -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;

View file

@ -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

View file

@ -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<AsyncNotification>,
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<bool> {
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<B: Backend>(
&mut self,
f: &mut Frame<B>,
@ -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<bool> {
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<CommandInfo>,
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
}
}

View file

@ -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;

View file

@ -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<ResetItem>,
target: Option<Action>,
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,
),
};
}
("", "")
}
}

View file

@ -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;
}
}

View file

@ -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<CommitInfo> for LogEntry {
@ -19,18 +19,22 @@ impl From<CommitInfo> 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<LogEntry>,
}

View file

@ -1,2 +1,3 @@
pub mod filetree;
pub mod logitems;
pub mod statustree;

View file

@ -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);

View file

@ -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,
}
///

View file

@ -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,
);
}

View file

@ -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;

139
src/tabs/revlog.rs Normal file
View file

@ -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<AsyncNotification>,
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<B: Backend>(
&mut self,
f: &mut Frame<B>,
area: Rect,
) -> Result<()> {
self.list.draw(f, area)?;
Ok(())
}
}
impl Component for Revlog {
fn event(&mut self, ev: Event) -> Result<bool> {
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<CommandInfo>,
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(())
}
}

View file

@ -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<CommandInfo>,
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)
}

152
src/tabs/stashlist.rs Normal file
View file

@ -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<B: tui::backend::Backend>(
&mut self,
f: &mut tui::Frame<B>,
rect: tui::layout::Rect,
) -> Result<()> {
self.list.draw(f, rect)?;
Ok(())
}
}
impl Component for StashList {
fn commands(
&self,
out: &mut Vec<CommandInfo>,
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<bool> {
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(())
}
}

View file

@ -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 {