diff --git a/asyncgit/src/sync/merge.rs b/asyncgit/src/sync/merge.rs index f91b33a4d6..10d74d6415 100644 --- a/asyncgit/src/sync/merge.rs +++ b/asyncgit/src/sync/merge.rs @@ -1,13 +1,18 @@ use crate::{ error::{Error, Result}, sync::{ - branch::merge_commit::commit_merge_with_head, reset_stage, - reset_workdir, utils, CommitId, + branch::merge_commit::commit_merge_with_head, + rebase::{ + abort_rebase, continue_rebase, get_rebase_progress, + }, + reset_stage, reset_workdir, utils, CommitId, }, }; use git2::{BranchType, Commit, MergeOptions, Repository}; use scopetime::scope_time; +use super::rebase::{RebaseProgress, RebaseState}; + /// pub fn mergehead_ids(repo_path: &str) -> Result> { scope_time!("mergehead_ids"); @@ -51,6 +56,35 @@ pub fn merge_branch(repo_path: &str, branch: &str) -> Result<()> { Ok(()) } +/// +pub fn rebase_progress(repo_path: &str) -> Result { + scope_time!("rebase_progress"); + + let repo = utils::repo(repo_path)?; + + get_rebase_progress(&repo) +} + +/// +pub fn continue_pending_rebase( + repo_path: &str, +) -> Result { + scope_time!("continue_pending_rebase"); + + let repo = utils::repo(repo_path)?; + + continue_rebase(&repo) +} + +/// +pub fn abort_pending_rebase(repo_path: &str) -> Result<()> { + scope_time!("abort_pending_rebase"); + + let repo = utils::repo(repo_path)?; + + abort_rebase(&repo) +} + /// pub fn merge_branch_repo( repo: &Repository, diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 331e3c8a8a..2eb32bb34b 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -58,7 +58,9 @@ pub use hunks::{reset_hunk, stage_hunk, unstage_hunk}; pub use ignore::add_to_ignore; pub use logwalker::{LogWalker, LogWalkerFilter}; pub use merge::{ - abort_merge, merge_branch, merge_commit, merge_msg, mergehead_ids, + abort_merge, abort_pending_rebase, continue_pending_rebase, + merge_branch, merge_commit, merge_msg, mergehead_ids, + rebase_progress, }; pub use rebase::rebase_branch; pub use remotes::{ diff --git a/asyncgit/src/sync/rebase.rs b/asyncgit/src/sync/rebase.rs index be8a700205..b98bda0814 100644 --- a/asyncgit/src/sync/rebase.rs +++ b/asyncgit/src/sync/rebase.rs @@ -12,7 +12,7 @@ use super::CommitId; pub fn rebase_branch( repo_path: &str, branch: &str, -) -> Result { +) -> Result { scope_time!("rebase_branch"); let repo = utils::repo(repo_path)?; @@ -23,13 +23,13 @@ pub fn rebase_branch( fn rebase_branch_repo( repo: &Repository, branch_name: &str, -) -> Result { +) -> Result { let branch = repo.find_branch(branch_name, BranchType::Local)?; let annotated = repo.reference_to_annotated_commit(&branch.into_reference())?; - conflict_free_rebase(repo, &annotated) + rebase(repo, &annotated) } /// rebase attempt which aborts and undo's rebase if any conflict appears @@ -66,16 +66,133 @@ pub fn conflict_free_rebase( }) } +/// +#[derive(PartialEq, Debug)] +pub enum RebaseState { + /// + Finished, + /// + Conflicted, +} + +/// rebase +pub fn rebase( + repo: &git2::Repository, + commit: &git2::AnnotatedCommit, +) -> Result { + let mut rebase = repo.rebase(None, Some(commit), None, None)?; + let signature = + crate::sync::commit::signature_allow_undefined_name(repo)?; + + while let Some(op) = rebase.next() { + let _op = op?; + // dbg!(op.id()); + + if repo.index()?.has_conflicts() { + return Ok(RebaseState::Conflicted); + } + + rebase.commit(None, &signature, None)?; + } + + if repo.index()?.has_conflicts() { + return Ok(RebaseState::Conflicted); + } + + rebase.finish(Some(&signature))?; + + Ok(RebaseState::Finished) +} + +/// continue pending rebase +pub fn continue_rebase( + repo: &git2::Repository, +) -> Result { + let mut rebase = repo.open_rebase(None)?; + let signature = + crate::sync::commit::signature_allow_undefined_name(repo)?; + + if repo.index()?.has_conflicts() { + return Ok(RebaseState::Conflicted); + } + + // try commit current rebase step + if !repo.index()?.is_empty() { + rebase.commit(None, &signature, None)?; + } + + while let Some(op) = rebase.next() { + let _op = op?; + // dbg!(op.id()); + + if repo.index()?.has_conflicts() { + return Ok(RebaseState::Conflicted); + } + + rebase.commit(None, &signature, None)?; + } + + if repo.index()?.has_conflicts() { + return Ok(RebaseState::Conflicted); + } + + rebase.finish(Some(&signature))?; + + Ok(RebaseState::Finished) +} + +/// +#[derive(PartialEq, Debug)] +pub struct RebaseProgress { + /// + pub steps: usize, + /// + pub current: usize, + /// + pub current_commit: Option, +} + +/// +pub fn get_rebase_progress( + repo: &git2::Repository, +) -> Result { + let mut rebase = repo.open_rebase(None)?; + + let current_commit: Option = rebase + .operation_current() + .and_then(|idx| rebase.nth(idx)) + .map(|op| op.id().into()); + + let progress = RebaseProgress { + steps: rebase.len(), + current: rebase.operation_current().unwrap_or_default(), + current_commit, + }; + + Ok(progress) +} + +/// +pub fn abort_rebase(repo: &git2::Repository) -> Result<()> { + let mut rebase = repo.open_rebase(None)?; + + rebase.abort()?; + + Ok(()) +} + #[cfg(test)] -mod tests { +mod test_conflict_free_rebase { use crate::sync::{ checkout_branch, create_branch, - rebase::rebase_branch, + rebase::{rebase_branch, RebaseState}, repo_state, tests::{repo_init, write_commit_file}, - CommitId, RepoState, + utils, CommitId, RepoState, }; - use git2::Repository; + use git2::{BranchType, Repository}; + + use super::conflict_free_rebase; fn parent_ids(repo: &Repository, c: CommitId) -> Vec { let foo = repo @@ -88,6 +205,23 @@ mod tests { foo } + /// + fn test_rebase_branch_repo( + repo_path: &str, + branch_name: &str, + ) -> CommitId { + let repo = utils::repo(repo_path).unwrap(); + + let branch = + repo.find_branch(branch_name, BranchType::Local).unwrap(); + + let annotated = repo + .reference_to_annotated_commit(&branch.into_reference()) + .unwrap(); + + conflict_free_rebase(&repo, &annotated).unwrap() + } + #[test] fn test_smoke() { let (_td, repo) = repo_init().unwrap(); @@ -111,7 +245,7 @@ mod tests { checkout_branch(repo_path, "refs/heads/foo").unwrap(); - let r = rebase_branch(repo_path, "master").unwrap(); + let r = test_rebase_branch_repo(repo_path, "master"); assert_eq!(parent_ids(&repo, r), vec![c3]); } @@ -136,7 +270,64 @@ mod tests { let res = rebase_branch(repo_path, "master"); - assert!(res.is_err()); + assert!(matches!(res.unwrap(), RebaseState::Conflicted)); + + assert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase); + } +} + +#[cfg(test)] +mod test_rebase { + use crate::sync::{ + checkout_branch, create_branch, + rebase::{ + abort_rebase, get_rebase_progress, RebaseProgress, + RebaseState, + }, + rebase_branch, repo_state, + tests::{repo_init, write_commit_file}, + RepoState, + }; + + #[test] + fn test_conflicted_abort() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + write_commit_file(&repo, "test.txt", "test1", "commit1"); + + create_branch(repo_path, "foo").unwrap(); + + let c = + write_commit_file(&repo, "test.txt", "test2", "commit2"); + + checkout_branch(repo_path, "refs/heads/master").unwrap(); + + write_commit_file(&repo, "test.txt", "test3", "commit3"); + + checkout_branch(repo_path, "refs/heads/foo").unwrap(); + + assert!(get_rebase_progress(&repo).is_err()); + + // rebase + + let r = rebase_branch(repo_path, "master").unwrap(); + + assert_eq!(r, RebaseState::Conflicted); + assert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase); + assert_eq!( + get_rebase_progress(&repo).unwrap(), + RebaseProgress { + current: 0, + steps: 1, + current_commit: Some(c) + } + ); + + // abort + + abort_rebase(&repo).unwrap(); assert_eq!(repo_state(repo_path).unwrap(), RepoState::Clean); } diff --git a/asyncgit/src/sync/state.rs b/asyncgit/src/sync/state.rs index 410f6dfafd..6d78238c04 100644 --- a/asyncgit/src/sync/state.rs +++ b/asyncgit/src/sync/state.rs @@ -10,6 +10,8 @@ pub enum RepoState { /// Merge, /// + Rebase, + /// Other, } @@ -18,6 +20,7 @@ impl From for RepoState { match state { RepositoryState::Clean => Self::Clean, RepositoryState::Merge => Self::Merge, + RepositoryState::RebaseMerge => Self::Rebase, _ => Self::Other, } } @@ -29,5 +32,9 @@ pub fn repo_state(repo_path: &str) -> Result { let repo = utils::repo(repo_path)?; - Ok(repo.state().into()) + let state = repo.state(); + + // dbg!(&state); + + Ok(state.into()) } diff --git a/src/app.rs b/src/app.rs index c47ed3bc11..58f68876bd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -821,6 +821,10 @@ impl App { self.status_tab.abort_merge(); flags.insert(NeedsUpdate::ALL); } + Action::AbortRebase => { + self.status_tab.abort_rebase(); + flags.insert(NeedsUpdate::ALL); + } }; Ok(()) diff --git a/src/components/changes.rs b/src/components/changes.rs index 42ece875b0..86fa801362 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -192,43 +192,35 @@ impl Component for ChangesComponent { if self.is_working_dir { out.push(CommandInfo::new( strings::commands::stage_all(&self.key_config), - some_selection, - self.focused(), + true, + some_selection && self.focused(), )); out.push(CommandInfo::new( strings::commands::stage_item(&self.key_config), - some_selection, - self.focused(), + true, + some_selection && self.focused(), )); out.push(CommandInfo::new( strings::commands::reset_item(&self.key_config), - some_selection, - self.focused(), + true, + some_selection && self.focused(), )); out.push(CommandInfo::new( strings::commands::ignore_item(&self.key_config), - some_selection, - self.focused(), + true, + some_selection && self.focused(), )); } else { out.push(CommandInfo::new( strings::commands::unstage_item(&self.key_config), - some_selection, - self.focused(), + true, + some_selection && self.focused(), )); out.push(CommandInfo::new( strings::commands::unstage_all(&self.key_config), - some_selection, - self.focused(), + true, + some_selection && self.focused(), )); - out.push( - CommandInfo::new( - strings::commands::commit_open(&self.key_config), - !self.is_empty(), - self.focused() || force_all, - ) - .order(-1), - ); } CommandBlocking::PassingOn @@ -241,13 +233,7 @@ impl Component for ChangesComponent { if self.focused() { if let Event::Key(e) = ev { - return if e == self.key_config.open_commit - && !self.is_working_dir - && !self.is_empty() - { - self.queue.push(InternalEvent::OpenCommit); - Ok(EventState::Consumed) - } else if e == self.key_config.enter { + return if e == self.key_config.enter { try_or_popup!( self, "staging error:", diff --git a/src/components/reset.rs b/src/components/reset.rs index 0ff0a704f0..e389afb851 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -200,6 +200,10 @@ impl ConfirmComponent { Action::AbortMerge => ( strings::confirm_title_abortmerge(), strings::confirm_msg_abortmerge(), + ), + Action::AbortRebase => ( + strings::confirm_title_abortrebase(), + strings::confirm_msg_abortrebase(), ), }; } diff --git a/src/keys.rs b/src/keys.rs index dd6c2cffae..0b6a78a39d 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -158,7 +158,7 @@ impl Default for KeyConfig { force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT}, undo_commit: KeyEvent { code: KeyCode::Char('U'), modifiers: KeyModifiers::SHIFT}, pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, - abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT}, + abort_merge: KeyEvent { code: KeyCode::Char('A'), modifiers: KeyModifiers::SHIFT}, open_file_tree: KeyEvent { code: KeyCode::Char('F'), modifiers: KeyModifiers::SHIFT}, file_find: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, } diff --git a/src/queue.rs b/src/queue.rs index 94cc43ab46..8bd43b4dab 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -41,6 +41,7 @@ pub enum Action { ForcePush(String, bool), PullMerge { incoming: usize, rebase: bool }, AbortMerge, + AbortRebase, } /// diff --git a/src/strings.rs b/src/strings.rs index fcc45bc800..0ed98e03cd 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -153,6 +153,13 @@ pub fn confirm_msg_abortmerge() -> String { "This will revert all uncommitted changes. Are you sure?" .to_string() } +pub fn confirm_title_abortrebase() -> String { + "Abort rebase?".to_string() +} +pub fn confirm_msg_abortrebase() -> String { + "This will revert all uncommitted changes. Are you sure?" + .to_string() +} pub fn confirm_msg_reset() -> String { "confirm file reset?".to_string() } @@ -628,6 +635,31 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + + pub fn continue_rebase( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Continue rebase [{}]", + key_config.get_hint(key_config.rebase_branch), + ), + "continue ongoing rebase", + CMD_GROUP_GENERAL, + ) + } + + pub fn abort_rebase(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Abort rebase [{}]", + key_config.get_hint(key_config.abort_merge), + ), + "abort ongoing rebase", + CMD_GROUP_GENERAL, + ) + } + pub fn select_staging( key_config: &SharedKeyConfig, ) -> CommandText { diff --git a/src/tabs/status.rs b/src/tabs/status.rs index 517a68b1b1..51606bb06c 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -14,8 +14,8 @@ use crate::{ use anyhow::Result; use asyncgit::{ cached, - sync::BranchCompare, sync::{self, status::StatusType, RepoState}, + sync::{BranchCompare, CommitId}, AsyncDiff, AsyncGitNotification, AsyncStatus, DiffParams, DiffType, StatusParams, CWD, }; @@ -215,14 +215,12 @@ impl Status { } } - fn draw_repo_state( - f: &mut tui::Frame, - r: tui::layout::Rect, - ) -> Result<()> { - if let Ok(state) = sync::repo_state(CWD) { - if state != RepoState::Clean { + fn repo_state_text(state: &RepoState) -> String { + match state { + RepoState::Merge => { let ids = sync::mergehead_ids(CWD).unwrap_or_default(); + let ids = format!( "({})", ids.iter() @@ -231,7 +229,39 @@ impl Status { )) .join(",") ); - let txt = format!("{:?} {}", state, ids); + + format!("{:?} {}", state, ids) + } + RepoState::Rebase => { + let progress = + if let Ok(p) = sync::rebase_progress(CWD) { + format!( + "[{}] {}/{}", + p.current_commit + .as_ref() + .map(CommitId::get_short_string) + .unwrap_or_default(), + p.current + 1, + p.steps + ) + } else { + String::new() + }; + + format!("{:?} ({})", state, progress) + } + _ => format!("{:?}", state), + } + } + + fn draw_repo_state( + f: &mut tui::Frame, + r: tui::layout::Rect, + ) -> Result<()> { + if let Ok(state) = sync::repo_state(CWD) { + if state != RepoState::Clean { + let txt = Self::repo_state_text(&state); + let txt_len = u16::try_from(txt.len())?; let w = Paragraph::new(txt) .style(Style::default().fg(Color::Red)) @@ -519,10 +549,31 @@ impl Status { == RepoState::Merge } + fn pending_rebase() -> bool { + sync::repo_state(CWD).unwrap_or(RepoState::Clean) + == RepoState::Rebase + } + pub fn abort_merge(&self) { try_or_popup!(self, "abort merge", sync::abort_merge(CWD)); } + pub fn abort_rebase(&self) { + try_or_popup!( + self, + "abort rebase", + sync::abort_pending_rebase(CWD) + ); + } + + fn continue_rebase(&self) { + try_or_popup!( + self, + "continue rebase", + sync::continue_pending_rebase(CWD) + ); + } + fn commands_nav( &self, out: &mut Vec, @@ -566,6 +617,12 @@ impl Status { .order(strings::order::NAV), ); } + + fn can_commit(&self) -> bool { + self.index.focused() + && !self.index.is_empty() + && !Self::pending_rebase() + } } impl Component for Status { @@ -583,6 +640,15 @@ impl Component for Status { self.components().as_slice(), ); + out.push( + CommandInfo::new( + strings::commands::commit_open(&self.key_config), + true, + self.can_commit() || force_all, + ) + .order(-1), + ); + out.push(CommandInfo::new( strings::commands::open_branch_select_popup( &self.key_config, @@ -612,7 +678,8 @@ impl Component for Status { out.push(CommandInfo::new( strings::commands::undo_commit(&self.key_config), true, - !focus_on_diff, + (!Self::pending_rebase() && !focus_on_diff) + || force_all, )); out.push(CommandInfo::new( @@ -620,6 +687,17 @@ impl Component for Status { true, Self::can_abort_merge() || force_all, )); + + out.push(CommandInfo::new( + strings::commands::continue_rebase(&self.key_config), + true, + Self::pending_rebase() || force_all, + )); + out.push(CommandInfo::new( + strings::commands::abort_rebase(&self.key_config), + true, + Self::pending_rebase() || force_all, + )); } { @@ -639,6 +717,7 @@ impl Component for Status { visibility_blocking(self) } + #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn event( &mut self, ev: crossterm::event::Event, @@ -664,6 +743,11 @@ impl Component for Status { ); } Ok(EventState::Consumed) + } else if k == self.key_config.open_commit + && self.can_commit() + { + self.queue.push(InternalEvent::OpenCommit); + Ok(EventState::Consumed) } else if k == self.key_config.toggle_workarea && !self.is_focus_on_diff() { @@ -725,6 +809,22 @@ impl Component for Status { Action::AbortMerge, )); + Ok(EventState::Consumed) + } else if k == self.key_config.abort_merge + && Self::pending_rebase() + { + self.queue.push(InternalEvent::ConfirmAction( + Action::AbortRebase, + )); + + Ok(EventState::Consumed) + } else if k == self.key_config.rebase_branch + && Self::pending_rebase() + { + self.continue_rebase(); + self.queue.push(InternalEvent::Update( + NeedsUpdate::ALL, + )); Ok(EventState::Consumed) } else { Ok(EventState::NotConsumed)