Skip to content

Commit 1c135e8

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 79b473a commit 1c135e8

File tree

5 files changed

+166
-155
lines changed

5 files changed

+166
-155
lines changed

gix-negotiate/src/consecutive.rs

+46-69
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,15 @@
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

205
pub(crate) struct Algorithm<'find> {
21-
graph: gix_revision::Graph<'find, Flags>,
6+
graph: crate::Graph<'find>,
227
revs: gix_revision::PriorityQueue<CommitterTimestamp, ObjectId>,
238
non_common_revs: usize,
249
}
2510

2611
impl<'a> Algorithm<'a> {
27-
pub fn new(graph: gix_revision::Graph<'a, Flags>) -> Self {
12+
pub fn new(graph: crate::Graph<'a>) -> Self {
2813
Self {
2914
graph,
3015
revs: gix_revision::PriorityQueue::new(),
@@ -35,15 +20,17 @@ impl<'a> Algorithm<'a> {
3520
/// Add `id` to our priority queue and *add* `flags` to it.
3621
fn add_to_queue(&mut self, id: ObjectId, mark: Flags) -> 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) = self
25+
.graph
26+
.try_lookup_or_insert_commit(id, |data| {
27+
has_mark = data.flags.intersects(mark);
28+
data.flags |= mark;
29+
is_common = data.flags.contains(Flags::COMMON);
30+
})?
31+
.filter(|_| !has_mark)
32+
{
33+
self.revs.insert(commit.commit_time, id);
4734
if !is_common {
4835
self.non_common_revs += 1;
4936
}
@@ -55,39 +42,39 @@ impl<'a> Algorithm<'a> {
5542
let mut is_common = false;
5643
if let Some(commit) = self
5744
.graph
58-
.try_lookup_and_insert(id, |current| is_common = current.contains(Flags::COMMON))?
45+
.try_lookup_or_insert_commit(id, |data| is_common = data.flags.contains(Flags::COMMON))?
5946
.filter(|_| !is_common)
6047
{
61-
let mut queue =
62-
gix_revision::PriorityQueue::from_iter(Some((commit.committer_timestamp()?, (id, 0_usize))));
48+
let mut queue = gix_revision::PriorityQueue::from_iter(Some((commit.commit_time, (id, 0_usize))));
6349
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) {
50+
commit.data.flags |= Flags::COMMON;
51+
if commit.data.flags.contains(Flags::SEEN) && !commit.data.flags.contains(Flags::POPPED) {
6752
self.non_common_revs -= 1;
6853
}
6954
}
70-
let mut parents = SmallVec::new();
7155
while let Some((id, generation)) = queue.pop() {
72-
if self.graph.get(&id).map_or(true, |d| !d.contains(Flags::SEEN)) {
56+
if self
57+
.graph
58+
.get(&id)
59+
.map_or(true, |commit| !commit.data.flags.contains(Flags::SEEN))
60+
{
7361
self.add_to_queue(id, Flags::SEEN)?;
7462
} 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(..) {
63+
if let Some(commit) = self.graph.try_lookup_or_insert_commit(id, |_| {})? {
64+
for parent_id in commit.parents.clone() {
7865
let mut prev_flags = Flags::default();
7966
if let Some(parent) = self
8067
.graph
81-
.try_lookup_and_insert(parent_id, |d| {
82-
prev_flags = *d;
83-
*d |= Flags::COMMON;
68+
.try_lookup_or_insert_commit(parent_id, |data| {
69+
prev_flags = data.flags;
70+
data.flags |= Flags::COMMON;
8471
})?
8572
.filter(|_| !prev_flags.contains(Flags::COMMON))
8673
{
8774
if prev_flags.contains(Flags::SEEN) && !prev_flags.contains(Flags::POPPED) {
8875
self.non_common_revs -= 1;
8976
}
90-
queue.insert(parent.committer_timestamp()?, (parent_id, generation + 1))
77+
queue.insert(parent.commit_time, (parent_id, generation + 1))
9178
}
9279
}
9380
}
@@ -98,23 +85,13 @@ impl<'a> Algorithm<'a> {
9885
}
9986
}
10087

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-
11588
impl<'a> Negotiator for Algorithm<'a> {
11689
fn known_common(&mut self, id: ObjectId) -> Result<(), Error> {
117-
if self.graph.get(&id).map_or(true, |d| !d.contains(Flags::SEEN)) {
90+
if self
91+
.graph
92+
.get(&id)
93+
.map_or(true, |commit| !commit.data.flags.contains(Flags::SEEN))
94+
{
11895
self.add_to_queue(id, Flags::COMMON_REF | Flags::SEEN)?;
11996
self.mark_common(id, Mark::AncestorsOnly, Ancestors::DirectUnseen)?;
12097
}
@@ -126,10 +103,10 @@ impl<'a> Negotiator for Algorithm<'a> {
126103
}
127104

128105
fn next_have(&mut self) -> Option<Result<ObjectId, Error>> {
129-
let mut parents = SmallVec::new();
130106
loop {
131107
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");
108+
let commit = self.graph.get_mut(&id).expect("it was added to the graph by now");
109+
let flags = &mut commit.data.flags;
133110
*flags |= Flags::POPPED;
134111

135112
if !flags.contains(Flags::COMMON) {
@@ -144,15 +121,12 @@ impl<'a> Negotiator for Algorithm<'a> {
144121
(Some(id), Flags::SEEN)
145122
};
146123

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)) {
124+
for parent_id in commit.parents.clone() {
125+
if self
126+
.graph
127+
.get(&parent_id)
128+
.map_or(true, |commit| !commit.data.flags.contains(Flags::SEEN))
129+
{
156130
if let Err(err) = self.add_to_queue(parent_id, mark) {
157131
return Some(Err(err));
158132
}
@@ -171,7 +145,10 @@ impl<'a> Negotiator for Algorithm<'a> {
171145
}
172146

173147
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));
148+
let known_to_be_common = self
149+
.graph
150+
.get(&id)
151+
.map_or(false, |commit| commit.data.flags.contains(Flags::COMMON));
175152
self.mark_common(id, Mark::ThisCommitAndAncestors, Ancestors::DirectUnseen)?;
176153
Ok(known_to_be_common)
177154
}

gix-negotiate/src/lib.rs

+59-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,65 @@
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 was already handled and doesn't need to be looked at again.
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(crate) original_ttl: u16,
54+
/// Used by `skipping`.
55+
pub(crate) ttl: u16,
56+
57+
/// Additional information about each commit
58+
pub(crate) flags: Flags,
59+
}
60+
61+
/// The graph our callers use to store traversal information, for (re-)use in the negotiation implementation.
62+
pub type Graph<'find> = gix_revision::Graph<'find, gix_revision::graph::Commit<Metadata>>;
63+
1064
/// The way the negotiation is performed.
1165
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
1266
pub enum Algorithm {
@@ -57,11 +111,11 @@ impl Algorithm {
57111
match &self {
58112
Algorithm::Noop => Box::new(noop::Noop) as Box<dyn Negotiator>,
59113
Algorithm::Consecutive => {
60-
let graph = gix_revision::Graph::<'_, consecutive::Flags>::new(find, cache);
114+
let graph = Graph::new(find, cache);
61115
Box::new(consecutive::Algorithm::new(graph))
62116
}
63117
Algorithm::Skipping => {
64-
let graph = gix_revision::Graph::<'_, skipping::Entry>::new(find, cache);
118+
let graph = Graph::new(find, cache);
65119
Box::new(skipping::Algorithm::new(graph))
66120
}
67121
}
@@ -101,4 +155,6 @@ pub enum Error {
101155
DecodeCommitInGraph(#[from] gix_commitgraph::file::commit::Error),
102156
#[error(transparent)]
103157
LookupCommitInGraph(#[from] gix_revision::graph::lookup::Error),
158+
#[error(transparent)]
159+
LookupCommitInGraphTBD(#[from] gix_revision::graph::lookup::commit::Error), // TODO: should replace most of the above
104160
}

0 commit comments

Comments
 (0)