mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-22 02:12:58 +03:00
support reset from log view (#1534)
This commit is contained in:
parent
1a0167e7f8
commit
8ab62244ce
@ -7,11 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Unreleased
|
||||
|
||||
**reset to commit**
|
||||
|
||||
![reset](assets/reset_in_log.gif)
|
||||
|
||||
### Added
|
||||
* changes in commit message inside external editor [[@bc-universe]](https://github.com/bc-universe) ([#1420](https://github.com/extrawurst/gitui/issues/1420))
|
||||
* allow detaching HEAD and checking out specific commit from log view [[@fralcow]](https://github.com/fralcow) ([#1499](https://github.com/extrawurst/gitui/pull/1499))
|
||||
* add no-verify option on commits to not run hooks [[@dam5h]](https://github.com/dam5h) ([#1374](https://github.com/extrawurst/gitui/issues/1374))
|
||||
* allow `fetch` on status tab [[@alensiljak]](https://github.com/alensiljak) ([#1471](https://github.com/extrawurst/gitui/issues/1471))
|
||||
* allow reset (soft,mixed,hard) from commit log ([#1500](https://github.com/extrawurst/gitui/issues/1500))
|
||||
|
||||
### Fixes
|
||||
* commit msg history ordered the wrong way ([#1445](https://github.com/extrawurst/gitui/issues/1445))
|
||||
|
BIN
assets/reset_in_log.gif
Normal file
BIN
assets/reset_in_log.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
@ -75,7 +75,7 @@ pub use remotes::{
|
||||
};
|
||||
pub(crate) use repository::repo;
|
||||
pub use repository::{RepoPath, RepoPathRef};
|
||||
pub use reset::{reset_stage, reset_workdir};
|
||||
pub use reset::{reset_repo, reset_stage, reset_workdir};
|
||||
pub use staging::{discard_lines, stage_lines};
|
||||
pub use stash::{
|
||||
get_stashes, stash_apply, stash_drop, stash_pop, stash_save,
|
||||
@ -96,6 +96,8 @@ pub use utils::{
|
||||
stage_add_file, stage_addremoved, Head,
|
||||
};
|
||||
|
||||
pub use git2::ResetType;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::{utils::get_head_repo, RepoPath};
|
||||
use super::{utils::get_head_repo, CommitId, RepoPath};
|
||||
use crate::{error::Result, sync::repository::repo};
|
||||
use git2::{build::CheckoutBuilder, ObjectType};
|
||||
use git2::{build::CheckoutBuilder, ObjectType, ResetType};
|
||||
use scopetime::scope_time;
|
||||
|
||||
///
|
||||
@ -38,6 +38,23 @@ pub fn reset_workdir(repo_path: &RepoPath, path: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn reset_repo(
|
||||
repo_path: &RepoPath,
|
||||
commit: CommitId,
|
||||
kind: ResetType,
|
||||
) -> Result<()> {
|
||||
scope_time!("reset_repo");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let c = repo.find_commit(commit.into())?;
|
||||
|
||||
repo.reset(c.as_object(), kind, None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{reset_stage, reset_workdir};
|
||||
|
15
src/app.rs
15
src/app.rs
@ -10,7 +10,7 @@ use crate::{
|
||||
FileRevlogComponent, HelpComponent, InspectCommitComponent,
|
||||
MsgComponent, OptionsPopupComponent, PullComponent,
|
||||
PushComponent, PushTagsComponent, RenameBranchComponent,
|
||||
RevisionFilesPopup, StashMsgComponent,
|
||||
ResetPopupComponent, RevisionFilesPopup, StashMsgComponent,
|
||||
SubmodulesListComponent, TagCommitComponent,
|
||||
TagListComponent,
|
||||
},
|
||||
@ -84,6 +84,7 @@ pub struct App {
|
||||
options_popup: OptionsPopupComponent,
|
||||
submodule_popup: SubmodulesListComponent,
|
||||
tags_popup: TagListComponent,
|
||||
reset_popup: ResetPopupComponent,
|
||||
cmdbar: RefCell<CommandBar>,
|
||||
tab: usize,
|
||||
revlog: Revlog,
|
||||
@ -204,6 +205,12 @@ impl App {
|
||||
theme.clone(),
|
||||
key_config.clone(),
|
||||
),
|
||||
reset_popup: ResetPopupComponent::new(
|
||||
&queue,
|
||||
&repo,
|
||||
theme.clone(),
|
||||
key_config.clone(),
|
||||
),
|
||||
pull_popup: PullComponent::new(
|
||||
&repo,
|
||||
&queue,
|
||||
@ -484,6 +491,7 @@ impl App {
|
||||
self.files_tab.update()?;
|
||||
self.stashing_tab.update()?;
|
||||
self.stashlist_tab.update()?;
|
||||
self.reset_popup.update()?;
|
||||
|
||||
self.update_commands();
|
||||
|
||||
@ -590,6 +598,7 @@ impl App {
|
||||
revision_files_popup,
|
||||
submodule_popup,
|
||||
tags_popup,
|
||||
reset_popup,
|
||||
options_popup,
|
||||
help,
|
||||
revlog,
|
||||
@ -615,6 +624,7 @@ impl App {
|
||||
select_branch_popup,
|
||||
submodule_popup,
|
||||
tags_popup,
|
||||
reset_popup,
|
||||
create_branch_popup,
|
||||
rename_branch_popup,
|
||||
revision_files_popup,
|
||||
@ -926,6 +936,9 @@ impl App {
|
||||
self.do_quit =
|
||||
QuitState::OpenSubmodule(submodule_repo_path);
|
||||
}
|
||||
InternalEvent::OpenResetPopup(id) => {
|
||||
self.reset_popup.open(id)?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(flags)
|
||||
|
@ -22,6 +22,7 @@ mod push;
|
||||
mod push_tags;
|
||||
mod rename_branch;
|
||||
mod reset;
|
||||
mod reset_popup;
|
||||
mod revision_files;
|
||||
mod revision_files_popup;
|
||||
mod stashmsg;
|
||||
@ -57,6 +58,7 @@ pub use push::PushComponent;
|
||||
pub use push_tags::PushTagsComponent;
|
||||
pub use rename_branch::RenameBranchComponent;
|
||||
pub use reset::ConfirmComponent;
|
||||
pub use reset_popup::ResetPopupComponent;
|
||||
pub use revision_files::RevisionFilesComponent;
|
||||
pub use revision_files_popup::{FileTreeOpen, RevisionFilesPopup};
|
||||
pub use stashmsg::StashMsgComponent;
|
||||
|
272
src/components/reset_popup.rs
Normal file
272
src/components/reset_popup.rs
Normal file
@ -0,0 +1,272 @@
|
||||
use super::{
|
||||
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent, EventState,
|
||||
};
|
||||
use crate::{
|
||||
keys::{key_match, SharedKeyConfig},
|
||||
queue::Queue,
|
||||
strings, try_or_popup,
|
||||
ui::{self, style::SharedTheme},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use asyncgit::{
|
||||
cached,
|
||||
sync::{CommitId, RepoPath, RepoPathRef, ResetType},
|
||||
};
|
||||
use crossterm::event::Event;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Alignment, Rect},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
const fn type_to_string(
|
||||
kind: ResetType,
|
||||
) -> (&'static str, &'static str) {
|
||||
const RESET_TYPE_DESC_SOFT: &str =
|
||||
" 🟢 Keep all changes. Stage differences";
|
||||
const RESET_TYPE_DESC_MIXED: &str =
|
||||
" 🟡 Keep all changes. Unstage differences";
|
||||
const RESET_TYPE_DESC_HARD: &str =
|
||||
" 🔴 Discard all local changes";
|
||||
|
||||
match kind {
|
||||
ResetType::Soft => ("Soft", RESET_TYPE_DESC_SOFT),
|
||||
ResetType::Mixed => ("Mixed", RESET_TYPE_DESC_MIXED),
|
||||
ResetType::Hard => ("Hard", RESET_TYPE_DESC_HARD),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ResetPopupComponent {
|
||||
queue: Queue,
|
||||
repo: RepoPath,
|
||||
commit: Option<CommitId>,
|
||||
kind: ResetType,
|
||||
git_branch_name: cached::BranchName,
|
||||
visible: bool,
|
||||
key_config: SharedKeyConfig,
|
||||
theme: SharedTheme,
|
||||
}
|
||||
|
||||
impl ResetPopupComponent {
|
||||
///
|
||||
pub fn new(
|
||||
queue: &Queue,
|
||||
repo: &RepoPathRef,
|
||||
theme: SharedTheme,
|
||||
key_config: SharedKeyConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
queue: queue.clone(),
|
||||
repo: repo.borrow().clone(),
|
||||
commit: None,
|
||||
kind: ResetType::Soft,
|
||||
git_branch_name: cached::BranchName::new(repo.clone()),
|
||||
visible: false,
|
||||
key_config,
|
||||
theme,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_text(&self, _width: u16) -> Vec<Spans> {
|
||||
let mut txt: Vec<Spans> = Vec::with_capacity(10);
|
||||
|
||||
txt.push(Spans::from(vec![
|
||||
Span::styled(
|
||||
String::from("Branch: "),
|
||||
self.theme.text(true, false),
|
||||
),
|
||||
Span::styled(
|
||||
self.git_branch_name.last().unwrap_or_default(),
|
||||
self.theme.branch(false, true),
|
||||
),
|
||||
]));
|
||||
|
||||
txt.push(Spans::from(vec![
|
||||
Span::styled(
|
||||
String::from("Reset to: "),
|
||||
self.theme.text(true, false),
|
||||
),
|
||||
Span::styled(
|
||||
self.commit
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_default(),
|
||||
self.theme.commit_hash(false),
|
||||
),
|
||||
]));
|
||||
|
||||
let (kind_name, kind_desc) = type_to_string(self.kind);
|
||||
|
||||
txt.push(Spans::from(vec![
|
||||
Span::styled(
|
||||
String::from("How: "),
|
||||
self.theme.text(true, false),
|
||||
),
|
||||
Span::styled(kind_name, self.theme.text(true, true)),
|
||||
Span::styled(kind_desc, self.theme.text(true, false)),
|
||||
]));
|
||||
|
||||
txt
|
||||
}
|
||||
|
||||
///
|
||||
pub fn open(&mut self, id: CommitId) -> Result<()> {
|
||||
self.show()?;
|
||||
|
||||
self.commit = Some(id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub fn update(&mut self) -> Result<()> {
|
||||
self.git_branch_name.lookup().map(Some).unwrap_or(None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
if let Some(id) = self.commit {
|
||||
try_or_popup!(
|
||||
self,
|
||||
"reset:",
|
||||
asyncgit::sync::reset_repo(&self.repo, id, self.kind)
|
||||
);
|
||||
}
|
||||
|
||||
self.hide();
|
||||
}
|
||||
|
||||
fn change_kind(&mut self, incr: bool) {
|
||||
self.kind = if incr {
|
||||
match self.kind {
|
||||
ResetType::Soft => ResetType::Mixed,
|
||||
ResetType::Mixed => ResetType::Hard,
|
||||
ResetType::Hard => ResetType::Soft,
|
||||
}
|
||||
} else {
|
||||
match self.kind {
|
||||
ResetType::Soft => ResetType::Hard,
|
||||
ResetType::Mixed => ResetType::Soft,
|
||||
ResetType::Hard => ResetType::Mixed,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for ResetPopupComponent {
|
||||
fn draw<B: Backend>(
|
||||
&self,
|
||||
f: &mut Frame<B>,
|
||||
area: Rect,
|
||||
) -> Result<()> {
|
||||
if self.is_visible() {
|
||||
const SIZE: (u16, u16) = (55, 5);
|
||||
let area =
|
||||
ui::centered_rect_absolute(SIZE.0, SIZE.1, area);
|
||||
|
||||
let width = area.width;
|
||||
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(
|
||||
Paragraph::new(self.get_text(width))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(Span::styled(
|
||||
"Reset",
|
||||
self.theme.title(true),
|
||||
))
|
||||
.border_style(self.theme.block(true)),
|
||||
)
|
||||
.alignment(Alignment::Left),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ResetPopupComponent {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
if self.is_visible() || force_all {
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::close_popup(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.order(1),
|
||||
);
|
||||
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::reset_commit(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.order(1),
|
||||
);
|
||||
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::reset_type(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.order(1),
|
||||
);
|
||||
}
|
||||
|
||||
visibility_blocking(self)
|
||||
}
|
||||
|
||||
fn event(
|
||||
&mut self,
|
||||
event: &crossterm::event::Event,
|
||||
) -> Result<EventState> {
|
||||
if self.is_visible() {
|
||||
if let Event::Key(key) = &event {
|
||||
if key_match(key, self.key_config.keys.exit_popup) {
|
||||
self.hide();
|
||||
} else if key_match(
|
||||
key,
|
||||
self.key_config.keys.move_down,
|
||||
) {
|
||||
self.change_kind(true);
|
||||
} else if key_match(key, self.key_config.keys.move_up)
|
||||
{
|
||||
self.change_kind(false);
|
||||
} else if key_match(key, self.key_config.keys.enter) {
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.visible = false;
|
||||
}
|
||||
|
||||
fn show(&mut self) -> Result<()> {
|
||||
self.visible = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -16,10 +16,12 @@ macro_rules! try_or_popup {
|
||||
($self:ident, $msg:expr, $e:expr) => {
|
||||
if let Err(err) = $e {
|
||||
::log::error!("{} {}", $msg, err);
|
||||
$self.queue.push(InternalEvent::ShowErrorMsg(format!(
|
||||
$self.queue.push(
|
||||
$crate::queue::InternalEvent::ShowErrorMsg(format!(
|
||||
"{}\n{}",
|
||||
$msg, err
|
||||
)));
|
||||
)),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -88,6 +88,7 @@ pub struct KeysList {
|
||||
pub log_tag_commit: GituiKeyEvent,
|
||||
pub log_mark_commit: GituiKeyEvent,
|
||||
pub log_checkout_commit: GituiKeyEvent,
|
||||
pub log_reset_comit: GituiKeyEvent,
|
||||
pub commit_amend: GituiKeyEvent,
|
||||
pub toggle_verify: GituiKeyEvent,
|
||||
pub copy: GituiKeyEvent,
|
||||
@ -173,6 +174,7 @@ impl Default for KeysList {
|
||||
log_tag_commit: GituiKeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()),
|
||||
log_mark_commit: GituiKeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty()),
|
||||
log_checkout_commit: GituiKeyEvent { code: KeyCode::Char('S'), modifiers: KeyModifiers::SHIFT },
|
||||
log_reset_comit: GituiKeyEvent { code: KeyCode::Char('R'), modifiers: KeyModifiers::SHIFT },
|
||||
commit_amend: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
|
||||
toggle_verify: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL),
|
||||
copy: GituiKeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()),
|
||||
|
@ -59,6 +59,7 @@ pub struct KeysListFile {
|
||||
pub log_tag_commit: Option<GituiKeyEvent>,
|
||||
pub log_mark_commit: Option<GituiKeyEvent>,
|
||||
pub log_checkout_commit: Option<GituiKeyEvent>,
|
||||
pub log_reset_commit: Option<GituiKeyEvent>,
|
||||
pub commit_amend: Option<GituiKeyEvent>,
|
||||
pub toggle_verify: Option<GituiKeyEvent>,
|
||||
pub copy: Option<GituiKeyEvent>,
|
||||
@ -153,6 +154,7 @@ impl KeysListFile {
|
||||
log_tag_commit: self.log_tag_commit.unwrap_or(default.log_tag_commit),
|
||||
log_mark_commit: self.log_mark_commit.unwrap_or(default.log_mark_commit),
|
||||
log_checkout_commit: self.log_checkout_commit.unwrap_or(default.log_checkout_commit),
|
||||
log_reset_comit: self.log_reset_commit.unwrap_or(default.log_reset_comit),
|
||||
commit_amend: self.commit_amend.unwrap_or(default.commit_amend),
|
||||
toggle_verify: self.toggle_verify.unwrap_or(default.toggle_verify),
|
||||
copy: self.copy.unwrap_or(default.copy),
|
||||
|
@ -126,6 +126,8 @@ pub enum InternalEvent {
|
||||
ViewSubmodules,
|
||||
///
|
||||
OpenRepo { path: PathBuf },
|
||||
///
|
||||
OpenResetPopup(CommitId),
|
||||
}
|
||||
|
||||
/// single threaded simple queue for components to communicate with each other
|
||||
|
@ -1233,6 +1233,39 @@ pub mod commands {
|
||||
CMD_GROUP_LOG,
|
||||
)
|
||||
}
|
||||
pub fn log_reset_commit(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Reset [{}]",
|
||||
key_config.get_hint(key_config.keys.log_reset_comit),
|
||||
),
|
||||
"reset to commit",
|
||||
CMD_GROUP_LOG,
|
||||
)
|
||||
}
|
||||
pub fn reset_commit(key_config: &SharedKeyConfig) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Confirm [{}]",
|
||||
key_config.get_hint(key_config.keys.enter),
|
||||
),
|
||||
"confirm reset",
|
||||
CMD_GROUP_LOG,
|
||||
)
|
||||
}
|
||||
pub fn reset_type(key_config: &SharedKeyConfig) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Change Type [{}{}]",
|
||||
key_config.get_hint(key_config.keys.move_up),
|
||||
key_config.get_hint(key_config.keys.move_down)
|
||||
),
|
||||
"change reset type",
|
||||
CMD_GROUP_LOG,
|
||||
)
|
||||
}
|
||||
pub fn tag_commit_confirm_msg(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
|
@ -327,6 +327,19 @@ impl Component for Revlog {
|
||||
} else if key_match(k, self.key_config.keys.tags) {
|
||||
self.queue.push(InternalEvent::Tags);
|
||||
return Ok(EventState::Consumed);
|
||||
} else if key_match(
|
||||
k,
|
||||
self.key_config.keys.log_reset_comit,
|
||||
) {
|
||||
return self.selected_commit().map_or(
|
||||
Ok(EventState::NotConsumed),
|
||||
|id| {
|
||||
self.queue.push(
|
||||
InternalEvent::OpenResetPopup(id),
|
||||
);
|
||||
Ok(EventState::Consumed)
|
||||
},
|
||||
);
|
||||
} else if key_match(
|
||||
k,
|
||||
self.key_config.keys.compare_commits,
|
||||
@ -449,6 +462,12 @@ impl Component for Revlog {
|
||||
self.visible || force_all,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::log_reset_commit(&self.key_config),
|
||||
self.selected_commit().is_some(),
|
||||
self.visible || force_all,
|
||||
));
|
||||
|
||||
visibility_blocking(self)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user