Skip to content

Commit 81f9a69

Browse files
committed
radicle-surf: refactor design: part 4
- 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 81f9a69

File tree

16 files changed

+528
-377
lines changed

16 files changed

+528
-377
lines changed

radicle-surf/docs/refactor-design.md

Lines changed: 145 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,27 @@ can use it to create a GitHub-like UI for a git repo:
99

1010
1. Code browsing: given a specific commit/ref, browse files and directories.
1111
2. Diff between two revisions that resolve into two commits.
12-
3. Retrieve the history of the commits.
13-
4. Retrieve a specific object: all its metadata.
14-
5. Retrieve the refs: Branches, Tags, Remotes, Notes and user-defined
15-
"categories", where a category is: refs/<category>/<...>.
12+
3. Retrieve the history of commits with a given head, and optionally a file.
13+
4. List and retrieve metadata of refs and objects: Branches, Tags, Remotes,
14+
Notes and user-defined "categories", where a category is: refs/<category>/<...>.
1615

1716
## Motivation
1817

1918
The `radicle-surf` crate aims to provide a safe and easy-to-use API that
2019
supports the features listed in [Introduction]. Based on the existing API,
2120
the main goals of the refactoring are:
2221

23-
- API review: make API simpler whenever possible.
24-
- Address open issues in the original `radicle-surf` repo as much as possible.
25-
- Not to shy away from being `git` specific. (i.e. not to consider supporting
26-
other VCS systems)
27-
- Hide away `git2` from the API. The use of `git2` should be an implementation
28-
detail.
22+
- API review: identify the issues with the current API.
23+
- New API: propose a new API that could reuse parts of the existing API.
24+
- Address open issues in the original `radicle-surf` repo.
25+
- Be `git` specific. (i.e. no need to support other VCS systems)
26+
- Remove `git2` from the public API. The use of `git2` should be an
27+
implementation detail.
2928

3029
## API review
3130

32-
The current API has quite a bit accidental complexity that is not inherent with
33-
the requirements, especially when we can be git specific and don't care about
34-
other VCS systems.
31+
In this section, we review some core types in the current API and propose
32+
changes to them. The main theme is to make the API simpler and easier to use.
3533

3634
### Remove the `Browser`
3735

@@ -80,39 +78,92 @@ We no longer need another layer of indirection defined by `Vcs` trait.
8078
With the changes proposed in the previous section, we describe what the new API
8179
would look like and how they meet the requirements.
8280

83-
### Common principles
81+
### Basic types
8482

85-
#### How to identify things that resolve into a commit
83+
#### Revision and Commit
8684

87-
In our API, it will be good to have a single way to identify all refs and
88-
objects that resolve into commits. In other words, we try to avoid using
89-
different ways at different places. Currently there are multiple types in the
90-
API for this purpose:
85+
In Git, `Revision` commonly resolves into a `Commit` but could refers to other
86+
objects for example a `Blob`. Hence we need to keep both concepts in the API.
87+
Currently we have multiple types to identify a `Commit` or `Revision`.
9188

9289
- Commit
9390
- Oid
9491
- Rev
9592

96-
Because `Rev` is the most high level among those and supports refs already,
97-
I think we should use `Rev` in our API as much as possible.
93+
The relations between them are: all `Rev` and `Commit` can resolve into `Oid`,
94+
and most `Rev`s can resolve into `Commit`.
9895

99-
#### How to identify History
96+
On one hand, `Oid` is the ultimate unique identifer but it is more machine-
97+
friendly than human-friendly. On the other hand, `Revision` is most human-
98+
friendly and better suited in the API interface. A conversion from `Revision`
99+
to `Oid` will be useful.
100100

101-
TBD
101+
For the places where `Commit` is required, we should explicitly ask for
102+
`Commit` instead of `Revision`.
103+
104+
In conclusion, we define two new traits to support the use of `Revision` and
105+
`Commit`:
106+
107+
```Rust
108+
pub trait FromRevision {
109+
/// Resolves a revision into an object id in `repo`.
110+
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error>;
111+
}
112+
113+
pub trait ToCommit {
114+
/// Converts to a commit in `repo`.
115+
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error>;
116+
}
117+
```
118+
119+
These two traits will be implemented for most common representations of
120+
`Revision` and `Commit`, for example `&str`, refs like `Branch`, `Tag`, etc.
121+
Our API will use these traits where we expect a `Revision` or a `Commit`.
122+
123+
#### History
124+
125+
The current `History` is generic over VCS types and also retrieves the full list
126+
of commits when the history is created. The VCS part can be removed and the
127+
history can lazy-load the list of commits by implmenting `Iterator` to support
128+
potentially very long histories.
129+
130+
We can also store the head commit with the history so that it's easy to get
131+
the start point and it helps to identify the history.
132+
133+
To support getting the history of a file, we provide methods to modify a
134+
`History` to filter by a file path.
135+
136+
The new `History` type would look like this:
137+
138+
```Rust
139+
pub struct History<'a> {
140+
repo: RepositoryRef<'a>,
141+
head: Commit,
142+
revwalk: git2::Revwalk<'a>,
143+
filter_by: Option<FilterBy>,
144+
}
145+
146+
enum FilterBy {
147+
File { path: file_system::Path },
148+
}
149+
```
150+
151+
For the methods provided by `History`, please see section [Retrieve the history]
152+
(#retrieve-the-history) below.
102153

103154
### Code browsing
104155

105156
The user should be able to browse the files and directories for any given
106-
commits or references. The core API is:
157+
commit. The core API is:
107158

108159
- Create a root Directory:
109160
```Rust
110-
imp RepositoryRef {
111-
pub fn snapshot(&self, rev: &Rev) -> Result<Directory, Error>;
161+
impl RepositoryRef {
162+
pub fn snapshot<C: ToCommit>(&self, commit: C) -> Result<Directory, Error>;
112163
}
113164
```
114165

115-
- Browse a Directory:
166+
- Browse a Directory's contents:
116167
```Rust
117168
impl Directory {
118169
pub fn contents(&self) -> impl Iterator<Item = &DirectoryContents>;
@@ -135,26 +186,84 @@ pub enum DirectoryContents {
135186

136187
### Diffs
137188

138-
The user would be able to create a diff between any two revisions that resolve
139-
into two commits.
189+
The user would be able to create a diff between any two revisions. In the first
190+
implementation, these revisions have to resolve into commits. But in future,
191+
the revisions could refer to other objects, e.g. files (blobs).
140192

141-
The main change is to use `Rev` instead of `Oid` to identify `from` and `to`.
142193
The core API is:
143194

144195
```Rust
145-
imp RepositoryRef {
146-
pub fn diff(&self, from: &Rev, to: &Rev) -> Result<Diff, Error>;
196+
impl RepositoryRef {
197+
/// Returns the diff between two revisions.
198+
pub fn diff<R: FromRevision>(&self, from: R, to: R) -> Result<Diff, Error>;
199+
200+
/// Returns the diff between a revision and the initial state of the repo.
201+
pub fn initial_diff<R: FromRevision>(&self, rev: R) -> Result<Diff, Error>;
202+
203+
/// Returns the diff of a specific commit.
204+
pub fn diff_from_parent<C: ToCommit>(&self, commit: C) -> Result<Diff, Error>;
147205
}
148206
```
207+
TODO: can we use `diff()` to also support what `initial_diff()` does?
208+
209+
### Retrieve the history
210+
211+
The user would be able to get the list of previous commits reachable from a
212+
particular commit.
149213

150-
To help convert from `Oid` to `Rev`, we provide a helper method:
214+
To create a `History` from a repo with a given head:
151215
```Rust
152-
imp RepositoryRef {
153-
/// Returns the Oid of `rev`.
154-
pub fn rev_oid(&self, rev: &Rev) -> Result<Oid, Error>;
216+
impl RepositoryRef {
217+
pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error>;
155218
}
156219
```
157220

221+
`History` implements `Iterator` that produces `Result<Commit, Error>`, and
222+
also provides these methods:
223+
224+
```Rust
225+
impl<'a> History<'a> {
226+
pub fn new<C: ToCommit>(repo: RepositoryRef<'a>, head: C) -> Result<Self, Error>;
227+
228+
pub fn head(&self) -> &Commit;
229+
230+
// Modifies a history with a filter by `path`.
231+
// This is to support getting the history of a file.
232+
pub fn by_path(mut self, path: file_system::Path) -> Self;
233+
```
234+
235+
- Alternative design:
236+
237+
One potential downside of define `History` as an iterator is that:
238+
`history.next()` takes a mutable history object. A different design is to use
239+
`History` as immutable object that produces an iterator on-demand:
240+
241+
```Rust
242+
pub struct History<'a> {
243+
repo: RepositoryRef<'a>,
244+
head: Commit,
245+
}
246+
247+
impl<'a> History<'a> {
248+
/// This method creats a new `RevWalk` internally and return an
249+
/// iterator for all commits in a history.
250+
pub fn iter(&self) -> impl Iterator<Item = Commit>;
251+
}
252+
```
253+
254+
In this design, `History` does not keep `RevWalk` in its state. It will create
255+
a new one when `iter()` is called. I like the immutable interface of this design
256+
but did not implement it in the current code mainly because the libgit2 doc says
257+
[creating a new `RevWalk` is relatively expensive](https://libgit2.org/libgit2/#HEAD/group/revwalk/git_revwalk_new).
258+
259+
### List and retrieve metadata of refs
260+
261+
TODO: this section is work-in-progress.
262+
263+
Git has four different types of objects: Blob, Tree, Commit and Tag. The `blob`
264+
and `tree` are low-level to git and so I don't think we should expose them in
265+
our API.
266+
158267
## Error handling
159268

160269
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: 19 additions & 11 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>
@@ -142,14 +148,10 @@ pub struct Commits {
142148
/// Will return [`Error`] if the project doesn't exist or the surf interaction
143149
/// fails.
144150
pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
145-
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-
};
151+
let commit = repo.commit(rev)?;
152+
let sha1 = commit.id;
153+
let header = Header::from(&commit);
154+
let diff = repo.diff_from_parent(commit)?;
153155

154156
let mut deletions = 0;
155157
let mut additions = 0;
@@ -199,7 +201,7 @@ pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
199201
.collect();
200202

201203
Ok(Commit {
202-
header: Header::from(&commit),
204+
header,
203205
stats: Stats {
204206
additions,
205207
deletions,
@@ -216,7 +218,7 @@ pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
216218
/// Will return [`Error`] if the project doesn't exist or the surf interaction
217219
/// fails.
218220
pub fn header(repo: &RepositoryRef, sha1: Oid) -> Result<Header, Error> {
219-
let commit = repo.get_commit(sha1)?;
221+
let commit = repo.commit(sha1)?;
220222
Ok(Header::from(&commit))
221223
}
222224

@@ -241,7 +243,13 @@ where
241243
};
242244

243245
let stats = repo.get_stats(&rev)?;
244-
let headers = repo.history(rev)?.iter().map(Header::from).collect();
246+
let headers = repo
247+
.history(&rev)?
248+
.filter_map(|c| match c {
249+
Ok(c) => Some(Header::from(c)),
250+
Err(_) => None,
251+
})
252+
.collect();
245253

246254
Ok(Commits { headers, stats })
247255
}

radicle-surf/src/file_system/path.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ impl From<Path> for Vec<Label> {
169169
}
170170
}
171171

172+
impl From<&Path> for path::PathBuf {
173+
fn from(path: &Path) -> Self {
174+
path.iter()
175+
.map(|label| label.as_str())
176+
.collect::<path::PathBuf>()
177+
}
178+
}
179+
172180
impl git2::IntoCString for Path {
173181
fn into_c_string(self) -> Result<CString, git2::Error> {
174182
if self.is_root() {

radicle-surf/src/object/blob.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ where
141141
{
142142
let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
143143
let revision = maybe_revision.unwrap();
144-
145144
let root = repo.snapshot(&revision)?;
146145
let p = file_system::Path::from_str(path)?;
147146

radicle-surf/src/object/tree.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ where
155155
entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));
156156

157157
let last_commit = if path.is_root() {
158-
Some(commit::Header::from(repo.history(rev).unwrap().first()))
158+
let history = repo.history(&rev)?;
159+
Some(commit::Header::from(history.head()))
159160
} else {
160161
None
161162
};

0 commit comments

Comments
 (0)