Skip to content

fix: Reverse commit range before yanking #2441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 `<oldest>^..<newest>` for consecutive commit ranges. [[@Esgariot](https://github.com/Esgariot)] ([#2441](https://github.com/extrawurst/gitui/pull/2441))
Copy link
Contributor

@naseschwarz naseschwarz Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not entirely correct - right now (or in case #2577 gets merged), ordering is reversed for all lists of commits, even if they're lists of individual hashes.


### 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))
7 changes: 5 additions & 2 deletions asyncgit/src/sync/mod.rs
Original file line number Diff line number Diff line change
@@ -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;

58 changes: 58 additions & 0 deletions asyncgit/src/sync/revwalk.rs
Original file line number Diff line number Diff line change
@@ -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<R>(
repo_path: &RepoPath,
start: Bound<&CommitId>,
end: Bound<&CommitId>,
sort: git2::Sort,
iter_fn: impl FnOnce(
&mut (dyn Iterator<Item = Result<Oid>>),
) -> Result<R>,
) -> Result<R> {
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<Option<Commit<'r>>> {
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),
}
}
35 changes: 25 additions & 10 deletions src/components/commitlist.rs
Original file line number Diff line number Diff line change
@@ -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()