mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
Support choosing checkout branch method when status is not empty (#2494)
This commit is contained in:
parent
0cce6907a2
commit
3082396bf1
9 changed files with 383 additions and 33 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
* increase MSRV from 1.81 to 1.82 [[@cruessler](https://github.com/cruessler)]
|
||||
|
||||
### Added
|
||||
* support choosing checkout branch method when status is not empty [[@fatpandac](https://github.com/fatpandac)] ([#2404](https://github.com/extrawurst/gitui/issues/2404))
|
||||
* Support pre-push hook [[@xlai89](https://github.com/xlai89)] ([#1933](https://github.com/extrawurst/gitui/issues/1933))
|
||||
* Message tab supports pageUp and pageDown [[@xlai89](https://github.com/xlai89)] ([#2623](https://github.com/extrawurst/gitui/issues/2623))
|
||||
* Files and status tab support pageUp and pageDown [[@fatpandac](https://github.com/fatpandac)] ([#1951](https://github.com/extrawurst/gitui/issues/1951))
|
||||
|
|
|
|||
|
|
@ -104,6 +104,11 @@ impl BranchInfo {
|
|||
|
||||
None
|
||||
}
|
||||
|
||||
/// returns whether branch is local
|
||||
pub const fn is_local(&self) -> bool {
|
||||
matches!(self.details, BranchDetails::Local(_))
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
|
|
|
|||
|
|
@ -281,20 +281,59 @@ pub fn get_status(
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
/// discard all changes in the working directory
|
||||
pub fn discard_status(repo_path: &RepoPath) -> Result<bool> {
|
||||
let repo = repo(repo_path)?;
|
||||
let commit = repo.head()?.peel_to_commit()?;
|
||||
|
||||
repo.reset(commit.as_object(), git2::ResetType::Hard, None)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
sync::{
|
||||
commit, stage_add_file,
|
||||
status::{get_status, StatusType},
|
||||
tests::repo_init_bare,
|
||||
tests::{repo_init, repo_init_bare},
|
||||
RepoPath,
|
||||
},
|
||||
StatusItem, StatusItemType,
|
||||
};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_discard_status() {
|
||||
let file_path = Path::new("README.md");
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
let mut file = File::create(root.join(file_path)).unwrap();
|
||||
|
||||
// initial commit
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
commit(repo_path, "commit msg").unwrap();
|
||||
|
||||
writeln!(file, "Test for discard_status").unwrap();
|
||||
|
||||
let statuses =
|
||||
get_status(repo_path, StatusType::WorkingDir, None)
|
||||
.unwrap();
|
||||
assert_eq!(statuses.len(), 1);
|
||||
|
||||
discard_status(repo_path).unwrap();
|
||||
|
||||
let statuses =
|
||||
get_status(repo_path, StatusType::WorkingDir, None)
|
||||
.unwrap();
|
||||
assert_eq!(statuses.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_status_with_workdir() {
|
||||
|
|
|
|||
27
src/app.rs
27
src/app.rs
|
|
@ -11,16 +11,16 @@ use crate::{
|
|||
options::{Options, SharedOptions},
|
||||
popup_stack::PopupStack,
|
||||
popups::{
|
||||
AppOption, BlameFilePopup, BranchListPopup, CommitPopup,
|
||||
CompareCommitsPopup, ConfirmPopup, CreateBranchPopup,
|
||||
CreateRemotePopup, ExternalEditorPopup, FetchPopup,
|
||||
FileRevlogPopup, FuzzyFindPopup, GotoLinePopup, HelpPopup,
|
||||
InspectCommitPopup, LogSearchPopupPopup, MsgPopup,
|
||||
OptionsPopup, PullPopup, PushPopup, PushTagsPopup,
|
||||
RemoteListPopup, RenameBranchPopup, RenameRemotePopup,
|
||||
ResetPopup, RevisionFilesPopup, StashMsgPopup,
|
||||
SubmodulesListPopup, TagCommitPopup, TagListPopup,
|
||||
UpdateRemoteUrlPopup,
|
||||
AppOption, BlameFilePopup, BranchListPopup,
|
||||
CheckoutOptionPopup, CommitPopup, CompareCommitsPopup,
|
||||
ConfirmPopup, CreateBranchPopup, CreateRemotePopup,
|
||||
ExternalEditorPopup, FetchPopup, FileRevlogPopup,
|
||||
FuzzyFindPopup, GotoLinePopup, HelpPopup, InspectCommitPopup,
|
||||
LogSearchPopupPopup, MsgPopup, OptionsPopup, PullPopup,
|
||||
PushPopup, PushTagsPopup, RemoteListPopup, RenameBranchPopup,
|
||||
RenameRemotePopup, ResetPopup, RevisionFilesPopup,
|
||||
StashMsgPopup, SubmodulesListPopup, TagCommitPopup,
|
||||
TagListPopup, UpdateRemoteUrlPopup,
|
||||
},
|
||||
queue::{
|
||||
Action, AppTabs, InternalEvent, NeedsUpdate, Queue,
|
||||
|
|
@ -99,6 +99,7 @@ pub struct App {
|
|||
submodule_popup: SubmodulesListPopup,
|
||||
tags_popup: TagListPopup,
|
||||
reset_popup: ResetPopup,
|
||||
checkout_option_popup: CheckoutOptionPopup,
|
||||
cmdbar: RefCell<CommandBar>,
|
||||
tab: usize,
|
||||
revlog: Revlog,
|
||||
|
|
@ -234,6 +235,7 @@ impl App {
|
|||
stashing_tab: Stashing::new(&env),
|
||||
stashlist_tab: StashList::new(&env),
|
||||
files_tab: FilesTab::new(&env, select_file),
|
||||
checkout_option_popup: CheckoutOptionPopup::new(&env),
|
||||
goto_line_popup: GotoLinePopup::new(&env),
|
||||
tab: 0,
|
||||
queue: env.queue,
|
||||
|
|
@ -511,6 +513,7 @@ impl App {
|
|||
fetch_popup,
|
||||
tag_commit_popup,
|
||||
reset_popup,
|
||||
checkout_option_popup,
|
||||
create_branch_popup,
|
||||
create_remote_popup,
|
||||
rename_remote_popup,
|
||||
|
|
@ -551,6 +554,7 @@ impl App {
|
|||
submodule_popup,
|
||||
tags_popup,
|
||||
reset_popup,
|
||||
checkout_option_popup,
|
||||
create_branch_popup,
|
||||
rename_branch_popup,
|
||||
revision_files_popup,
|
||||
|
|
@ -932,6 +936,9 @@ impl App {
|
|||
self.blame_file_popup.goto_line(line);
|
||||
}
|
||||
}
|
||||
InternalEvent::CheckoutOption(branch) => {
|
||||
self.checkout_option_popup.open(branch)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(flags)
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ use asyncgit::{
|
|||
checkout_remote_branch, BranchDetails, LocalBranch,
|
||||
RemoteBranch,
|
||||
},
|
||||
checkout_branch, get_branches_info, BranchInfo, BranchType,
|
||||
CommitId, RepoPathRef, RepoState,
|
||||
checkout_branch, get_branches_info,
|
||||
status::StatusType,
|
||||
BranchInfo, BranchType, CommitId, RepoPathRef, RepoState,
|
||||
},
|
||||
AsyncGitNotification,
|
||||
};
|
||||
|
|
@ -582,22 +583,35 @@ impl BranchListPopup {
|
|||
anyhow::bail!("no valid branch selected");
|
||||
}
|
||||
|
||||
if self.local {
|
||||
checkout_branch(
|
||||
&self.repo.borrow(),
|
||||
&self.branches[self.selection as usize].name,
|
||||
)?;
|
||||
self.hide();
|
||||
} else {
|
||||
checkout_remote_branch(
|
||||
&self.repo.borrow(),
|
||||
&self.branches[self.selection as usize],
|
||||
)?;
|
||||
self.local = true;
|
||||
self.update_branches()?;
|
||||
}
|
||||
let status = sync::status::get_status(
|
||||
&self.repo.borrow(),
|
||||
StatusType::WorkingDir,
|
||||
None,
|
||||
)
|
||||
.expect("Could not get status");
|
||||
|
||||
self.queue.push(InternalEvent::Update(NeedsUpdate::ALL));
|
||||
let selected_branch = &self.branches[self.selection as usize];
|
||||
if status.is_empty() {
|
||||
if self.local {
|
||||
checkout_branch(
|
||||
&self.repo.borrow(),
|
||||
&selected_branch.name,
|
||||
)?;
|
||||
self.hide();
|
||||
} else {
|
||||
checkout_remote_branch(
|
||||
&self.repo.borrow(),
|
||||
selected_branch,
|
||||
)?;
|
||||
self.local = true;
|
||||
self.update_branches()?;
|
||||
}
|
||||
self.queue.push(InternalEvent::Update(NeedsUpdate::ALL));
|
||||
} else {
|
||||
self.queue.push(InternalEvent::CheckoutOption(
|
||||
selected_branch.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
239
src/popups/checkout_option.rs
Normal file
239
src/popups/checkout_option.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
use crate::components::{
|
||||
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent, EventState,
|
||||
};
|
||||
use crate::queue::{InternalEvent, NeedsUpdate};
|
||||
use crate::strings::CheckoutOptions;
|
||||
use crate::try_or_popup;
|
||||
use crate::{
|
||||
app::Environment,
|
||||
keys::{key_match, SharedKeyConfig},
|
||||
queue::Queue,
|
||||
strings,
|
||||
ui::{self, style::SharedTheme},
|
||||
};
|
||||
use anyhow::{Ok, Result};
|
||||
use asyncgit::sync::branch::checkout_remote_branch;
|
||||
use asyncgit::sync::status::discard_status;
|
||||
use asyncgit::sync::{checkout_branch, BranchInfo, RepoPath};
|
||||
use crossterm::event::Event;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub struct CheckoutOptionPopup {
|
||||
queue: Queue,
|
||||
repo: RepoPath,
|
||||
branch: Option<BranchInfo>,
|
||||
option: CheckoutOptions,
|
||||
visible: bool,
|
||||
key_config: SharedKeyConfig,
|
||||
theme: SharedTheme,
|
||||
}
|
||||
|
||||
impl CheckoutOptionPopup {
|
||||
///
|
||||
pub fn new(env: &Environment) -> Self {
|
||||
Self {
|
||||
queue: env.queue.clone(),
|
||||
repo: env.repo.borrow().clone(),
|
||||
branch: None,
|
||||
option: CheckoutOptions::KeepLocalChanges,
|
||||
visible: false,
|
||||
key_config: env.key_config.clone(),
|
||||
theme: env.theme.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_text(&self, _width: u16) -> Vec<Line<'_>> {
|
||||
let mut txt: Vec<Line> = Vec::with_capacity(10);
|
||||
|
||||
txt.push(Line::from(vec![
|
||||
Span::styled(
|
||||
String::from("Switch to: "),
|
||||
self.theme.text(true, false),
|
||||
),
|
||||
Span::styled(
|
||||
self.branch.as_ref().expect("No branch").name.clone(),
|
||||
self.theme.commit_hash(false),
|
||||
),
|
||||
]));
|
||||
|
||||
let (kind_name, kind_desc) = self.option.to_string_pair();
|
||||
|
||||
txt.push(Line::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, branch: BranchInfo) -> Result<()> {
|
||||
self.show()?;
|
||||
|
||||
self.branch = Some(branch);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn checkout(&self) -> Result<()> {
|
||||
if let Some(branch) = &self.branch {
|
||||
if branch.is_local() {
|
||||
checkout_branch(&self.repo, &branch.name)?;
|
||||
} else {
|
||||
checkout_remote_branch(&self.repo, branch)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(&mut self) -> Result<()> {
|
||||
match self.option {
|
||||
CheckoutOptions::KeepLocalChanges => {
|
||||
self.checkout()?;
|
||||
}
|
||||
CheckoutOptions::DiscardAllLocalChagnes => {
|
||||
discard_status(&self.repo)?;
|
||||
self.checkout()?;
|
||||
}
|
||||
}
|
||||
|
||||
self.queue.push(InternalEvent::Update(NeedsUpdate::ALL));
|
||||
self.queue.push(InternalEvent::SelectBranch);
|
||||
self.hide();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn change_kind(&mut self, incr: bool) {
|
||||
self.option = if incr {
|
||||
self.option.next()
|
||||
} else {
|
||||
self.option.previous()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for CheckoutOptionPopup {
|
||||
fn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_visible() {
|
||||
const SIZE: (u16, u16) = (55, 4);
|
||||
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(
|
||||
"Checkout options",
|
||||
self.theme.title(true),
|
||||
))
|
||||
.border_style(self.theme.block(true)),
|
||||
)
|
||||
.alignment(Alignment::Left),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CheckoutOptionPopup {
|
||||
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) {
|
||||
try_or_popup!(
|
||||
self,
|
||||
"checkout error:",
|
||||
self.handle_event()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
mod blame_file;
|
||||
mod branchlist;
|
||||
mod checkout_option;
|
||||
mod commit;
|
||||
mod compare_commits;
|
||||
mod confirm;
|
||||
|
|
@ -31,6 +32,7 @@ mod update_remote_url;
|
|||
|
||||
pub use blame_file::{BlameFileOpen, BlameFilePopup};
|
||||
pub use branchlist::BranchListPopup;
|
||||
pub use checkout_option::CheckoutOptionPopup;
|
||||
pub use commit::CommitPopup;
|
||||
pub use compare_commits::CompareCommitsPopup;
|
||||
pub use confirm::ConfirmPopup;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ use crate::{
|
|||
};
|
||||
use asyncgit::{
|
||||
sync::{
|
||||
diff::DiffLinePosition, CommitId, LogFilterSearchOptions,
|
||||
diff::DiffLinePosition, BranchInfo, CommitId,
|
||||
LogFilterSearchOptions,
|
||||
},
|
||||
PushType,
|
||||
};
|
||||
|
|
@ -161,6 +162,8 @@ pub enum InternalEvent {
|
|||
OpenGotoLinePopup(usize),
|
||||
///
|
||||
GotoLine(usize),
|
||||
///
|
||||
CheckoutOption(BranchInfo),
|
||||
}
|
||||
|
||||
/// single threaded simple queue for components to communicate with each other
|
||||
|
|
|
|||
|
|
@ -438,6 +438,46 @@ pub fn ellipsis_trim_start(s: &str, width: usize) -> Cow<'_, str> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum CheckoutOptions {
|
||||
KeepLocalChanges,
|
||||
DiscardAllLocalChagnes,
|
||||
}
|
||||
|
||||
impl CheckoutOptions {
|
||||
pub const fn previous(self) -> Self {
|
||||
match self {
|
||||
Self::KeepLocalChanges => Self::DiscardAllLocalChagnes,
|
||||
Self::DiscardAllLocalChagnes => Self::KeepLocalChanges,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn next(self) -> Self {
|
||||
match self {
|
||||
Self::KeepLocalChanges => Self::DiscardAllLocalChagnes,
|
||||
Self::DiscardAllLocalChagnes => Self::KeepLocalChanges,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn to_string_pair(
|
||||
self,
|
||||
) -> (&'static str, &'static str) {
|
||||
const CHECKOUT_OPTION_UNCHANGE: &str =
|
||||
" 🟡 Keep local changes";
|
||||
const CHECKOUT_OPTION_DISCARD: &str =
|
||||
" 🔴 Discard all local changes";
|
||||
|
||||
match self {
|
||||
Self::KeepLocalChanges => {
|
||||
("Don't change", CHECKOUT_OPTION_UNCHANGE)
|
||||
}
|
||||
Self::DiscardAllLocalChagnes => {
|
||||
("Discard", CHECKOUT_OPTION_DISCARD)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod commit {
|
||||
use crate::keys::SharedKeyConfig;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue