diff --git a/CHANGELOG.md b/CHANGELOG.md index 012b60e5ff..395d9f2f6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Breaking Changes +* reverse commit range before yanking marked commits, producing `^..` for consecutive commit ranges. [[@Esgariot](https://github.com/Esgariot)] ([#2441](https://github.com/extrawurst/gitui/pull/2441)) + ### Added * support loading custom syntax highlighting themes from a file [[@acuteenvy](https://github.com/acuteenvy)] ([#2565](https://github.com/gitui-org/gitui/pull/2565)) * Select syntax highlighting theme out of the defaults from syntect [[@vasilismanol](https://github.com/vasilismanol)] ([#1931](https://github.com/extrawurst/gitui/issues/1931)) diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index c52a556aad..e914e170f9 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -24,6 +24,7 @@ mod rebase; pub mod remotes; mod repository; mod reset; +mod revwalk; mod reword; pub mod sign; mod staging; @@ -65,6 +66,8 @@ pub use config::{ }; pub use diff::get_diff_commit; pub use git2::BranchType; +pub use git2::ResetType; +pub use git2::Sort; pub use hooks::{ hooks_commit_msg, hooks_post_commit, hooks_pre_commit, hooks_prepare_commit_msg, HookResult, PrepareCommitMsgSource, @@ -87,6 +90,7 @@ pub use remotes::{ pub(crate) use repository::repo; pub use repository::{RepoPath, RepoPathRef}; pub use reset::{reset_repo, reset_stage, reset_workdir}; +pub use revwalk::revwalk; pub use reword::reword; pub use staging::{discard_lines, stage_lines}; pub use stash::{ @@ -108,8 +112,6 @@ pub use utils::{ stage_add_all, stage_add_file, stage_addremoved, Head, }; -pub use git2::ResetType; - /// test utils #[cfg(test)] pub mod tests { @@ -123,6 +125,7 @@ pub mod tests { }; use crate::error::Result; use git2::Repository; + use std::{path::Path, process::Command}; use tempfile::TempDir; diff --git a/asyncgit/src/sync/revwalk.rs b/asyncgit/src/sync/revwalk.rs new file mode 100644 index 0000000000..cfa0c4da68 --- /dev/null +++ b/asyncgit/src/sync/revwalk.rs @@ -0,0 +1,58 @@ +use std::ops::Bound; + +use crate::Result; +use git2::{Commit, Oid}; + +use super::{repo, CommitId, RepoPath}; + +/// Performs a Git revision walk on `repo_path`, optionally bounded by `start` and `end` commits, +/// sorted according to `sort`. The revwalk iterator bound by repository's lifetime is exposed through +/// the `iter_fn`. +/// +/// +pub fn revwalk( + repo_path: &RepoPath, + start: Bound<&CommitId>, + end: Bound<&CommitId>, + sort: git2::Sort, + iter_fn: impl FnOnce( + &mut (dyn Iterator>), + ) -> Result, +) -> Result { + let repo = repo(repo_path)?; + let mut revwalk = repo.revwalk()?; + revwalk.set_sorting(sort)?; + let start = resolve(&repo, start)?; + let end = resolve(&repo, end)?; + + if let Some(s) = start { + revwalk.hide(s.id())?; + } + if let Some(e) = end { + revwalk.push(e.id())?; + } + let ret = iter_fn(&mut revwalk.map(|r| { + r.map_err(|x| crate::Error::Generic(x.to_string())) + })); + ret +} + +fn resolve<'r>( + repo: &'r git2::Repository, + commit: Bound<&CommitId>, +) -> Result>> { + match commit { + Bound::Included(c) => { + let commit = repo.find_commit(c.get_oid())?; + Ok(Some(commit)) + } + Bound::Excluded(s) => { + let commit = repo.find_commit(s.get_oid())?; + let res = (commit.parent_count() == 1) + .then(|| commit.parent(0)) + .transpose()?; + Ok(res) + } + Bound::Unbounded => Ok(None), + } +} diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 52e4e7be9d..2e23e92de7 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -14,8 +14,8 @@ use crate::{ }; use anyhow::Result; use asyncgit::sync::{ - self, checkout_commit, BranchDetails, BranchInfo, CommitId, - RepoPathRef, Tags, + self, checkout_commit, revwalk, BranchDetails, BranchInfo, + CommitId, RepoPathRef, Sort, Tags, }; use chrono::{DateTime, Local}; use crossterm::event::Event; @@ -29,8 +29,8 @@ use ratatui::{ Frame, }; use std::{ - borrow::Cow, cell::Cell, cmp, collections::BTreeMap, rc::Rc, - time::Instant, + borrow::Cow, cell::Cell, cmp, collections::BTreeMap, ops::Bound, + rc::Rc, time::Instant, }; const ELEMENTS_PER_LINE: usize = 9; @@ -145,12 +145,27 @@ impl CommitList { ) .map(|e| e.id.to_string()), [(_idx, commit)] => Some(commit.to_string()), - [first, .., last] => { - let marked_consecutive = - marked.windows(2).all(|w| w[0].0 + 1 == w[1].0); - - let yank = if marked_consecutive { - format!("{}^..{}", first.1, last.1) + [latest, .., earliest] => { + let marked_rev = marked.iter().rev(); + let marked_topo_consecutive = revwalk( + &self.repo.borrow(), + Bound::Excluded(&earliest.1), + Bound::Included(&latest.1), + Sort::TOPOLOGICAL | Sort::REVERSE, + |revwalk| { + revwalk.zip(marked_rev).try_fold( + true, + |acc, (r, m)| { + let revwalked = CommitId::new(r?); + let marked = m.1; + log::trace!("comparing revwalk with marked: {} <-> {}",revwalked,marked); + Ok(acc && (revwalked == marked)) + }, + ) + }, + )?; + let yank = if marked_topo_consecutive { + format!("{}^..{}", earliest.1, latest.1) } else { marked .iter()