Skip to content

Commit fa769ad

Browse files
committed
fix: Use revwalk to determine consecutive range
1 parent 1a5569b commit fa769ad

File tree

3 files changed

+106
-26
lines changed

3 files changed

+106
-26
lines changed

asyncgit/src/sync/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod rebase;
2424
pub mod remotes;
2525
mod repository;
2626
mod reset;
27+
mod revwalk;
2728
mod reword;
2829
pub mod sign;
2930
mod staging;
@@ -65,6 +66,7 @@ pub use config::{
6566
};
6667
pub use diff::get_diff_commit;
6768
pub use git2::BranchType;
69+
pub use git2::Sort;
6870
pub use hooks::{
6971
hooks_commit_msg, hooks_post_commit, hooks_pre_commit,
7072
hooks_prepare_commit_msg, HookResult, PrepareCommitMsgSource,
@@ -87,6 +89,7 @@ pub use remotes::{
8789
pub(crate) use repository::{gix_repo, repo};
8890
pub use repository::{RepoPath, RepoPathRef};
8991
pub use reset::{reset_repo, reset_stage, reset_workdir};
92+
pub use revwalk::revwalk;
9093
pub use reword::reword;
9194
pub use staging::{discard_lines, stage_lines};
9295
pub use stash::{

asyncgit/src/sync/revwalk.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::ops::Bound;
2+
3+
use crate::Result;
4+
use git2::{Commit, Oid};
5+
6+
use super::{repo, CommitId, RepoPath};
7+
8+
/// Performs a Git revision walk on `repo_path`, optionally bounded by `start` and `end` commits,
9+
/// sorted according to `sort`. The revwalk iterator bound by repository's lifetime is exposed through
10+
/// the `iter_fn`.
11+
///
12+
///
13+
pub fn revwalk<R>(
14+
repo_path: &RepoPath,
15+
start: Bound<&CommitId>,
16+
end: Bound<&CommitId>,
17+
sort: git2::Sort,
18+
iter_fn: impl FnOnce(
19+
&mut (dyn Iterator<Item = Result<Oid>>),
20+
) -> Result<R>,
21+
) -> Result<R> {
22+
let repo = repo(repo_path)?;
23+
let mut revwalk = repo.revwalk()?;
24+
revwalk.set_sorting(sort)?;
25+
let start = resolve(&repo, start)?;
26+
let end = resolve(&repo, end)?;
27+
28+
if let Some(s) = start {
29+
revwalk.hide(s.id())?;
30+
}
31+
if let Some(e) = end {
32+
revwalk.push(e.id())?;
33+
}
34+
let ret = iter_fn(&mut revwalk.map(|r| {
35+
r.map_err(|x| crate::Error::Generic(x.to_string()))
36+
}));
37+
ret
38+
}
39+
40+
fn resolve<'r>(
41+
repo: &'r git2::Repository,
42+
commit: Bound<&CommitId>,
43+
) -> Result<Option<Commit<'r>>> {
44+
match commit {
45+
Bound::Included(c) => {
46+
let commit = repo.find_commit(c.get_oid())?;
47+
Ok(Some(commit))
48+
}
49+
Bound::Excluded(s) => {
50+
let commit = repo.find_commit(s.get_oid())?;
51+
let res = (commit.parent_count() == 1)
52+
.then(|| commit.parent(0))
53+
.transpose()?;
54+
Ok(res)
55+
}
56+
Bound::Unbounded => Ok(None),
57+
}
58+
}

src/components/commitlist.rs

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use crate::{
1414
};
1515
use anyhow::Result;
1616
use asyncgit::sync::{
17-
self, checkout_commit, BranchDetails, BranchInfo, CommitId,
18-
RepoPathRef, Tags,
17+
self, checkout_commit, revwalk, BranchDetails, BranchInfo,
18+
CommitId, RepoPathRef, Sort, Tags,
1919
};
2020
use chrono::{DateTime, Local};
2121
use crossterm::event::Event;
@@ -29,8 +29,8 @@ use ratatui::{
2929
Frame,
3030
};
3131
use std::{
32-
borrow::Cow, cell::Cell, cmp, collections::BTreeMap, rc::Rc,
33-
time::Instant,
32+
borrow::Cow, cell::Cell, cmp, collections::BTreeMap, ops::Bound,
33+
rc::Rc, time::Instant,
3434
};
3535

3636
const ELEMENTS_PER_LINE: usize = 9;
@@ -131,37 +131,52 @@ impl CommitList {
131131
}
132132

133133
/// Build string of marked or selected (if none are marked) commit ids
134-
fn concat_selected_commit_ids(&self) -> Option<String> {
134+
fn concat_selected_commit_ids(&self) -> Result<Option<String>> {
135135
match self.marked.as_slice() {
136-
[] => self
136+
[] => Ok(self
137137
.items
138138
.iter()
139139
.nth(
140140
self.selection
141141
.saturating_sub(self.items.index_offset()),
142142
)
143-
.map(|e| e.id.to_string()),
144-
[latest, .., earliest]
145-
if self
146-
.marked()
147-
.windows(2)
148-
.all(|w| w[0].0 + 1 == w[1].0) =>
149-
{
150-
Some(format!("{}^..{}", earliest.1, latest.1))
143+
.map(|e| e.id.to_string())),
144+
[(_idx, commit)] => Ok(Some(commit.to_string())),
145+
[latest, .., earliest] => {
146+
let marked_rev = self.marked.iter().rev();
147+
let marked_topo_consecutive = revwalk(
148+
&self.repo.borrow(),
149+
Bound::Excluded(&earliest.1),
150+
Bound::Included(&latest.1),
151+
Sort::TOPOLOGICAL | Sort::REVERSE,
152+
|revwalk| {
153+
revwalk.zip(marked_rev).try_fold(
154+
true,
155+
|acc, (r, m)| {
156+
let revwalked = CommitId::new(r?);
157+
let marked = m.1;
158+
Ok(acc && (revwalked == marked))
159+
},
160+
)
161+
},
162+
)?;
163+
let yank = if marked_topo_consecutive {
164+
format!("{}^..{}", earliest.1, latest.1)
165+
} else {
166+
self.marked
167+
.iter()
168+
.map(|(_idx, commit)| commit.to_string())
169+
.join(" ")
170+
};
171+
Ok(Some(yank))
151172
}
152-
marked => Some(
153-
marked
154-
.iter()
155-
.map(|(_idx, commit)| commit.to_string())
156-
.join(" "),
157-
),
158173
}
159174
}
160175

161176
/// Copy currently marked or selected (if none are marked) commit ids
162177
/// to clipboard
163178
pub fn copy_commit_hash(&self) -> Result<()> {
164-
if let Some(yank) = self.concat_selected_commit_ids() {
179+
if let Some(yank) = self.concat_selected_commit_ids()? {
165180
crate::clipboard::copy_string(&yank)?;
166181
self.queue.push(InternalEvent::ShowInfoMsg(
167182
strings::copy_success(&yank),
@@ -1008,7 +1023,9 @@ mod tests {
10081023
#[test]
10091024
fn test_copy_commit_list_empty() {
10101025
assert_eq!(
1011-
CommitList::default().concat_selected_commit_ids(),
1026+
CommitList::default()
1027+
.concat_selected_commit_ids()
1028+
.unwrap(),
10121029
None
10131030
);
10141031
}
@@ -1023,7 +1040,7 @@ mod tests {
10231040
// offset by two, so we expect commit id 2 for
10241041
// selection = 4
10251042
assert_eq!(
1026-
cl.concat_selected_commit_ids(),
1043+
cl.concat_selected_commit_ids().unwrap(),
10271044
Some(String::from(
10281045
"0000000000000000000000000000000000000002"
10291046
))
@@ -1038,35 +1055,37 @@ mod tests {
10381055
..cl
10391056
};
10401057
assert_eq!(
1041-
cl.concat_selected_commit_ids(),
1058+
cl.concat_selected_commit_ids().unwrap(),
10421059
Some(String::from(
10431060
"0000000000000000000000000000000000000001",
10441061
))
10451062
);
10461063
}
10471064

10481065
#[test]
1066+
#[ignore = "needs real repository to run test. Will be moved to revwalk module."]
10491067
fn test_copy_commit_range_marked() {
10501068
let cl = build_commit_list_with_some_commits();
10511069
let cl = CommitList {
10521070
marked: build_marked_from_indices(&cl, &[4, 5, 6, 7]),
10531071
..cl
10541072
};
10551073
assert_eq!(
1056-
cl.concat_selected_commit_ids(),
1074+
cl.concat_selected_commit_ids().unwrap(),
10571075
Some(String::from("0000000000000000000000000000000000000005^..0000000000000000000000000000000000000002"))
10581076
);
10591077
}
10601078

10611079
#[test]
1080+
#[ignore = "needs real repository to run test. Will be moved to revwalk module."]
10621081
fn test_copy_commit_random_marked() {
10631082
let cl = build_commit_list_with_some_commits();
10641083
let cl = CommitList {
10651084
marked: build_marked_from_indices(&cl, &[4, 7]),
10661085
..cl
10671086
};
10681087
assert_eq!(
1069-
cl.concat_selected_commit_ids(),
1088+
cl.concat_selected_commit_ids().unwrap(),
10701089
Some(String::from(concat!(
10711090
"0000000000000000000000000000000000000002 ",
10721091
"0000000000000000000000000000000000000005"

0 commit comments

Comments
 (0)