Skip to content

Commit 01c1213

Browse files
committed
feat: implement consecutive algorithm.
This is the default negotiation algorithm.
1 parent 62d1314 commit 01c1213

File tree

6 files changed

+250
-30
lines changed

6 files changed

+250
-30
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-negotiate/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ gix-hash = { version = "^0.11.1", path = "../gix-hash" }
1717
gix-object = { version = "^0.29.2", path = "../gix-object" }
1818
gix-commitgraph = { version = "^0.15.0", path = "../gix-commitgraph" }
1919
gix-revision = { version = "^0.14.0", path = "../gix-revision" }
20+
thiserror = "1.0.40"
21+
smallvec = "1.10.0"
22+
bitflags = "2"
2023

2124
[dev-dependencies]
2225
gix-testtools = { path = "../tests/tools" }

gix-negotiate/src/consecutive.rs

Lines changed: 193 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,193 @@
1-
// TODO: make this the actual bitflags
2-
pub(crate) type Flags = u32;
1+
use crate::{Error, Negotiator};
2+
use gix_hash::ObjectId;
3+
use gix_revision::graph::CommitterTimestamp;
4+
use smallvec::SmallVec;
5+
bitflags::bitflags! {
6+
/// Whether something can be read or written.
7+
#[derive(Debug, Default, Copy, Clone)]
8+
pub struct Flags: u8 {
9+
/// The revision is known to be in common with the remote
10+
const COMMON = 1 << 0;
11+
/// The revision is common and was set by merit of a remote tracking ref (e.g. `refs/heads/origin/main`).
12+
const COMMON_REF = 1 << 1;
13+
/// The revision was processed by us and used to avoid processing it again.
14+
const SEEN = 1 << 2;
15+
/// The revision was popped off our primary priority queue, used to avoid double-counting of `non_common_revs`
16+
const POPPED = 1 << 3;
17+
}
18+
}
19+
20+
pub(crate) struct Algorithm<'find> {
21+
graph: gix_revision::Graph<'find, Flags>,
22+
revs: gix_revision::PriorityQueue<CommitterTimestamp, ObjectId>,
23+
non_common_revs: usize,
24+
}
25+
26+
impl<'a> Algorithm<'a> {
27+
pub fn new(graph: gix_revision::Graph<'a, Flags>) -> Self {
28+
Self {
29+
graph,
30+
revs: gix_revision::PriorityQueue::new(),
31+
non_common_revs: 0,
32+
}
33+
}
34+
35+
/// Add `id` to our priority queue and *add* `flags` to it.
36+
fn add_to_queue(&mut self, id: ObjectId, mark: Flags) -> Result<(), Error> {
37+
let mut is_common = false;
38+
let mut had_mark = false;
39+
let commit = self.graph.try_lookup_and_insert(id, |current| {
40+
had_mark = current.contains(mark);
41+
*current |= mark;
42+
is_common = current.contains(Flags::COMMON);
43+
})?;
44+
if let Some(timestamp) = commit
45+
.filter(|_| !had_mark)
46+
.map(|c| c.committer_timestamp())
47+
.transpose()?
48+
{
49+
self.revs.insert(timestamp, id);
50+
if !is_common {
51+
self.non_common_revs += 1;
52+
}
53+
}
54+
Ok(())
55+
}
56+
57+
fn mark_common(&mut self, id: ObjectId, mode: Mark, ancestors: Ancestors) -> Result<(), Error> {
58+
let mut is_common = false;
59+
if let Some(commit) = self
60+
.graph
61+
.try_lookup_and_insert(id, |current| is_common = current.contains(Flags::COMMON))?
62+
.filter(|_| !is_common)
63+
{
64+
let mut queue =
65+
gix_revision::PriorityQueue::from_iter(Some((commit.committer_timestamp()?, (id, 0_usize))));
66+
if let Mark::ThisCommitAndAncestors = mode {
67+
let current = self.graph.get_mut(&id).expect("just inserted");
68+
*current |= Flags::COMMON;
69+
if current.contains(Flags::SEEN) && !current.contains(Flags::POPPED) {
70+
self.non_common_revs -= 1;
71+
}
72+
}
73+
let mut parents = SmallVec::new();
74+
while let Some((id, generation)) = queue.pop() {
75+
if self.graph.get(&id).map_or(true, |d| !d.contains(Flags::SEEN)) {
76+
self.add_to_queue(id, Flags::SEEN)?;
77+
} else if matches!(ancestors, Ancestors::AllUnseen) || generation == 0 {
78+
if let Some(commit) = self.graph.try_lookup_and_insert(id, |_| {})? {
79+
collect_parents(commit.iter_parents(), &mut parents)?;
80+
for parent_id in parents.drain(..) {
81+
let mut prev_flags = Flags::default();
82+
if let Some(parent) = self
83+
.graph
84+
.try_lookup_and_insert(parent_id, |d| {
85+
prev_flags = *d;
86+
*d |= Flags::COMMON;
87+
})?
88+
.filter(|_| !prev_flags.contains(Flags::COMMON))
89+
{
90+
if prev_flags.contains(Flags::SEEN) && !prev_flags.contains(Flags::POPPED) {
91+
self.non_common_revs -= 1;
92+
}
93+
queue.insert(parent.committer_timestamp()?, (parent_id, generation + 1))
94+
}
95+
}
96+
}
97+
}
98+
}
99+
}
100+
Ok(())
101+
}
102+
}
103+
104+
fn collect_parents(
105+
parents: gix_revision::graph::commit::Parents<'_>,
106+
out: &mut SmallVec<[ObjectId; 2]>,
107+
) -> Result<(), Error> {
108+
out.clear();
109+
for parent in parents {
110+
out.push(parent.map_err(|err| match err {
111+
gix_revision::graph::commit::iter_parents::Error::DecodeCommit(err) => Error::DecodeCommit(err),
112+
gix_revision::graph::commit::iter_parents::Error::DecodeCommitGraph(err) => Error::DecodeCommitInGraph(err),
113+
})?);
114+
}
115+
Ok(())
116+
}
117+
118+
impl<'a> Negotiator for Algorithm<'a> {
119+
fn known_common(&mut self, id: ObjectId) -> Result<(), Error> {
120+
if self.graph.get(&id).map_or(true, |d| !d.contains(Flags::SEEN)) {
121+
self.add_to_queue(id, Flags::COMMON_REF | Flags::SEEN)?;
122+
self.mark_common(id, Mark::AncestorsOnly, Ancestors::DirectUnseen)?;
123+
}
124+
Ok(())
125+
}
126+
127+
fn add_tip(&mut self, id: ObjectId) -> Result<(), Error> {
128+
self.add_to_queue(id, Flags::SEEN)
129+
}
130+
131+
fn next_have(&mut self) -> Option<Result<ObjectId, Error>> {
132+
let mut parents = SmallVec::new();
133+
loop {
134+
let id = self.revs.pop().filter(|_| self.non_common_revs != 0)?;
135+
let flags = self.graph.get_mut(&id).expect("it was added to the graph by now");
136+
*flags |= Flags::POPPED;
137+
138+
if !flags.contains(Flags::COMMON) {
139+
self.non_common_revs -= 1;
140+
}
141+
142+
let (res, mark) = if flags.contains(Flags::COMMON) {
143+
(None, Flags::COMMON | Flags::SEEN)
144+
} else if flags.contains(Flags::COMMON_REF) {
145+
(Some(id), Flags::COMMON | Flags::SEEN)
146+
} else {
147+
(Some(id), Flags::SEEN)
148+
};
149+
150+
let commit = match self.graph.try_lookup(&id) {
151+
Ok(c) => c.expect("it was found before, must still be there"),
152+
Err(err) => return Some(Err(err.into())),
153+
};
154+
if let Err(err) = collect_parents(commit.iter_parents(), &mut parents) {
155+
return Some(Err(err));
156+
}
157+
for parent_id in parents.drain(..) {
158+
if self.graph.get(&parent_id).map_or(true, |d| !d.contains(Flags::SEEN)) {
159+
if let Err(err) = self.add_to_queue(parent_id, mark) {
160+
return Some(Err(err));
161+
}
162+
}
163+
if mark.contains(Flags::COMMON) {
164+
if let Err(err) = self.mark_common(parent_id, Mark::AncestorsOnly, Ancestors::AllUnseen) {
165+
return Some(Err(err));
166+
}
167+
}
168+
}
169+
170+
if let Some(id) = res {
171+
return Some(Ok(id));
172+
}
173+
}
174+
}
175+
176+
fn in_common_with_remote(&mut self, id: ObjectId) -> Result<bool, Error> {
177+
let known_to_be_common = self.graph.get(&id).map_or(false, |d| d.contains(Flags::COMMON));
178+
self.mark_common(id, Mark::ThisCommitAndAncestors, Ancestors::DirectUnseen)?;
179+
Ok(known_to_be_common)
180+
}
181+
}
182+
183+
enum Mark {
184+
AncestorsOnly,
185+
ThisCommitAndAncestors,
186+
}
187+
188+
enum Ancestors {
189+
/// Traverse only the parents of a commit.
190+
DirectUnseen,
191+
/// Traverse all ancestors that weren't yet seen.
192+
AllUnseen,
193+
}

gix-negotiate/src/lib.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ impl Algorithm {
4747
self,
4848
find: Find,
4949
cache: impl Into<Option<gix_commitgraph::Graph>>,
50-
) -> Box<dyn Negotiator>
50+
) -> Box<dyn Negotiator + 'find>
5151
where
5252
Find:
5353
for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<Option<gix_object::CommitRefIter<'a>>, E> + 'find,
@@ -56,8 +56,8 @@ impl Algorithm {
5656
match &self {
5757
Algorithm::Noop => Box::new(noop::Noop) as Box<dyn Negotiator>,
5858
Algorithm::Consecutive => {
59-
let _graph = gix_revision::Graph::<'_, consecutive::Flags>::new(find, cache);
60-
todo!()
59+
let graph = gix_revision::Graph::<'_, consecutive::Flags>::new(find, cache);
60+
Box::new(consecutive::Algorithm::new(graph))
6161
}
6262
Algorithm::Skipping => todo!(),
6363
}
@@ -69,18 +69,32 @@ pub trait Negotiator {
6969
/// Mark `id` as common between the remote and us.
7070
///
7171
/// These ids are typically the local tips of remote tracking branches.
72-
fn known_common(&mut self, id: &gix_hash::oid);
72+
fn known_common(&mut self, id: gix_hash::ObjectId) -> Result<(), Error>;
7373

7474
/// Add `id` as starting point of a traversal across commits that aren't necessarily common between the remote and us.
7575
///
7676
/// These tips are usually the commits of local references whose tips should lead to objects that we have in common with the remote.
77-
fn add_tip(&mut self, id: &gix_hash::oid);
77+
fn add_tip(&mut self, id: gix_hash::ObjectId) -> Result<(), Error>;
7878

7979
/// Produce the next id of an object that we want the server to know we have. It's an object we don't know we have in common or not.
8080
///
8181
/// Returns `None` if we have exhausted all options, which might mean we have traversed the entire commit graph.
82-
fn next_have(&mut self) -> Option<gix_hash::ObjectId>;
82+
fn next_have(&mut self) -> Option<Result<gix_hash::ObjectId, Error>>;
8383

8484
/// Mark `id` as being common with the remote (as informed by the remote itself) and return `true` if we knew it was common already.
85-
fn in_common_with_remote(&mut self, id: &gix_hash::oid) -> bool;
85+
///
86+
/// We can assume to have already seen `id` as we were the one to inform the remote in a prior `have`.
87+
fn in_common_with_remote(&mut self, id: gix_hash::ObjectId) -> Result<bool, Error>;
88+
}
89+
90+
/// An error that happened during any of the methods on a [`Negotiator`].
91+
#[derive(Debug, thiserror::Error)]
92+
#[allow(missing_docs)]
93+
pub enum Error {
94+
#[error(transparent)]
95+
DecodeCommit(#[from] gix_object::decode::Error),
96+
#[error(transparent)]
97+
DecodeCommitInGraph(#[from] gix_commitgraph::file::commit::Error),
98+
#[error(transparent)]
99+
LookupCommitInGraph(#[from] gix_revision::graph::lookup::Error),
86100
}

gix-negotiate/src/noop.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
use crate::Negotiator;
2-
use gix_hash::{oid, ObjectId};
1+
use crate::{Error, Negotiator};
2+
use gix_hash::ObjectId;
33

44
pub(crate) struct Noop;
55

66
impl Negotiator for Noop {
7-
fn known_common(&mut self, _id: &oid) {}
7+
fn known_common(&mut self, _id: ObjectId) -> Result<(), Error> {
8+
Ok(())
9+
}
810

9-
fn add_tip(&mut self, _id: &oid) {}
11+
fn add_tip(&mut self, _id: ObjectId) -> Result<(), Error> {
12+
Ok(())
13+
}
1014

11-
fn next_have(&mut self) -> Option<ObjectId> {
15+
fn next_have(&mut self) -> Option<Result<ObjectId, Error>> {
1216
None
1317
}
1418

15-
fn in_common_with_remote(&mut self, _id: &oid) -> bool {
16-
false
19+
fn in_common_with_remote(&mut self, _id: ObjectId) -> Result<bool, Error> {
20+
Ok(false)
1721
}
1822
}

gix-negotiate/tests/baseline/mod.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ use gix_odb::Find;
66
#[test]
77
fn run() -> crate::Result {
88
let root = gix_testtools::scripted_fixture_read_only("make_repos.sh")?;
9-
for case in ["no_parents"] {
9+
for case in [
10+
"no_parents",
11+
"clock_skew",
12+
"two_colliding_skips", /* "multi_round" */
13+
] {
1014
let base = root.join(case);
1115

1216
for (algo_name, algo) in [
1317
("noop", Algorithm::Noop),
14-
// ("consecutive", Algorithm::Consecutive),
18+
("consecutive", Algorithm::Consecutive),
1519
// ("skipping", Algorithm::Skipping),
1620
] {
1721
let buf = std::fs::read(base.join(format!("baseline.{algo_name}")))?;
@@ -31,26 +35,27 @@ fn run() -> crate::Result {
3135
cache,
3236
);
3337
for tip in &tips {
34-
negotiator.add_tip(tip);
38+
negotiator.add_tip(*tip)?;
3539
}
3640
for Round { haves, common } in ParseRounds::new(buf.lines()) {
3741
for have in haves {
3842
let actual = negotiator.next_have().unwrap_or_else(|| {
39-
panic!(
40-
"{algo_name}: one have per baseline: {have} missing or in wrong order, left: {:?}",
41-
std::iter::from_fn(|| negotiator.next_have()).collect::<Vec<_>>()
42-
)
43-
});
44-
assert_eq!(actual, have, "{algo_name}: order and commit matches exactly");
43+
panic!("{algo_name}:{use_cache}: one have per baseline: {have} missing or in wrong order")
44+
})?;
45+
assert_eq!(
46+
actual,
47+
have,
48+
"{algo_name}:{use_cache}: order and commit matches exactly, left: {:?}",
49+
std::iter::from_fn(|| negotiator.next_have()).collect::<Vec<_>>()
50+
);
4551
}
4652
for common_revision in common {
47-
negotiator.in_common_with_remote(&common_revision);
53+
negotiator.in_common_with_remote(common_revision)?;
4854
}
4955
}
50-
assert_eq!(
51-
negotiator.next_have(),
52-
None,
53-
"{algo_name}: negotiator should be depleted after all recorded baseline rounds"
56+
assert!(
57+
negotiator.next_have().is_none(),
58+
"{algo_name}:{use_cache}: negotiator should be depleted after all recorded baseline rounds"
5459
);
5560
}
5661
}

0 commit comments

Comments
 (0)