support reset from log view (#1534)

This commit is contained in:
extrawurst 2023-02-04 16:15:26 +01:00 committed by GitHub
parent 1a0167e7f8
commit 8ab62244ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 379 additions and 8 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

View File

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

View File

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

View File

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

View 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(())
}
}

View File

@ -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!(
"{}\n{}",
$msg, err
)));
$self.queue.push(
$crate::queue::InternalEvent::ShowErrorMsg(format!(
"{}\n{}",
$msg, err
)),
);
}
};
}

View File

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

View File

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

View File

@ -126,6 +126,8 @@ pub enum InternalEvent {
ViewSubmodules,
///
OpenRepo { path: PathBuf },
///
OpenResetPopup(CommitId),
}
/// single threaded simple queue for components to communicate with each other

View File

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

View File

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