Skip to content

Commit 4523f2a

Browse files
committed
feat: inline revwalk into is_continuous
`revwalk` turned out too complicated, include simplified version into `is_continuous`. Add tests.
1 parent 011fdec commit 4523f2a

File tree

2 files changed

+124
-74
lines changed

2 files changed

+124
-74
lines changed

asyncgit/src/sync/revwalk.rs

Lines changed: 123 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,141 @@
11
//! git revwalk utils
2-
use std::ops::{Bound, ControlFlow};
3-
4-
use crate::Result;
5-
use git2::{Commit, Oid};
6-
72
use super::{repo, CommitId, RepoPath};
3+
use crate::Result;
4+
use git2::Oid;
5+
use std::ops::ControlFlow;
86

9-
/// Performs a Git revision walk.
7+
/// Checks if `commits` range is topologically continuous
108
///
11-
/// The revwalk is optionally bounded by `start` and `end` commits, sorted according to `sort`.
12-
/// The revwalk iterator bound by repository's lifetime is exposed through the `iter_fn`.
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-
{
35-
#![allow(clippy::let_and_return)]
36-
let ret = iter_fn(&mut revwalk.map(|r| {
37-
r.map_err(|x| crate::Error::Generic(x.to_string()))
38-
}));
39-
ret
40-
}
41-
}
42-
43-
fn resolve<'r>(
44-
repo: &'r git2::Repository,
45-
commit: Bound<&CommitId>,
46-
) -> Result<Option<Commit<'r>>> {
47-
match commit {
48-
Bound::Included(c) => {
49-
let commit = repo.find_commit(c.get_oid())?;
50-
Ok(Some(commit))
51-
}
52-
Bound::Excluded(s) => {
53-
let commit = repo.find_commit(s.get_oid())?;
54-
let res = (commit.parent_count() == 1)
55-
.then(|| commit.parent(0))
56-
.transpose()?;
57-
Ok(res)
58-
}
59-
Bound::Unbounded => Ok(None),
60-
}
61-
}
62-
63-
/// Checks if `commits` range is continuous under `sort` flags.
9+
/// Supports only linear paths - presence of merge commits results in `false`.
6410
pub fn is_continuous(
6511
repo_path: &RepoPath,
66-
sort: git2::Sort,
6712
commits: &[CommitId],
6813
) -> Result<bool> {
6914
match commits {
7015
[] | [_] => Ok(true),
71-
[start, .., end] => revwalk(
72-
repo_path,
73-
Bound::Excluded(start),
74-
Bound::Included(end),
75-
sort,
76-
|revwalk| match revwalk.zip(commits).try_fold(
16+
commits => {
17+
let repo = repo(repo_path)?;
18+
let mut revwalk = repo.revwalk()?;
19+
revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?;
20+
revwalk.simplify_first_parent()?;
21+
revwalk.push(commits[0].get_oid())?;
22+
let revwalked: Vec<Oid> =
23+
revwalk
24+
.take(commits.len())
25+
.collect::<std::result::Result<Vec<_>, _>>()?;
26+
27+
if revwalked.len() != commits.len() {
28+
return Ok(false);
29+
}
30+
31+
match revwalked.iter().zip(commits).try_fold(
7732
Ok(true),
78-
|acc, (r, c)| match r
79-
.map(CommitId::new)
80-
.and_then(|r| acc.map(|acc| acc && (&r == c)))
33+
|acc, (r, c)| match acc
34+
.map(|acc| acc && (&(CommitId::from(*r)) == c))
8135
{
82-
Ok(true) => ControlFlow::Continue(Ok(true)),
36+
ok @ Ok(true) => ControlFlow::Continue(ok),
8337
otherwise => ControlFlow::Break(otherwise),
8438
},
8539
) {
8640
ControlFlow::Continue(v) | ControlFlow::Break(v) => v,
87-
},
88-
),
41+
}
42+
}
43+
}
44+
}
45+
#[cfg(test)]
46+
mod tests_is_continuous {
47+
use crate::sync::{
48+
checkout_commit, commit, merge_commit,
49+
revwalk::is_continuous, tests::repo_init_empty, RepoPath,
50+
};
51+
52+
#[test]
53+
fn test_linear_commits_are_continuous() {
54+
// * c3 (HEAD)
55+
// * c2
56+
// * c1
57+
// * c0 (root)
58+
59+
let (_td, repo) = repo_init_empty().unwrap();
60+
let root = repo.path().parent().unwrap();
61+
let repo_path: &RepoPath =
62+
&root.as_os_str().to_str().unwrap().into();
63+
let _c0 = commit(repo_path, "commit 0").unwrap();
64+
let c1 = commit(repo_path, "commit 1").unwrap();
65+
let c2 = commit(repo_path, "commit 2").unwrap();
66+
let c3 = commit(repo_path, "commit 3").unwrap();
67+
68+
let result = is_continuous(repo_path, &[c3, c2, c1]).unwrap();
69+
assert!(result, "linear commits should be continuous");
70+
71+
let result = is_continuous(repo_path, &[c2]).unwrap();
72+
assert!(result, "single commit should be continuous");
73+
74+
let result = is_continuous(repo_path, &[]).unwrap();
75+
assert!(result, "empty range should be continuous");
76+
}
77+
78+
#[test]
79+
fn test_merge_commits_break_continuity() {
80+
// * c4 (HEAD)
81+
// |\
82+
// | * c3
83+
// * | c2
84+
// |/
85+
// * c1
86+
// * c0 (root)
87+
88+
let (_td, repo) = repo_init_empty().unwrap();
89+
let root = repo.path().parent().unwrap();
90+
let repo_path: &RepoPath =
91+
&root.as_os_str().to_str().unwrap().into();
92+
93+
let _c0 = commit(repo_path, "commit 0").unwrap();
94+
let c1 = commit(repo_path, "commit 1").unwrap();
95+
let c2 = commit(repo_path, "commit 2").unwrap();
96+
97+
checkout_commit(repo_path, c1).unwrap();
98+
let c3 = commit(repo_path, "commit 3").unwrap();
99+
100+
let c4 =
101+
merge_commit(repo_path, "commit 4", &[c2, c3]).unwrap();
102+
103+
let result = is_continuous(repo_path, &[c4, c2, c1]).unwrap();
104+
assert!(
105+
!result,
106+
"range including merge should not be continuous"
107+
);
108+
109+
let result = is_continuous(repo_path, &[c2, c1]).unwrap();
110+
assert!(
111+
result,
112+
"linear range before merge should be continuous"
113+
);
114+
}
115+
116+
#[test]
117+
fn test_non_continuous_commits() {
118+
// * c4 (HEAD)
119+
// * c3
120+
// * c2
121+
// * c1
122+
// * c0 (root)
123+
124+
let (_td, repo) = repo_init_empty().unwrap();
125+
let root = repo.path().parent().unwrap();
126+
let repo_path: &RepoPath =
127+
&root.as_os_str().to_str().unwrap().into();
128+
129+
let _c0 = commit(repo_path, "commit 0").unwrap();
130+
let c1 = commit(repo_path, "commit 1").unwrap();
131+
let c2 = commit(repo_path, "commit 2").unwrap();
132+
let c3 = commit(repo_path, "commit 3").unwrap();
133+
let c4 = commit(repo_path, "commit 4").unwrap();
134+
135+
let result = is_continuous(repo_path, &[c4, c3, c1]).unwrap();
136+
assert!(!result, "commit range with gap should return false");
137+
138+
let result = is_continuous(repo_path, &[c1, c2, c3]).unwrap();
139+
assert!(!result, "wrong order should return false");
89140
}
90141
}

src/components/commitlist.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
use anyhow::Result;
1616
use asyncgit::sync::{
1717
self, checkout_commit, revwalk, BranchDetails, BranchInfo,
18-
CommitId, RepoPathRef, Sort, Tags,
18+
CommitId, RepoPathRef, Tags,
1919
};
2020
use chrono::{DateTime, Local};
2121
use crossterm::event::Event;
@@ -144,7 +144,6 @@ impl CommitList {
144144
[latest, .., earliest]
145145
if revwalk::is_continuous(
146146
&self.repo.borrow(),
147-
Sort::TOPOLOGICAL,
148147
&self.marked.iter().map(|x| x.1).collect_vec(),
149148
)? =>
150149
{

0 commit comments

Comments
 (0)