Skip to content

Commit 100beba

Browse files
committed
feat: implement consecutive algorithm.
This is the default negotiation algorithm.
1 parent 1762c74 commit 100beba

File tree

6 files changed

+237
-29
lines changed

6 files changed

+237
-29
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-negotiate/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ 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+
bitflags = "2"
2022

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

gix-negotiate/src/consecutive.rs

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

gix-negotiate/src/lib.rs

+21-7
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

+11-7
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

+18-13
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,25 +35,26 @@ 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}: one have per baseline: {have} missing or in wrong order")
44+
})?;
45+
assert_eq!(
46+
actual,
47+
have,
48+
"{algo_name}: 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,
56+
assert!(
57+
negotiator.next_have().is_none(),
5358
"{algo_name}: negotiator should be depleted after all recorded baseline rounds"
5459
);
5560
}

0 commit comments

Comments
 (0)