allow rebase with conflicts (#897)

This commit is contained in:
Stephan Dilly 2021-09-29 18:55:47 +02:00 committed by GitHub
parent 9f8fc6b907
commit e4c7867564
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 411 additions and 50 deletions

View File

@ -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<Vec<CommitId>> {
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<RebaseProgress> {
scope_time!("rebase_progress");
let repo = utils::repo(repo_path)?;
get_rebase_progress(&repo)
}
///
pub fn continue_pending_rebase(
repo_path: &str,
) -> Result<RebaseState> {
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,

View File

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

View File

@ -12,7 +12,7 @@ use super::CommitId;
pub fn rebase_branch(
repo_path: &str,
branch: &str,
) -> Result<CommitId> {
) -> Result<RebaseState> {
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<CommitId> {
) -> Result<RebaseState> {
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<RebaseState> {
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<RebaseState> {
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<CommitId>,
}
///
pub fn get_rebase_progress(
repo: &git2::Repository,
) -> Result<RebaseProgress> {
let mut rebase = repo.open_rebase(None)?;
let current_commit: Option<CommitId> = 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<CommitId> {
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);
}

View File

@ -10,6 +10,8 @@ pub enum RepoState {
///
Merge,
///
Rebase,
///
Other,
}
@ -18,6 +20,7 @@ impl From<RepositoryState> 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<RepoState> {
let repo = utils::repo(repo_path)?;
Ok(repo.state().into())
let state = repo.state();
// dbg!(&state);
Ok(state.into())
}

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,7 @@ pub enum Action {
ForcePush(String, bool),
PullMerge { incoming: usize, rebase: bool },
AbortMerge,
AbortRebase,
}
///

View File

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

View File

@ -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<B: tui::backend::Backend>(
f: &mut tui::Frame<B>,
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<B: tui::backend::Backend>(
f: &mut tui::Frame<B>,
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<CommandInfo>,
@ -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)