Skip to content

Commit beb69a3

Browse files
committed
radicle-surf: refactor design: part 4 (WIP)
- Update design doc. - Refactor History API. - Use FromRevision and IntoCommit traits. - Add more test cases. Signed-off-by: Han Xu <[email protected]>
1 parent 7fe6993 commit beb69a3

File tree

13 files changed

+351
-323
lines changed

13 files changed

+351
-323
lines changed

radicle-surf/docs/refactor-design.md

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ I think we should use `Rev` in our API as much as possible.
9898

9999
#### How to identify History
100100

101-
TBD
101+
Please see section [Retrieve the history](#retrieve-the-history) below.
102102

103103
### Code browsing
104104

@@ -143,18 +143,80 @@ The core API is:
143143

144144
```Rust
145145
imp RepositoryRef {
146+
/// Returns the diff between two revisions.
146147
pub fn diff(&self, from: &Rev, to: &Rev) -> Result<Diff, Error>;
148+
149+
/// Returns the diff between a revision and the initial state of the repo.
150+
pub fn initial_diff(&self, rev: &Rev) -> Result<Diff, Error> {
151+
152+
/// Returns the diff of a specific revision.
153+
pub fn diff_of_rev(&self, rev: &Rev) -> Result<Diff, Error>;
147154
}
148155
```
149156

150-
To help convert from `Oid` to `Rev`, we provide a helper method:
157+
### Retrieve the history
158+
159+
The user would be able to get the list of previous commits reachable from a
160+
particular commit or a ref.
161+
151162
```Rust
152163
imp RepositoryRef {
153-
/// Returns the Oid of `rev`.
154-
pub fn rev_oid(&self, rev: &Rev) -> Result<Oid, Error>;
164+
pub fn history(&self, rev: &Rev) -> Result<History, Error>;
155165
}
156166
```
157167

168+
`History` is defined as an iterator to support potentially very long histories.
169+
170+
```Rust
171+
pub struct History<'a> {
172+
repo: RepositoryRef<'a>,
173+
head: Commit,
174+
revwalk: git2::Revwalk<'a>,
175+
}
176+
177+
impl<'a> History<'a> {
178+
/// `head` is always valid. A history is guaranteed not emnpty.
179+
pub fn head(&self) -> &Commit;
180+
181+
/// Returns an iterator that only iterates commits that has the file `path`.
182+
pub fn has_file(self, path: file_system::Path) -> impl Iterator<Item = Commit> + 'a;
183+
184+
/// Reloads the history so that a caller can iterate it again.
185+
pub fn reload(&mut self);
186+
}
187+
```
188+
Note: the only way to create a new `History` to call `repo.history(rev)`.
189+
190+
- Alternative design:
191+
192+
One potential downside of define `History` as an iterator is that:
193+
`history.next()` takes a mutable history object. A different design is to use
194+
`History` as immutable object that produces an iterator on-demand:
195+
196+
```Rust
197+
pub struct History<'a> {
198+
repo: RepositoryRef<'a>,
199+
head: Commit,
200+
}
201+
202+
impl<'a> History<'a> {
203+
/// This method creats a new `RevWalk` internally and return an
204+
/// iterator for all commits in a history.
205+
pub fn iter(&self) -> impl Iterator<Item = Commit>;
206+
}
207+
```
208+
209+
In this design, `History` does not keep `RevWalk` in its state. It will create
210+
a new one when `iter()` is called. I like the immutable interface of this design
211+
but did not implement it in the current code mainly because the libgit2 doc says
212+
[creating a new `RevWalk` is relatively expensive](https://libgit2.org/libgit2/#HEAD/group/revwalk/git_revwalk_new).
213+
214+
### Retrieve a specific object: all its metadata
215+
216+
Git has four different types of objects: Blob, Tree, Commit and Tag. The `blob`
217+
and `tree` are low-level to git and so I don't think we should expose them in
218+
our API.
219+
158220
## Error handling
159221

160222
TBD

radicle-surf/examples/diff.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ fn main() {
3232
let base_oid = Oid::from_str(&options.base_revision).unwrap();
3333
let now = Instant::now();
3434
let elapsed_nanos = now.elapsed().as_nanos();
35-
let diff = repo
36-
.as_ref()
37-
.diff(&base_oid.into(), &head_oid.into())
38-
.unwrap();
35+
let diff = repo.as_ref().diff(base_oid, head_oid).unwrap();
3936
print_diff_summary(&diff, elapsed_nanos);
4037
}
4138

radicle-surf/src/commit.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ impl From<&git::Commit> for Header {
109109
}
110110
}
111111

112+
impl From<git::Commit> for Header {
113+
fn from(commit: git::Commit) -> Self {
114+
Self::from(&commit)
115+
}
116+
}
117+
112118
#[cfg(feature = "serialize")]
113119
impl Serialize for Header {
114120
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -143,13 +149,8 @@ pub struct Commits {
143149
/// fails.
144150
pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
145151
let sha1 = repo.rev_oid(rev)?;
146-
let commit = repo.get_commit(sha1)?;
147-
let diff = if let Some(parent) = commit.parents.first() {
148-
let parent_rev = (*parent).into();
149-
repo.diff(&parent_rev, rev)?
150-
} else {
151-
repo.initial_diff(rev)?
152-
};
152+
let commit = repo.commit(sha1)?;
153+
let diff = repo.diff_of_commit(&commit)?;
153154

154155
let mut deletions = 0;
155156
let mut additions = 0;
@@ -216,7 +217,7 @@ pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
216217
/// Will return [`Error`] if the project doesn't exist or the surf interaction
217218
/// fails.
218219
pub fn header(repo: &RepositoryRef, sha1: Oid) -> Result<Header, Error> {
219-
let commit = repo.get_commit(sha1)?;
220+
let commit = repo.commit(sha1)?;
220221
Ok(Header::from(&commit))
221222
}
222223

@@ -241,7 +242,7 @@ where
241242
};
242243

243244
let stats = repo.get_stats(&rev)?;
244-
let headers = repo.history(rev)?.iter().map(Header::from).collect();
245+
let headers = repo.history(&rev)?.map(Header::from).collect();
245246

246247
Ok(Commits { headers, stats })
247248
}

radicle-surf/src/object/blob.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ where
141141
{
142142
let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
143143
let revision = maybe_revision.unwrap();
144-
145-
let root = repo.snapshot(&revision)?;
144+
let oid = repo.rev_oid(&revision)?;
145+
let root = repo.snapshot(oid)?;
146146
let p = file_system::Path::from_str(path)?;
147147

148148
let file = root

radicle-surf/src/object/tree.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ where
108108
file_system::Path::from_str(&prefix)?
109109
};
110110

111-
let root_dir = repo.snapshot(&rev)?;
111+
let oid = repo.rev_oid(&rev)?;
112+
let root_dir = repo.snapshot(oid)?;
112113
let prefix_dir = if path.is_root() {
113114
&root_dir
114115
} else {
@@ -155,7 +156,8 @@ where
155156
entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));
156157

157158
let last_commit = if path.is_root() {
158-
Some(commit::Header::from(repo.history(rev).unwrap().first()))
159+
let history = repo.history(&rev)?;
160+
Some(commit::Header::from(history.head()))
159161
} else {
160162
None
161163
};

radicle-surf/src/vcs.rs

Lines changed: 1 addition & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -15,120 +15,6 @@
1515
// You should have received a copy of the GNU General Public License
1616
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1717

18-
//! A model of a general VCS History.
19-
20-
use nonempty::NonEmpty;
18+
//! A model of a general VCS.
2119
2220
pub mod git;
23-
24-
/// A non-empty bag of artifacts which are used to
25-
/// derive a [`crate::file_system::Directory`] view. Examples of artifacts
26-
/// would be commits in Git or patches in Pijul.
27-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
28-
pub struct History<A>(pub NonEmpty<A>);
29-
30-
impl<A> History<A> {
31-
/// Create a new `History` consisting of one artifact.
32-
pub fn new(a: A) -> Self {
33-
History(NonEmpty::new(a))
34-
}
35-
36-
/// Push an artifact to the end of the `History`.
37-
pub fn push(&mut self, a: A) {
38-
self.0.push(a)
39-
}
40-
41-
/// Iterator over the artifacts.
42-
pub fn iter(&self) -> impl Iterator<Item = &A> {
43-
self.0.iter()
44-
}
45-
46-
/// Get the firest artifact in the `History`.
47-
pub fn first(&self) -> &A {
48-
self.0.first()
49-
}
50-
51-
/// Get the length of `History` (aka the artefacts count)
52-
pub fn len(&self) -> usize {
53-
self.0.len()
54-
}
55-
56-
/// Check if `History` is empty
57-
pub fn is_empty(&self) -> bool {
58-
self.0.is_empty()
59-
}
60-
61-
/// Given that the `History` is topological order from most
62-
/// recent artifact to least recent, `find_suffix` gets returns
63-
/// the history up until the point of the given artifact.
64-
///
65-
/// This operation may fail if the artifact does not exist in
66-
/// the given `History`.
67-
pub fn find_suffix(&self, artifact: &A) -> Option<Self>
68-
where
69-
A: Clone + PartialEq,
70-
{
71-
let new_history: Option<NonEmpty<A>> = NonEmpty::from_slice(
72-
&self
73-
.iter()
74-
.cloned()
75-
.skip_while(|current| *current != *artifact)
76-
.collect::<Vec<_>>(),
77-
);
78-
79-
new_history.map(History)
80-
}
81-
82-
/// Apply a function from `A` to `B` over the `History`
83-
pub fn map<F, B>(self, f: F) -> History<B>
84-
where
85-
F: FnMut(A) -> B,
86-
{
87-
History(self.0.map(f))
88-
}
89-
90-
/// Find an artifact in the `History`.
91-
///
92-
/// The function provided should return `Some` if the item is the desired
93-
/// output and `None` otherwise.
94-
pub fn find<F, B>(&self, f: F) -> Option<B>
95-
where
96-
F: Fn(&A) -> Option<B>,
97-
{
98-
self.iter().find_map(f)
99-
}
100-
101-
/// Find an atrifact in the given `History` using the artifacts ID.
102-
///
103-
/// This operation may fail if the artifact does not exist in the history.
104-
pub fn find_in_history<Identifier, F>(&self, identifier: &Identifier, id_of: F) -> Option<A>
105-
where
106-
A: Clone,
107-
F: Fn(&A) -> Identifier,
108-
Identifier: PartialEq,
109-
{
110-
self.iter()
111-
.find(|artifact| {
112-
let current_id = id_of(artifact);
113-
*identifier == current_id
114-
})
115-
.cloned()
116-
}
117-
118-
/// Find all occurences of an artifact in a bag of `History`s.
119-
pub fn find_in_histories<Identifier, F>(
120-
histories: Vec<Self>,
121-
identifier: &Identifier,
122-
id_of: F,
123-
) -> Vec<Self>
124-
where
125-
A: Clone,
126-
F: Fn(&A) -> Identifier + Copy,
127-
Identifier: PartialEq,
128-
{
129-
histories
130-
.into_iter()
131-
.filter(|history| history.find_in_history(identifier, id_of).is_some())
132-
.collect()
133-
}
134-
}

radicle-surf/src/vcs/git.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
//! # }
6464
//! ```
6565
66+
use std::str::FromStr;
67+
6668
// Re-export git2 as sub-module
6769
pub use git2::{self, Error as Git2Error, Time};
6870
pub use radicle_git_ext::Oid;
@@ -75,6 +77,7 @@ mod repo;
7577
pub use repo::{History, Repository, RepositoryRef};
7678

7779
pub mod error;
80+
pub use error::Error;
7881

7982
pub mod ext;
8083

@@ -138,3 +141,35 @@ where
138141
})
139142
}
140143
}
144+
145+
/// Supports various ways to specify a revision used in Git.
146+
pub trait FromRevision {
147+
/// Resolves a revision into an object id in `repo`.
148+
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error>;
149+
}
150+
151+
impl FromRevision for Oid {
152+
fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, Error> {
153+
Ok(*self)
154+
}
155+
}
156+
157+
impl FromRevision for &str {
158+
fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, Error> {
159+
Oid::from_str(*self).map_err(Error::Git)
160+
}
161+
}
162+
163+
impl FromRevision for &Branch {
164+
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
165+
let git2_oid = repo.repo_ref.refname_to_id(&self.refname())?;
166+
Ok(git2_oid.into())
167+
}
168+
}
169+
170+
impl FromRevision for &Tag {
171+
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
172+
let git2_oid = repo.repo_ref.refname_to_id(&self.refname())?;
173+
Ok(git2_oid.into())
174+
}
175+
}

radicle-surf/src/vcs/git/branch.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@
1616
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1717

1818
use crate::vcs::git::{self, error::Error, ext, reference::Ref};
19-
use std::{cmp::Ordering, convert::TryFrom, fmt, str};
20-
2119
#[cfg(feature = "serialize")]
2220
use serde::{Deserialize, Serialize};
21+
use std::{cmp::Ordering, convert::TryFrom, fmt, str};
2322

2423
/// The branch type we want to filter on.
2524
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
@@ -147,13 +146,13 @@ impl Branch {
147146
}
148147

149148
/// Get the name of the `Branch`.
150-
pub fn name(&self) -> String {
149+
pub fn refname(&self) -> String {
151150
let branch_name = self.name.0.clone();
152151
match self.locality {
153-
BranchType::Local => branch_name,
152+
BranchType::Local => format!("refs/heads/{}", branch_name),
154153
BranchType::Remote { ref name } => match name {
155154
None => branch_name,
156-
Some(remote_name) => format!("{}/{}", remote_name, branch_name),
155+
Some(remote_name) => format!("refs/remotes/{}/{}", remote_name, branch_name),
157156
},
158157
}
159158
}

0 commit comments

Comments
 (0)