Skip to content

Commit 33e05fc

Browse files
committed
feat: A Graph for quick access to commits and for associating state with them.
This data structure should be used whenever stateful traversal is required, usually by associating information with each commit to remember what was seen and what wasn't.
1 parent 96b5b16 commit 33e05fc

File tree

6 files changed

+188
-1
lines changed

6 files changed

+188
-1
lines changed

Cargo.lock

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

gix-revision/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ gix-hash = { version = "^0.11.1", path = "../gix-hash" }
2222
gix-object = { version = "^0.29.2", path = "../gix-object" }
2323
gix-date = { version = "^0.5.0", path = "../gix-date" }
2424
gix-hashtable = { version = "^0.2.0", path = "../gix-hashtable" }
25+
gix-commitgraph = { version = "0.14.0", path = "../gix-commitgraph" }
2526

2627
bstr = { version = "1.3.0", default-features = false, features = ["std"]}
2728
thiserror = "1.0.26"

gix-revision/src/graph.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use crate::Graph;
2+
use gix_hash::oid;
3+
use std::ops::Index;
4+
5+
impl<'find, T> Graph<'find, T> {
6+
/// Create a new instance with `find` to retrieve commits and optionally `cache` to accelerate commit access.
7+
pub fn new<Find, E>(mut find: Find, cache: impl Into<Option<gix_commitgraph::Graph>>) -> Self
8+
where
9+
Find:
10+
for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<Option<gix_object::CommitRefIter<'a>>, E> + 'find,
11+
E: std::error::Error + Send + Sync + 'static,
12+
{
13+
Graph {
14+
find: Box::new(move |id, buf| {
15+
find(id, buf).map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)
16+
}),
17+
cache: cache.into(),
18+
set: gix_hashtable::HashMap::default(),
19+
buf: Vec::new(),
20+
parent_buf: Vec::new(),
21+
}
22+
}
23+
24+
/// Returns true if `id` has data associated with it, meaning that we processed it already.
25+
pub fn contains(&self, id: &gix_hash::oid) -> bool {
26+
self.set.contains_key(id.as_ref())
27+
}
28+
29+
/// Returns the data associated with `id` if available.
30+
pub fn get(&self, id: &gix_hash::oid) -> Option<&T> {
31+
self.set.get(id)
32+
}
33+
34+
/// Returns the data associated with `id` if available as mutable reference.
35+
pub fn get_mut(&mut self, id: &gix_hash::oid) -> Option<&mut T> {
36+
self.set.get_mut(id)
37+
}
38+
39+
/// Insert `id` into the graph and associate it with `value`, returning the previous value associated with it if it existed.
40+
pub fn insert(&mut self, id: gix_hash::ObjectId, value: T) -> Option<T> {
41+
self.set.insert(id, value)
42+
}
43+
44+
/// Remove all data from the graph to start over.
45+
pub fn clear(&mut self) {
46+
self.set.clear();
47+
}
48+
49+
#[allow(missing_docs)]
50+
pub fn try_lookup_mut(&mut self, id: &gix_hash::oid) -> Result<Option<CommitMut<'_, 'find, T>>, lookup::Error> {
51+
match (self.find)(id, &mut self.buf)? {
52+
Some(_) => Ok(Some(CommitMut { graph: self })),
53+
None => Ok(None),
54+
}
55+
}
56+
}
57+
58+
impl<'a, 'find, T> Index<&'a gix_hash::oid> for Graph<'find, T> {
59+
type Output = T;
60+
61+
fn index(&self, index: &'a oid) -> &Self::Output {
62+
&self.set[index]
63+
}
64+
}
65+
66+
///
67+
pub mod lookup {
68+
/// The error returned by [`try_lookup()`][super::Graph::try_lookup()].
69+
#[derive(Debug, thiserror::Error)]
70+
#[allow(missing_docs)]
71+
pub enum Error {
72+
#[error("Could not find commit")]
73+
Find(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
74+
}
75+
}
76+
77+
///
78+
pub mod insert_parents {
79+
/// The error returned by [`CommitMut::insert_parents()`][super::CommitMut::insert_parents()].
80+
#[derive(Debug, thiserror::Error)]
81+
#[allow(missing_docs)]
82+
pub enum Error {
83+
#[error("Could not find commit")]
84+
Find(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
85+
#[error("A commit could not be decoded during traversal")]
86+
Decode(#[from] gix_object::decode::Error),
87+
}
88+
}
89+
90+
/// A commit that provides access to graph-related information.
91+
pub struct CommitMut<'graph, 'find, T> {
92+
graph: &'graph mut Graph<'find, T>,
93+
}
94+
95+
mod commit {
96+
use super::insert_parents;
97+
use super::CommitMut;
98+
use gix_hashtable::hash_map;
99+
100+
impl<'graph, 'find, T> CommitMut<'graph, 'find, T> {
101+
#[allow(missing_docs)]
102+
pub fn insert_parents(&mut self) -> Result<(), insert_parents::Error> {
103+
let mut commit_iter = gix_object::CommitRefIter::from_bytes(&self.graph.buf);
104+
for token in commit_iter {
105+
match token {
106+
Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
107+
Ok(gix_object::commit::ref_iter::Token::Parent { id: parent_id }) => {
108+
match self.graph.set.entry(parent_id) {
109+
hash_map::Entry::Vacant(entry) => {
110+
let parent = match (self.graph.find)(&parent_id, &mut self.graph.parent_buf)? {
111+
Some(p) => p,
112+
None => continue, // skip missing objects, they don't exist.
113+
};
114+
115+
let parent_commit_date = parent
116+
.committer()
117+
.map(|committer| committer.time.seconds_since_unix_epoch)
118+
.unwrap_or_default();
119+
120+
// TODO: call user function
121+
// entry.insert(commit_flags);
122+
}
123+
hash_map::Entry::Occupied(mut entry) => {
124+
todo!()
125+
}
126+
}
127+
}
128+
Ok(_unused_token) => break,
129+
Err(err) => return Err(err.into()),
130+
}
131+
// TODO: check if we should abort
132+
}
133+
Ok(())
134+
}
135+
}
136+
}

gix-revision/src/lib.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,33 @@ pub mod spec;
1919
mod types;
2020
pub use types::Spec;
2121

22+
/// A graph of commits which additionally allows to associate data with commits.
23+
///
24+
/// It starts empty, but each access may fill it with commit information.
25+
/// Note that the traversal can be accelerated if a [commit-graph][gix_commitgraph::Graph] is also made available.
26+
pub struct Graph<'find, T> {
27+
/// A way to resolve a commit from the object database.
28+
find: Box<
29+
dyn for<'a> FnMut(
30+
&gix_hash::oid,
31+
&'a mut Vec<u8>,
32+
) -> Result<
33+
Option<gix_object::CommitRefIter<'a>>,
34+
Box<dyn std::error::Error + Send + Sync + 'static>,
35+
> + 'find,
36+
>,
37+
/// A way to speedup commit access, essentially a multi-file commit database.
38+
cache: Option<gix_commitgraph::Graph>,
39+
/// The set of cached commits that we have seen once, along with data associated with them.
40+
set: gix_hashtable::HashMap<gix_hash::ObjectId, T>,
41+
/// A buffer for writing commit data into.
42+
buf: Vec<u8>,
43+
/// Another buffer we typically use to store parents.
44+
parent_buf: Vec<u8>,
45+
}
46+
///
47+
pub mod graph;
48+
2249
/// A utility type implementing a queue which can be used to automatically sort data by its time in ascending order.
2350
#[derive(Default)]
2451
pub struct PriorityQueue<K: Ord, T>(VecDeque<(K, T)>);

gix-revision/tests/describe/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ fn shallow_yields_result_if_refs_are_available() {
241241
assert_eq!(res.into_format(7).to_string(), "at-c5-1-g01ec18a");
242242
}
243243

244-
fn repo() -> Repository {
244+
pub(crate) fn repo() -> Repository {
245245
let dir = gix_testtools::scripted_fixture_read_only_standalone("make_repo_with_branches.sh").unwrap();
246246
gix::open(dir).unwrap()
247247
}

gix-revision/tests/revision.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
mod describe;
22
mod spec;
3+
mod graph {
4+
use crate::hex_to_id;
5+
use gix::odb::Find;
6+
use gix_revision::Graph;
7+
8+
#[test]
9+
fn parent_insertion_theory() -> crate::Result {
10+
let repo = crate::describe::repo();
11+
let head_id = repo.head_id()?;
12+
let mut g = Graph::<'_, ()>::new(
13+
|id, buf| {
14+
repo.objects
15+
.try_find(id, buf)
16+
.map(|d| d.and_then(|d| d.try_into_commit_iter()))
17+
},
18+
None,
19+
);
20+
let mut c = g.try_lookup_mut(&head_id)?.expect("available");
21+
c.insert_parents();
22+
Ok(())
23+
}
24+
}
325

426
pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + 'static>>;
527

0 commit comments

Comments
 (0)