Skip to content

Commit 1bd93be

Browse files
committed
feat: allow graph sharing by unifying Flags type.
This makes the graph used in `gix-negotiate` shareable by callers, which can do their own traversal and store their own flags. The knowlege of this traversal can be kept using such shared flags, like the `PARSED` bit which should be set whenever parents are traversed. That way we are able to emulate the algorithms git uses perfectly, as we keep exactly the same state.
1 parent 11ad8a8 commit 1bd93be

File tree

8 files changed

+238
-242
lines changed

8 files changed

+238
-242
lines changed

gix-negotiate/src/consecutive.rs

Lines changed: 66 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,82 @@
1-
use crate::{Error, Negotiator};
1+
use crate::{Error, Flags, Negotiator};
22
use gix_hash::ObjectId;
33
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 has entered the priority queue.
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-
}
194

20-
pub(crate) struct Algorithm<'find> {
21-
graph: gix_revision::Graph<'find, Flags>,
5+
pub(crate) struct Algorithm {
226
revs: gix_revision::PriorityQueue<CommitterTimestamp, ObjectId>,
237
non_common_revs: usize,
248
}
259

26-
impl<'a> Algorithm<'a> {
27-
pub fn new(graph: gix_revision::Graph<'a, Flags>) -> Self {
10+
impl Default for Algorithm {
11+
fn default() -> Self {
2812
Self {
29-
graph,
3013
revs: gix_revision::PriorityQueue::new(),
3114
non_common_revs: 0,
3215
}
3316
}
17+
}
3418

19+
impl Algorithm {
3520
/// Add `id` to our priority queue and *add* `flags` to it.
36-
fn add_to_queue(&mut self, id: ObjectId, mark: Flags) -> Result<(), Error> {
21+
fn add_to_queue(&mut self, id: ObjectId, mark: Flags, graph: &mut crate::Graph<'_>) -> Result<(), Error> {
3722
let mut is_common = false;
38-
if self.graph.get(&id).map_or(false, |flags| flags.intersects(mark)) {
39-
return Ok(());
40-
}
41-
let commit = self.graph.try_lookup_and_insert(id, |current| {
42-
*current |= mark;
43-
is_common = current.contains(Flags::COMMON);
44-
})?;
45-
if let Some(timestamp) = commit.map(|c| c.committer_timestamp()).transpose()? {
46-
self.revs.insert(timestamp, id);
23+
let mut has_mark = false;
24+
if let Some(commit) = graph
25+
.try_lookup_or_insert_commit(id, |data| {
26+
has_mark = data.flags.intersects(mark);
27+
data.flags |= mark;
28+
is_common = data.flags.contains(Flags::COMMON);
29+
})?
30+
.filter(|_| !has_mark)
31+
{
32+
self.revs.insert(commit.commit_time, id);
4733
if !is_common {
4834
self.non_common_revs += 1;
4935
}
5036
}
5137
Ok(())
5238
}
5339

54-
fn mark_common(&mut self, id: ObjectId, mode: Mark, ancestors: Ancestors) -> Result<(), Error> {
40+
fn mark_common(
41+
&mut self,
42+
id: ObjectId,
43+
mode: Mark,
44+
ancestors: Ancestors,
45+
graph: &mut crate::Graph<'_>,
46+
) -> Result<(), Error> {
5547
let mut is_common = false;
56-
if let Some(commit) = self
57-
.graph
58-
.try_lookup_and_insert(id, |current| is_common = current.contains(Flags::COMMON))?
48+
if let Some(commit) = graph
49+
.try_lookup_or_insert_commit(id, |data| is_common = data.flags.contains(Flags::COMMON))?
5950
.filter(|_| !is_common)
6051
{
61-
let mut queue =
62-
gix_revision::PriorityQueue::from_iter(Some((commit.committer_timestamp()?, (id, 0_usize))));
52+
let mut queue = gix_revision::PriorityQueue::from_iter(Some((commit.commit_time, (id, 0_usize))));
6353
if let Mark::ThisCommitAndAncestors = mode {
64-
let current = self.graph.get_mut(&id).expect("just inserted");
65-
*current |= Flags::COMMON;
66-
if current.contains(Flags::SEEN) && !current.contains(Flags::POPPED) {
54+
commit.data.flags |= Flags::COMMON;
55+
if commit.data.flags.contains(Flags::SEEN) && !commit.data.flags.contains(Flags::POPPED) {
6756
self.non_common_revs -= 1;
6857
}
6958
}
70-
let mut parents = SmallVec::new();
7159
while let Some((id, generation)) = queue.pop() {
72-
if self.graph.get(&id).map_or(true, |d| !d.contains(Flags::SEEN)) {
73-
self.add_to_queue(id, Flags::SEEN)?;
60+
if graph
61+
.get(&id)
62+
.map_or(true, |commit| !commit.data.flags.contains(Flags::SEEN))
63+
{
64+
self.add_to_queue(id, Flags::SEEN, graph)?;
7465
} else if matches!(ancestors, Ancestors::AllUnseen) || generation < 2 {
75-
if let Some(commit) = self.graph.try_lookup_and_insert(id, |_| {})? {
76-
collect_parents(commit.iter_parents(), &mut parents)?;
77-
for parent_id in parents.drain(..) {
66+
if let Some(commit) = graph.try_lookup_or_insert_commit(id, |_| {})? {
67+
for parent_id in commit.parents.clone() {
7868
let mut prev_flags = Flags::default();
79-
if let Some(parent) = self
80-
.graph
81-
.try_lookup_and_insert(parent_id, |d| {
82-
prev_flags = *d;
83-
*d |= Flags::COMMON;
69+
if let Some(parent) = graph
70+
.try_lookup_or_insert_commit(parent_id, |data| {
71+
prev_flags = data.flags;
72+
data.flags |= Flags::COMMON;
8473
})?
8574
.filter(|_| !prev_flags.contains(Flags::COMMON))
8675
{
8776
if prev_flags.contains(Flags::SEEN) && !prev_flags.contains(Flags::POPPED) {
8877
self.non_common_revs -= 1;
8978
}
90-
queue.insert(parent.committer_timestamp()?, (parent_id, generation + 1))
79+
queue.insert(parent.commit_time, (parent_id, generation + 1))
9180
}
9281
}
9382
}
@@ -98,38 +87,27 @@ impl<'a> Algorithm<'a> {
9887
}
9988
}
10089

101-
pub(crate) fn collect_parents(
102-
parents: gix_revision::graph::commit::Parents<'_>,
103-
out: &mut SmallVec<[ObjectId; 2]>,
104-
) -> Result<(), Error> {
105-
out.clear();
106-
for parent in parents {
107-
out.push(parent.map_err(|err| match err {
108-
gix_revision::graph::commit::iter_parents::Error::DecodeCommit(err) => Error::DecodeCommit(err),
109-
gix_revision::graph::commit::iter_parents::Error::DecodeCommitGraph(err) => Error::DecodeCommitInGraph(err),
110-
})?);
111-
}
112-
Ok(())
113-
}
114-
115-
impl<'a> Negotiator for Algorithm<'a> {
116-
fn known_common(&mut self, id: ObjectId) -> Result<(), Error> {
117-
if self.graph.get(&id).map_or(true, |d| !d.contains(Flags::SEEN)) {
118-
self.add_to_queue(id, Flags::COMMON_REF | Flags::SEEN)?;
119-
self.mark_common(id, Mark::AncestorsOnly, Ancestors::DirectUnseen)?;
90+
impl Negotiator for Algorithm {
91+
fn known_common(&mut self, id: ObjectId, graph: &mut crate::Graph<'_>) -> Result<(), Error> {
92+
if graph
93+
.get(&id)
94+
.map_or(true, |commit| !commit.data.flags.contains(Flags::SEEN))
95+
{
96+
self.add_to_queue(id, Flags::COMMON_REF | Flags::SEEN, graph)?;
97+
self.mark_common(id, Mark::AncestorsOnly, Ancestors::DirectUnseen, graph)?;
12098
}
12199
Ok(())
122100
}
123101

124-
fn add_tip(&mut self, id: ObjectId) -> Result<(), Error> {
125-
self.add_to_queue(id, Flags::SEEN)
102+
fn add_tip(&mut self, id: ObjectId, graph: &mut crate::Graph<'_>) -> Result<(), Error> {
103+
self.add_to_queue(id, Flags::SEEN, graph)
126104
}
127105

128-
fn next_have(&mut self) -> Option<Result<ObjectId, Error>> {
129-
let mut parents = SmallVec::new();
106+
fn next_have(&mut self, graph: &mut crate::Graph<'_>) -> Option<Result<ObjectId, Error>> {
130107
loop {
131108
let id = self.revs.pop().filter(|_| self.non_common_revs != 0)?;
132-
let flags = self.graph.get_mut(&id).expect("it was added to the graph by now");
109+
let commit = graph.get_mut(&id).expect("it was added to the graph by now");
110+
let flags = &mut commit.data.flags;
133111
*flags |= Flags::POPPED;
134112

135113
if !flags.contains(Flags::COMMON) {
@@ -144,21 +122,17 @@ impl<'a> Negotiator for Algorithm<'a> {
144122
(Some(id), Flags::SEEN)
145123
};
146124

147-
let commit = match self.graph.try_lookup(&id) {
148-
Ok(c) => c.expect("it was found before, must still be there"),
149-
Err(err) => return Some(Err(err.into())),
150-
};
151-
if let Err(err) = collect_parents(commit.iter_parents(), &mut parents) {
152-
return Some(Err(err));
153-
}
154-
for parent_id in parents.drain(..) {
155-
if self.graph.get(&parent_id).map_or(true, |d| !d.contains(Flags::SEEN)) {
156-
if let Err(err) = self.add_to_queue(parent_id, mark) {
125+
for parent_id in commit.parents.clone() {
126+
if graph
127+
.get(&parent_id)
128+
.map_or(true, |commit| !commit.data.flags.contains(Flags::SEEN))
129+
{
130+
if let Err(err) = self.add_to_queue(parent_id, mark, graph) {
157131
return Some(Err(err));
158132
}
159133
}
160134
if mark.contains(Flags::COMMON) {
161-
if let Err(err) = self.mark_common(parent_id, Mark::AncestorsOnly, Ancestors::AllUnseen) {
135+
if let Err(err) = self.mark_common(parent_id, Mark::AncestorsOnly, Ancestors::AllUnseen, graph) {
162136
return Some(Err(err));
163137
}
164138
}
@@ -170,9 +144,11 @@ impl<'a> Negotiator for Algorithm<'a> {
170144
}
171145
}
172146

173-
fn in_common_with_remote(&mut self, id: ObjectId) -> Result<bool, Error> {
174-
let known_to_be_common = self.graph.get(&id).map_or(false, |d| d.contains(Flags::COMMON));
175-
self.mark_common(id, Mark::ThisCommitAndAncestors, Ancestors::DirectUnseen)?;
147+
fn in_common_with_remote(&mut self, id: ObjectId, graph: &mut crate::Graph<'_>) -> Result<bool, Error> {
148+
let known_to_be_common = graph
149+
.get(&id)
150+
.map_or(false, |commit| commit.data.flags.contains(Flags::COMMON));
151+
self.mark_common(id, Mark::ThisCommitAndAncestors, Ancestors::DirectUnseen, graph)?;
176152
Ok(known_to_be_common)
177153
}
178154
}

gix-negotiate/src/lib.rs

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,64 @@
22
//! the pack it sends to only contain what we don't have.
33
#![deny(rust_2018_idioms, missing_docs)]
44
#![forbid(unsafe_code)]
5-
65
mod consecutive;
76
mod noop;
87
mod skipping;
98

9+
bitflags::bitflags! {
10+
/// Multi purpose, shared flags that are used by negotiation algorithms and by the caller as well
11+
///
12+
/// However, in this crate we can't implement the calling side, so we marry this type to whatever is needed in downstream crates.
13+
#[derive(Debug, Default, Copy, Clone)]
14+
pub struct Flags: u8 {
15+
/// The object is already available locally and doesn't need to be fetched by the remote.
16+
const COMPLETE = 1 << 0;
17+
/// A commit from an alternate object database.
18+
const ALTERNATE = 1 << 1;
19+
20+
/// The revision is known to be in common with the remote.
21+
///
22+
/// Used by `consecutive` and `skipping`.
23+
const COMMON = 1 << 2;
24+
/// The revision has entered the priority queue.
25+
///
26+
/// Used by `consecutive` and `skipping`.
27+
const SEEN = 1 << 3;
28+
/// The revision was popped off our primary priority queue, used to avoid double-counting of `non_common_revs`
29+
///
30+
/// Used by `consecutive` and `skipping`.
31+
const POPPED = 1 << 4;
32+
33+
/// The revision is common and was set by merit of a remote tracking ref (e.g. `refs/heads/origin/main`).
34+
///
35+
/// Used by `consecutive`.
36+
const COMMON_REF = 1 << 5;
37+
38+
/// The remote let us know it has the object. We still have to tell the server we have this object or one of its descendants.
39+
/// We won't tell the server about its ancestors.
40+
///
41+
/// Used by `skipping`.
42+
const ADVERTISED = 1 << 6;
43+
}
44+
}
45+
46+
/// Additional data to store with each commit when used by any of our algorithms.
47+
///
48+
/// It's shared among those who use the [`Negotiator`] trait, and all implementations of it.
49+
#[derive(Default, Copy, Clone)]
50+
pub struct Metadata {
51+
/// Used by `skipping`.
52+
/// Only used if commit is not COMMON
53+
pub original_ttl: u16,
54+
/// Used by `skipping`.
55+
pub ttl: u16,
56+
/// Additional information about each commit
57+
pub flags: Flags,
58+
}
59+
60+
/// The graph our callers use to store traversal information, for (re-)use in the negotiation implementation.
61+
pub type Graph<'find> = gix_revision::Graph<'find, gix_revision::graph::Commit<Metadata>>;
62+
1063
/// The way the negotiation is performed.
1164
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
1265
pub enum Algorithm {
@@ -19,6 +72,17 @@ pub enum Algorithm {
1972
Skipping,
2073
}
2174

75+
impl std::fmt::Display for Algorithm {
76+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77+
match self {
78+
Algorithm::Noop => "noop",
79+
Algorithm::Consecutive => "consecutive",
80+
Algorithm::Skipping => "skipping",
81+
}
82+
.fmt(f)
83+
}
84+
}
85+
2286
/// Calculate how many `HAVE` lines we may send in one round, with variation depending on whether the `transport_is_stateless` or not.
2387
/// `window_size` is the previous (or initial) value of the window size.
2488
pub fn window_size(transport_is_stateless: bool, window_size: impl Into<Option<usize>>) -> usize {
@@ -44,26 +108,11 @@ pub fn window_size(transport_is_stateless: bool, window_size: impl Into<Option<u
44108

45109
impl Algorithm {
46110
/// Create an instance of a negotiator which implements this algorithm.
47-
pub fn into_negotiator<'find, Find, E>(
48-
self,
49-
find: Find,
50-
cache: impl Into<Option<gix_commitgraph::Graph>>,
51-
) -> Box<dyn Negotiator + 'find>
52-
where
53-
Find:
54-
for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<Option<gix_object::CommitRefIter<'a>>, E> + 'find,
55-
E: std::error::Error + Send + Sync + 'static,
56-
{
111+
pub fn into_negotiator(self) -> Box<dyn Negotiator> {
57112
match &self {
58113
Algorithm::Noop => Box::new(noop::Noop) as Box<dyn Negotiator>,
59-
Algorithm::Consecutive => {
60-
let graph = gix_revision::Graph::<'_, consecutive::Flags>::new(find, cache);
61-
Box::new(consecutive::Algorithm::new(graph))
62-
}
63-
Algorithm::Skipping => {
64-
let graph = gix_revision::Graph::<'_, skipping::Entry>::new(find, cache);
65-
Box::new(skipping::Algorithm::new(graph))
66-
}
114+
Algorithm::Consecutive => Box::<consecutive::Algorithm>::default(),
115+
Algorithm::Skipping => Box::<skipping::Algorithm>::default(),
67116
}
68117
}
69118
}
@@ -73,32 +122,23 @@ pub trait Negotiator {
73122
/// Mark `id` as common between the remote and us.
74123
///
75124
/// These ids are typically the local tips of remote tracking branches.
76-
fn known_common(&mut self, id: gix_hash::ObjectId) -> Result<(), Error>;
125+
fn known_common(&mut self, id: gix_hash::ObjectId, graph: &mut Graph<'_>) -> Result<(), Error>;
77126

78127
/// Add `id` as starting point of a traversal across commits that aren't necessarily common between the remote and us.
79128
///
80129
/// These tips are usually the commits of local references whose tips should lead to objects that we have in common with the remote.
81-
fn add_tip(&mut self, id: gix_hash::ObjectId) -> Result<(), Error>;
130+
fn add_tip(&mut self, id: gix_hash::ObjectId, graph: &mut Graph<'_>) -> Result<(), Error>;
82131

83132
/// 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.
84133
///
85134
/// Returns `None` if we have exhausted all options, which might mean we have traversed the entire commit graph.
86-
fn next_have(&mut self) -> Option<Result<gix_hash::ObjectId, Error>>;
135+
fn next_have(&mut self, graph: &mut Graph<'_>) -> Option<Result<gix_hash::ObjectId, Error>>;
87136

88137
/// Mark `id` as being common with the remote (as informed by the remote itself) and return `true` if we knew it was common already.
89138
///
90139
/// We can assume to have already seen `id` as we were the one to inform the remote in a prior `have`.
91-
fn in_common_with_remote(&mut self, id: gix_hash::ObjectId) -> Result<bool, Error>;
140+
fn in_common_with_remote(&mut self, id: gix_hash::ObjectId, graph: &mut Graph<'_>) -> Result<bool, Error>;
92141
}
93142

94143
/// An error that happened during any of the methods on a [`Negotiator`].
95-
#[derive(Debug, thiserror::Error)]
96-
#[allow(missing_docs)]
97-
pub enum Error {
98-
#[error(transparent)]
99-
DecodeCommit(#[from] gix_object::decode::Error),
100-
#[error(transparent)]
101-
DecodeCommitInGraph(#[from] gix_commitgraph::file::commit::Error),
102-
#[error(transparent)]
103-
LookupCommitInGraph(#[from] gix_revision::graph::lookup::Error),
104-
}
144+
pub type Error = gix_revision::graph::lookup::commit::Error;

0 commit comments

Comments
 (0)