mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-24 13:50:36 +08:00
Merge remote-tracking branch 'han/new-design-4'
* han/new-design-4: radicle-surf: refactor design: part 4 Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -9,10 +9,9 @@ can use it to create a GitHub-like UI for a git repo:
|
||||
|
||||
1. Code browsing: given a specific commit/ref, browse files and directories.
|
||||
2. Diff between two revisions that resolve into two commits.
|
||||
3. Retrieve the history of the commits.
|
||||
4. Retrieve a specific object: all its metadata.
|
||||
5. Retrieve the refs: Branches, Tags, Remotes, Notes and user-defined
|
||||
"categories", where a category is: refs/<category>/<...>.
|
||||
3. Retrieve the history of commits with a given head, and optionally a file.
|
||||
4. List refs and retrieve their metadata: Branches, Tags, Remotes,
|
||||
Notes and user-defined "categories", where a category is: refs/<category>/<...>.
|
||||
|
||||
## Motivation
|
||||
|
||||
@@ -20,18 +19,17 @@ The `radicle-surf` crate aims to provide a safe and easy-to-use API that
|
||||
supports the features listed in [Introduction]. Based on the existing API,
|
||||
the main goals of the refactoring are:
|
||||
|
||||
- API review: make API simpler whenever possible.
|
||||
- Address open issues in the original `radicle-surf` repo as much as possible.
|
||||
- Not to shy away from being `git` specific. (i.e. not to consider supporting
|
||||
other VCS systems)
|
||||
- Hide away `git2` from the API. The use of `git2` should be an implementation
|
||||
detail.
|
||||
- API review: identify the issues with the current API.
|
||||
- New API: propose a new API that could reuse parts of the existing API.
|
||||
- Address open issues in the original `radicle-surf` repo.
|
||||
- Be `git` specific. (i.e. no need to support other VCS systems)
|
||||
- Remove `git2` from the public API. The use of `git2` should be an
|
||||
implementation detail.
|
||||
|
||||
## API review
|
||||
|
||||
The current API has quite a bit accidental complexity that is not inherent with
|
||||
the requirements, especially when we can be git specific and don't care about
|
||||
other VCS systems.
|
||||
In this section, we review some core types in the current API and propose
|
||||
changes to them. The main theme is to make the API simpler and easier to use.
|
||||
|
||||
### Remove the `Browser`
|
||||
|
||||
@@ -80,39 +78,121 @@ We no longer need another layer of indirection defined by `Vcs` trait.
|
||||
With the changes proposed in the previous section, we describe what the new API
|
||||
would look like and how they meet the requirements.
|
||||
|
||||
### Common principles
|
||||
### Basic types
|
||||
|
||||
#### How to identify things that resolve into a commit
|
||||
#### Revision and Commit
|
||||
|
||||
In our API, it will be good to have a single way to identify all refs and
|
||||
objects that resolve into commits. In other words, we try to avoid using
|
||||
different ways at different places. Currently there are multiple types in the
|
||||
API for this purpose:
|
||||
In Git, `Revision` commonly resolves into a `Commit` but could refers to other
|
||||
objects for example a `Blob`. Hence we need to keep both concepts in the API.
|
||||
Currently we have multiple types to identify a `Commit` or `Revision`.
|
||||
|
||||
- Commit
|
||||
- Oid
|
||||
- Rev
|
||||
|
||||
Because `Rev` is the most high level among those and supports refs already,
|
||||
I think we should use `Rev` in our API as much as possible.
|
||||
The relations between them are: all `Rev` and `Commit` can resolve into `Oid`,
|
||||
and most `Rev`s can resolve into `Commit`.
|
||||
|
||||
#### How to identify History
|
||||
On one hand, `Oid` is the ultimate unique identifer but it is more machine-
|
||||
friendly than human-friendly. On the other hand, `Revision` is most human-
|
||||
friendly and better suited in the API interface. A conversion from `Revision`
|
||||
to `Oid` will be useful.
|
||||
|
||||
TBD
|
||||
For the places where `Commit` is required, we should explicitly ask for
|
||||
`Commit` instead of `Revision`.
|
||||
|
||||
In conclusion, we define two new traits to support the use of `Revision` and
|
||||
`Commit`:
|
||||
|
||||
```Rust
|
||||
pub trait Revision {
|
||||
/// Resolves a revision into an object id in `repo`.
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error>;
|
||||
}
|
||||
|
||||
pub trait ToCommit {
|
||||
/// Converts to a commit in `repo`.
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error>;
|
||||
}
|
||||
```
|
||||
|
||||
These two traits will be implemented for most common representations of
|
||||
`Revision` and `Commit`, for example `&str`, refs like `Branch`, `Tag`, etc.
|
||||
Our API will use these traits where we expect a `Revision` or a `Commit`.
|
||||
|
||||
#### History
|
||||
|
||||
The current `History` is generic over VCS types and also retrieves the full list
|
||||
of commits when the history is created. The VCS part can be removed and the
|
||||
history can lazy-load the list of commits by implmenting `Iterator` to support
|
||||
potentially very long histories.
|
||||
|
||||
We can also store the head commit with the history so that it's easy to get
|
||||
the start point and it helps to identify the history.
|
||||
|
||||
To support getting the history of a file, we provide methods to modify a
|
||||
`History` to filter by a file path.
|
||||
|
||||
The new `History` type would look like this:
|
||||
|
||||
```Rust
|
||||
pub struct History<'a> {
|
||||
repo: RepositoryRef<'a>,
|
||||
head: Commit,
|
||||
revwalk: git2::Revwalk<'a>,
|
||||
filter_by: Option<FilterBy>,
|
||||
}
|
||||
|
||||
enum FilterBy {
|
||||
File { path: file_system::Path },
|
||||
}
|
||||
```
|
||||
|
||||
For the methods provided by `History`, please see section [Retrieve the history]
|
||||
(#retrieve-the-history) below.
|
||||
|
||||
#### Commit
|
||||
|
||||
`Commit` is a central concept in Git. In `radicle-surf` we define `Commit` type
|
||||
to represent its metadata:
|
||||
|
||||
```Rust
|
||||
pub struct Commit {
|
||||
/// Object Id
|
||||
pub id: Oid,
|
||||
/// The author of the commit.
|
||||
pub author: Author,
|
||||
/// The actor who committed this commit.
|
||||
pub committer: Author,
|
||||
/// The long form message of the commit.
|
||||
pub message: String,
|
||||
/// The summary message of the commit.
|
||||
pub summary: String,
|
||||
/// The parents of this commit.
|
||||
pub parents: Vec<Oid>,
|
||||
}
|
||||
```
|
||||
|
||||
To get the content (i.e. the tree object) of the commit, the user should use
|
||||
`snapshot` method described in [Code browsing](#code-browsing) section.
|
||||
|
||||
To get the diff of the commit, the user should use `diff_from_parent` method
|
||||
described in [Diffs](#diffs) section. Note that we might move that method to
|
||||
`Commit` type itself.
|
||||
|
||||
### Code browsing
|
||||
|
||||
The user should be able to browse the files and directories for any given
|
||||
commits or references. The core API is:
|
||||
commit. The core API is:
|
||||
|
||||
- Create a root Directory:
|
||||
```Rust
|
||||
imp RepositoryRef {
|
||||
pub fn snapshot(&self, rev: &Rev) -> Result<Directory, Error>;
|
||||
impl RepositoryRef {
|
||||
pub fn snapshot<C: ToCommit>(&self, commit: C) -> Result<Directory, Error>;
|
||||
}
|
||||
```
|
||||
|
||||
- Browse a Directory:
|
||||
- Browse a Directory's contents:
|
||||
```Rust
|
||||
impl Directory {
|
||||
pub fn contents(&self) -> impl Iterator<Item = &DirectoryContents>;
|
||||
@@ -135,26 +215,142 @@ pub enum DirectoryContents {
|
||||
|
||||
### Diffs
|
||||
|
||||
The user would be able to create a diff between any two revisions that resolve
|
||||
into two commits.
|
||||
The user would be able to create a diff between any two revisions. In the first
|
||||
implementation, these revisions have to resolve into commits. But in future,
|
||||
the revisions could refer to other objects, e.g. files (blobs).
|
||||
|
||||
The main change is to use `Rev` instead of `Oid` to identify `from` and `to`.
|
||||
The core API is:
|
||||
|
||||
```Rust
|
||||
imp RepositoryRef {
|
||||
pub fn diff(&self, from: &Rev, to: &Rev) -> Result<Diff, Error>;
|
||||
impl RepositoryRef {
|
||||
/// Returns the diff between two revisions.
|
||||
pub fn diff<R: Revision>(&self, from: R, to: R) -> Result<Diff, Error>;
|
||||
}
|
||||
```
|
||||
|
||||
To help convert from `Oid` to `Rev`, we provide a helper method:
|
||||
We used to have the following method:
|
||||
```Rust
|
||||
imp RepositoryRef {
|
||||
/// Returns the Oid of `rev`.
|
||||
pub fn rev_oid(&self, rev: &Rev) -> Result<Oid, Error>;
|
||||
/// Returns the diff between a revision and the initial state of the repo.
|
||||
pub fn initial_diff<R: Revision>(&self, rev: R) -> Result<Diff, Error>;
|
||||
```
|
||||
|
||||
However, it is not comparing with any other commit so the output is basically
|
||||
the snapshot of `R`. I am not sure if it is necessary. My take is that we can
|
||||
remove this method.
|
||||
|
||||
We also have the following method:
|
||||
```Rust
|
||||
/// Returns the diff of a specific commit.
|
||||
pub fn diff_from_parent<C: ToCommit>(&self, commit: C) -> Result<Diff, Error>;
|
||||
```
|
||||
|
||||
I think it is probably better to instead define the above method as
|
||||
`Commit::diff()` as its output is associated with a `Commit`.
|
||||
|
||||
### Retrieve the history
|
||||
|
||||
The user would be able to get the list of previous commits reachable from a
|
||||
particular commit.
|
||||
|
||||
To create a `History` from a repo with a given head:
|
||||
```Rust
|
||||
impl RepositoryRef {
|
||||
pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error>;
|
||||
}
|
||||
```
|
||||
|
||||
`History` implements `Iterator` that produces `Result<Commit, Error>`, and
|
||||
also provides these methods:
|
||||
|
||||
```Rust
|
||||
impl<'a> History<'a> {
|
||||
pub fn new<C: ToCommit>(repo: RepositoryRef<'a>, head: C) -> Result<Self, Error>;
|
||||
|
||||
pub fn head(&self) -> &Commit;
|
||||
|
||||
// Modifies a history with a filter by `path`.
|
||||
// This is to support getting the history of a file.
|
||||
pub fn by_path(mut self, path: file_system::Path) -> Self;
|
||||
```
|
||||
|
||||
- Alternative design:
|
||||
|
||||
One potential downside of define `History` as an iterator is that:
|
||||
`history.next()` takes a mutable history object. A different design is to use
|
||||
`History` as immutable object that produces an iterator on-demand:
|
||||
|
||||
```Rust
|
||||
pub struct History<'a> {
|
||||
repo: RepositoryRef<'a>,
|
||||
head: Commit,
|
||||
}
|
||||
|
||||
impl<'a> History<'a> {
|
||||
/// This method creats a new `RevWalk` internally and return an
|
||||
/// iterator for all commits in a history.
|
||||
pub fn iter(&self) -> impl Iterator<Item = Commit>;
|
||||
}
|
||||
```
|
||||
|
||||
In this design, `History` does not keep `RevWalk` in its state. It will create
|
||||
a new one when `iter()` is called. I like the immutable interface of this design
|
||||
but did not implement it in the current code mainly because the libgit2 doc says
|
||||
[creating a new `RevWalk` is relatively expensive](https://libgit2.org/libgit2/#HEAD/group/revwalk/git_revwalk_new).
|
||||
|
||||
### List refs and retrieve their metadata
|
||||
|
||||
Git refs are simple names that point to objects using object IDs. `radicle-surf`
|
||||
support refs by its `Ref` type.
|
||||
|
||||
```Rust
|
||||
pub enum Ref {
|
||||
/// A git tag, which can be found under `.git/refs/tags/`.
|
||||
Tag {
|
||||
/// The name of the tag, e.g. `v1.0.0`.
|
||||
name: TagName,
|
||||
},
|
||||
/// A git branch, which can be found under `.git/refs/heads/`.
|
||||
LocalBranch {
|
||||
/// The name of the branch, e.g. `master`.
|
||||
name: BranchName,
|
||||
},
|
||||
/// A git branch, which can be found under `.git/refs/remotes/`.
|
||||
RemoteBranch {
|
||||
/// The remote name, e.g. `origin`.
|
||||
remote: String,
|
||||
/// The name of the branch, e.g. `master`.
|
||||
name: BranchName,
|
||||
},
|
||||
/// A git namespace, which can be found under `.git/refs/namespaces/`.
|
||||
///
|
||||
/// Note that namespaces can be nested.
|
||||
Namespace {
|
||||
/// The name value of the namespace.
|
||||
namespace: String,
|
||||
/// The reference under that namespace, e.g. The
|
||||
/// `refs/remotes/origin/master/ portion of `refs/namespaces/
|
||||
/// moi/refs/remotes/origin/master`.
|
||||
reference: Box<Ref>,
|
||||
},
|
||||
/// A git notes, which can be found under `.git/refs/notes`
|
||||
Notes {
|
||||
/// The default name is "commits".
|
||||
name: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Git Objects
|
||||
|
||||
Git has four kinds of objects: Blob, Tree, Commit and Tag. We have already
|
||||
discussed `Commit` (a struct) and `Tag` (`Ref::Tag`) types. For `blob`, we
|
||||
use `File` type to represent, and for `tree`, we use `Directory` type to
|
||||
represent. The motivation is to let the user "surf" a repo as a file system
|
||||
as much as possible, and to avoid Git internal concepts in our API if possible.
|
||||
|
||||
Open question: there could be some cases where the names `blob` and `tree`
|
||||
shall be used. We need to define such cases clearly if they exist.
|
||||
|
||||
## Error handling
|
||||
|
||||
TBD
|
||||
|
||||
@@ -32,10 +32,7 @@ fn main() {
|
||||
let base_oid = Oid::from_str(&options.base_revision).unwrap();
|
||||
let now = Instant::now();
|
||||
let elapsed_nanos = now.elapsed().as_nanos();
|
||||
let diff = repo
|
||||
.as_ref()
|
||||
.diff(&base_oid.into(), &head_oid.into())
|
||||
.unwrap();
|
||||
let diff = repo.as_ref().diff(base_oid, head_oid).unwrap();
|
||||
print_diff_summary(&diff, elapsed_nanos);
|
||||
}
|
||||
|
||||
|
||||
+15
-12
@@ -109,6 +109,12 @@ impl From<&git::Commit> for Header {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git::Commit> for Header {
|
||||
fn from(commit: git::Commit) -> Self {
|
||||
Self::from(&commit)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
impl Serialize for Header {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
@@ -142,14 +148,10 @@ pub struct Commits {
|
||||
/// Will return [`Error`] if the project doesn't exist or the surf interaction
|
||||
/// fails.
|
||||
pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
|
||||
let sha1 = repo.rev_oid(rev)?;
|
||||
let commit = repo.get_commit(sha1)?;
|
||||
let diff = if let Some(parent) = commit.parents.first() {
|
||||
let parent_rev = (*parent).into();
|
||||
repo.diff(&parent_rev, rev)?
|
||||
} else {
|
||||
repo.initial_diff(rev)?
|
||||
};
|
||||
let commit = repo.commit(rev)?;
|
||||
let sha1 = commit.id;
|
||||
let header = Header::from(&commit);
|
||||
let diff = repo.diff_from_parent(commit)?;
|
||||
|
||||
let mut deletions = 0;
|
||||
let mut additions = 0;
|
||||
@@ -199,7 +201,7 @@ pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
|
||||
.collect();
|
||||
|
||||
Ok(Commit {
|
||||
header: Header::from(&commit),
|
||||
header,
|
||||
stats: Stats {
|
||||
additions,
|
||||
deletions,
|
||||
@@ -216,7 +218,7 @@ pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
|
||||
/// Will return [`Error`] if the project doesn't exist or the surf interaction
|
||||
/// fails.
|
||||
pub fn header(repo: &RepositoryRef, sha1: Oid) -> Result<Header, Error> {
|
||||
let commit = repo.get_commit(sha1)?;
|
||||
let commit = repo.commit(sha1)?;
|
||||
Ok(Header::from(&commit))
|
||||
}
|
||||
|
||||
@@ -240,8 +242,9 @@ where
|
||||
None => repo.head_oid()?.into(),
|
||||
};
|
||||
|
||||
let stats = repo.get_stats(&rev)?;
|
||||
let headers = repo.history(rev)?.iter().map(Header::from).collect();
|
||||
let stats = repo.get_commit_stats(&rev)?;
|
||||
let commits: Result<Vec<git::Commit>, git::Error> = repo.history(&rev)?.collect();
|
||||
let headers = commits?.iter().map(Header::from).collect();
|
||||
|
||||
Ok(Commits { headers, stats })
|
||||
}
|
||||
|
||||
@@ -169,6 +169,14 @@ impl From<Path> for Vec<Label> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Path> for path::PathBuf {
|
||||
fn from(path: &Path) -> Self {
|
||||
path.iter()
|
||||
.map(|label| label.as_str())
|
||||
.collect::<path::PathBuf>()
|
||||
}
|
||||
}
|
||||
|
||||
impl git2::IntoCString for Path {
|
||||
fn into_c_string(self) -> Result<CString, git2::Error> {
|
||||
if self.is_root() {
|
||||
|
||||
@@ -141,7 +141,6 @@ where
|
||||
{
|
||||
let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
|
||||
let revision = maybe_revision.unwrap();
|
||||
|
||||
let root = repo.snapshot(&revision)?;
|
||||
let p = file_system::Path::from_str(path)?;
|
||||
|
||||
|
||||
@@ -155,7 +155,8 @@ where
|
||||
entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));
|
||||
|
||||
let last_commit = if path.is_root() {
|
||||
Some(commit::Header::from(repo.history(rev).unwrap().first()))
|
||||
let history = repo.history(&rev)?;
|
||||
Some(commit::Header::from(history.head()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
+1
-115
@@ -15,120 +15,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! A model of a general VCS History.
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
//! A model of a general VCS.
|
||||
|
||||
pub mod git;
|
||||
|
||||
/// A non-empty bag of artifacts which are used to
|
||||
/// derive a [`crate::file_system::Directory`] view. Examples of artifacts
|
||||
/// would be commits in Git or patches in Pijul.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct History<A>(pub NonEmpty<A>);
|
||||
|
||||
impl<A> History<A> {
|
||||
/// Create a new `History` consisting of one artifact.
|
||||
pub fn new(a: A) -> Self {
|
||||
History(NonEmpty::new(a))
|
||||
}
|
||||
|
||||
/// Push an artifact to the end of the `History`.
|
||||
pub fn push(&mut self, a: A) {
|
||||
self.0.push(a)
|
||||
}
|
||||
|
||||
/// Iterator over the artifacts.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &A> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Get the firest artifact in the `History`.
|
||||
pub fn first(&self) -> &A {
|
||||
self.0.first()
|
||||
}
|
||||
|
||||
/// Get the length of `History` (aka the artefacts count)
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Check if `History` is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Given that the `History` is topological order from most
|
||||
/// recent artifact to least recent, `find_suffix` gets returns
|
||||
/// the history up until the point of the given artifact.
|
||||
///
|
||||
/// This operation may fail if the artifact does not exist in
|
||||
/// the given `History`.
|
||||
pub fn find_suffix(&self, artifact: &A) -> Option<Self>
|
||||
where
|
||||
A: Clone + PartialEq,
|
||||
{
|
||||
let new_history: Option<NonEmpty<A>> = NonEmpty::from_slice(
|
||||
&self
|
||||
.iter()
|
||||
.cloned()
|
||||
.skip_while(|current| *current != *artifact)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
new_history.map(History)
|
||||
}
|
||||
|
||||
/// Apply a function from `A` to `B` over the `History`
|
||||
pub fn map<F, B>(self, f: F) -> History<B>
|
||||
where
|
||||
F: FnMut(A) -> B,
|
||||
{
|
||||
History(self.0.map(f))
|
||||
}
|
||||
|
||||
/// Find an artifact in the `History`.
|
||||
///
|
||||
/// The function provided should return `Some` if the item is the desired
|
||||
/// output and `None` otherwise.
|
||||
pub fn find<F, B>(&self, f: F) -> Option<B>
|
||||
where
|
||||
F: Fn(&A) -> Option<B>,
|
||||
{
|
||||
self.iter().find_map(f)
|
||||
}
|
||||
|
||||
/// Find an atrifact in the given `History` using the artifacts ID.
|
||||
///
|
||||
/// This operation may fail if the artifact does not exist in the history.
|
||||
pub fn find_in_history<Identifier, F>(&self, identifier: &Identifier, id_of: F) -> Option<A>
|
||||
where
|
||||
A: Clone,
|
||||
F: Fn(&A) -> Identifier,
|
||||
Identifier: PartialEq,
|
||||
{
|
||||
self.iter()
|
||||
.find(|artifact| {
|
||||
let current_id = id_of(artifact);
|
||||
*identifier == current_id
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Find all occurences of an artifact in a bag of `History`s.
|
||||
pub fn find_in_histories<Identifier, F>(
|
||||
histories: Vec<Self>,
|
||||
identifier: &Identifier,
|
||||
id_of: F,
|
||||
) -> Vec<Self>
|
||||
where
|
||||
A: Clone,
|
||||
F: Fn(&A) -> Identifier + Copy,
|
||||
Identifier: PartialEq,
|
||||
{
|
||||
histories
|
||||
.into_iter()
|
||||
.filter(|history| history.find_in_history(identifier, id_of).is_some())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
// Re-export git2 as sub-module
|
||||
pub use git2::{self, Error as Git2Error, Time};
|
||||
pub use radicle_git_ext::Oid;
|
||||
@@ -72,9 +74,13 @@ mod reference;
|
||||
pub use reference::{ParseError, Ref, Rev};
|
||||
|
||||
mod repo;
|
||||
pub use repo::{History, Repository, RepositoryRef};
|
||||
pub use repo::{Repository, RepositoryRef};
|
||||
|
||||
mod history;
|
||||
pub use history::History;
|
||||
|
||||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
pub mod ext;
|
||||
|
||||
@@ -138,3 +144,54 @@ where
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Supports various ways to specify a revision used in Git.
|
||||
pub trait Revision {
|
||||
/// Returns the object id of this revision in `repo`.
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error>;
|
||||
}
|
||||
|
||||
impl Revision for Oid {
|
||||
fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
Ok(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &str {
|
||||
fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
Oid::from_str(*self).map_err(Error::Git)
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &Branch {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
let refname = repo.namespaced_refname(&self.refname())?;
|
||||
Ok(repo.repo_ref.refname_to_id(&refname).map(Oid::from)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &Tag {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
let refname = repo.namespaced_refname(&self.refname())?;
|
||||
Ok(repo.repo_ref.refname_to_id(&refname).map(Oid::from)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &Rev {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
match *self {
|
||||
Rev::Oid(oid) => Ok(*oid),
|
||||
Rev::Ref(r) => {
|
||||
let r = match repo.which_namespace()? {
|
||||
None => r.clone(),
|
||||
Some(namespace) => match r {
|
||||
Ref::Namespace { .. } => r.clone(),
|
||||
_ => r.clone().namespaced(namespace),
|
||||
},
|
||||
};
|
||||
let refname = format!("{}", r);
|
||||
Ok(repo.repo_ref.refname_to_id(&refname).map(Oid::from)?)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::vcs::git::{self, error::Error, ext, reference::Ref};
|
||||
use std::{cmp::Ordering, convert::TryFrom, fmt, str};
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Ordering, convert::TryFrom, fmt, str};
|
||||
|
||||
/// The branch type we want to filter on.
|
||||
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
|
||||
@@ -147,13 +146,13 @@ impl Branch {
|
||||
}
|
||||
|
||||
/// Get the name of the `Branch`.
|
||||
pub fn name(&self) -> String {
|
||||
pub fn refname(&self) -> String {
|
||||
let branch_name = self.name.0.clone();
|
||||
match self.locality {
|
||||
BranchType::Local => branch_name,
|
||||
BranchType::Local => format!("refs/heads/{}", branch_name),
|
||||
BranchType::Remote { ref name } => match name {
|
||||
None => branch_name,
|
||||
Some(remote_name) => format!("{}/{}", remote_name, branch_name),
|
||||
Some(remote_name) => format!("refs/remotes/{}/{}", remote_name, branch_name),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::vcs::git::error::Error;
|
||||
use crate::{
|
||||
file_system::{self, directory},
|
||||
vcs::git::{error::Error, Branch, RepositoryRef, Rev, Tag},
|
||||
};
|
||||
use radicle_git_ext::Oid;
|
||||
use std::{convert::TryFrom, str};
|
||||
|
||||
@@ -116,6 +119,16 @@ impl Commit {
|
||||
.unwrap_or(&self.message)
|
||||
.trim()
|
||||
}
|
||||
|
||||
/// Retrieves the file with `path` in this commit.
|
||||
pub fn get_file(
|
||||
&self,
|
||||
repo: &RepositoryRef,
|
||||
path: file_system::Path,
|
||||
) -> Result<directory::File, Error> {
|
||||
let git2_commit = repo.get_git2_commit(self.id)?;
|
||||
repo.get_commit_file(&git2_commit, path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
@@ -166,3 +179,45 @@ impl<'repo> TryFrom<git2::Commit<'repo>> for Commit {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A common trait for anything that can convert to a `Commit`.
|
||||
pub trait ToCommit {
|
||||
/// Converts to a commit in `repo`.
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error>;
|
||||
}
|
||||
|
||||
impl ToCommit for Commit {
|
||||
fn to_commit(self, _repo: &RepositoryRef) -> Result<Commit, Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCommit for &str {
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCommit for Oid {
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCommit for &Branch {
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCommit for &Tag {
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCommit for &Rev {
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// This file is part of radicle-surf
|
||||
// <https://github.com/radicle-dev/radicle-git>
|
||||
//
|
||||
// Copyright (C) 2022 The Radicle Team <dev@radicle.xyz>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 3 or
|
||||
// later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{
|
||||
file_system,
|
||||
vcs::git::{commit::ToCommit, Commit, Error, RepositoryRef},
|
||||
};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
/// An iterator that produces the history of commits for a given `head`,
|
||||
/// in the `repo`.
|
||||
pub struct History<'a> {
|
||||
repo: RepositoryRef<'a>,
|
||||
head: Commit,
|
||||
revwalk: git2::Revwalk<'a>,
|
||||
filter_by: Option<FilterBy>,
|
||||
}
|
||||
|
||||
/// Internal implementation, subject to refactoring.
|
||||
enum FilterBy {
|
||||
File { path: file_system::Path },
|
||||
}
|
||||
|
||||
impl<'a> History<'a> {
|
||||
/// Creates a new history starting from `head`, in `repo`.
|
||||
pub fn new<C: ToCommit>(repo: RepositoryRef<'a>, head: C) -> Result<Self, Error> {
|
||||
let head = head.to_commit(&repo)?;
|
||||
let mut revwalk = repo.repo_ref.revwalk()?;
|
||||
revwalk.push(head.id.into())?;
|
||||
let history = Self {
|
||||
repo,
|
||||
head,
|
||||
revwalk,
|
||||
filter_by: None,
|
||||
};
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
/// Returns the first commit (i.e. the head) in the history.
|
||||
pub fn head(&self) -> &Commit {
|
||||
&self.head
|
||||
}
|
||||
|
||||
/// Returns a modified `History` filtered by `path`.
|
||||
///
|
||||
/// Note that it is possible that a filtered History becomes empty,
|
||||
/// even though calling `.head()` still returns the original head.
|
||||
pub fn by_path(mut self, path: file_system::Path) -> Self {
|
||||
self.filter_by = Some(FilterBy::File { path });
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for History<'a> {
|
||||
type Item = Result<Commit, Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// Loop through the commits with the optional filtering.
|
||||
while let Some(oid) = self.revwalk.next() {
|
||||
let found = oid
|
||||
.map_err(Error::Git)
|
||||
.and_then(|oid| {
|
||||
let git2_commit = self.repo.repo_ref.find_commit(oid)?;
|
||||
|
||||
// Handles the optional filter_by.
|
||||
if let Some(FilterBy::File { path }) = &self.filter_by {
|
||||
let path_opt = self.repo.diff_commit_and_parents(path, &git2_commit)?;
|
||||
if path_opt.is_none() {
|
||||
return Ok(None); // Filter out this commit.
|
||||
}
|
||||
}
|
||||
|
||||
let commit = Commit::try_from(git2_commit)?;
|
||||
Ok(Some(commit))
|
||||
})
|
||||
.transpose();
|
||||
if found.is_some() {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Debug for History<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "History of {}", self.head.id)
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,18 @@ pub struct Namespace {
|
||||
pub(super) values: NonEmpty<String>,
|
||||
}
|
||||
|
||||
impl Namespace {
|
||||
/// Appends a `refname` to this namespace, and returns
|
||||
/// the full reference path.
|
||||
pub fn append_refname(&self, refname: &str) -> String {
|
||||
let mut prefix = String::new();
|
||||
for value in self.values.iter() {
|
||||
prefix = format!("{}refs/namespaces/{}/", &prefix, value);
|
||||
}
|
||||
format!("{}{}", &prefix, refname)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Namespace {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let values: Vec<_> = self.values.clone().into();
|
||||
|
||||
@@ -19,7 +19,7 @@ use std::{fmt, str};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::vcs::git::{repo::RepositoryRef, BranchName, Namespace, TagName};
|
||||
use crate::vcs::git::{BranchName, Namespace, TagName};
|
||||
use radicle_git_ext::Oid;
|
||||
pub(super) mod glob;
|
||||
|
||||
@@ -93,15 +93,6 @@ impl Ref {
|
||||
|
||||
ref_namespace
|
||||
}
|
||||
|
||||
/// We try to find a [`git2::Reference`] based off of a `Ref` by turning the
|
||||
/// ref into a fully qualified ref (e.g. refs/remotes/**/master).
|
||||
pub fn find_ref<'a>(
|
||||
&self,
|
||||
repo: &RepositoryRef<'a>,
|
||||
) -> Result<git2::Reference<'a>, git2::Error> {
|
||||
repo.repo_ref.find_reference(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ref {
|
||||
|
||||
@@ -19,15 +19,16 @@ use crate::{
|
||||
diff::*,
|
||||
file_system,
|
||||
file_system::{directory, DirectoryContents, Label},
|
||||
vcs,
|
||||
vcs::git::{
|
||||
error::*,
|
||||
reference::{glob::RefGlob, Ref, Rev},
|
||||
reference::{glob::RefGlob, Rev},
|
||||
Branch,
|
||||
BranchName,
|
||||
Commit,
|
||||
History,
|
||||
Namespace,
|
||||
RefScope,
|
||||
Revision,
|
||||
Signature,
|
||||
Stats,
|
||||
Tag,
|
||||
@@ -35,23 +36,15 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use directory::Directory;
|
||||
use nonempty::NonEmpty;
|
||||
use radicle_git_ext::Oid;
|
||||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
convert::TryFrom,
|
||||
path::PathBuf,
|
||||
str,
|
||||
};
|
||||
|
||||
/// This is for flagging to the `file_history` function that it should
|
||||
/// stop at the first (i.e. Last) commit it finds for a file.
|
||||
pub(super) enum CommitHistory {
|
||||
_Full,
|
||||
Last,
|
||||
}
|
||||
|
||||
/// A `History` that uses `git2::Commit` as the underlying artifact.
|
||||
pub type History = vcs::History<Commit>;
|
||||
use super::commit::ToCommit;
|
||||
|
||||
/// Wrapper around the `git2`'s `git2::Repository` type.
|
||||
/// This is to to limit the functionality that we can do
|
||||
@@ -66,6 +59,7 @@ pub struct Repository(pub(super) git2::Repository);
|
||||
///
|
||||
/// Use the `From<&'a git2::Repository>` implementation to construct a
|
||||
/// `RepositoryRef`.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RepositoryRef<'a> {
|
||||
pub(super) repo_ref: &'a git2::Repository,
|
||||
}
|
||||
@@ -142,69 +136,72 @@ impl<'a> RepositoryRef<'a> {
|
||||
Ok(namespaces?.into_iter().collect())
|
||||
}
|
||||
|
||||
pub(super) fn ref_history<R>(&self, reference: R) -> Result<History, Error>
|
||||
where
|
||||
R: Into<Ref>,
|
||||
{
|
||||
let reference = match self.which_namespace()? {
|
||||
None => reference.into(),
|
||||
Some(namespace) => reference.into().namespaced(namespace),
|
||||
}
|
||||
.find_ref(self)?;
|
||||
self.to_history(&reference)
|
||||
}
|
||||
|
||||
/// Get the [`Diff`] between two commits.
|
||||
pub fn diff(&self, from: &Rev, to: &Rev) -> Result<Diff, Error> {
|
||||
let from_commit = self.rev_to_commit(from)?;
|
||||
let to_commit = self.rev_to_commit(to)?;
|
||||
pub fn diff(&self, from: impl Revision, to: impl Revision) -> Result<Diff, Error> {
|
||||
let from_commit = self.get_git2_commit(from.object_id(self)?)?;
|
||||
let to_commit = self.get_git2_commit(to.object_id(self)?)?;
|
||||
self.diff_commits(None, Some(&from_commit), &to_commit)
|
||||
.and_then(|diff| Diff::try_from(diff).map_err(Error::from))
|
||||
}
|
||||
|
||||
/// Get the [`Diff`] of a commit with no parents.
|
||||
pub fn initial_diff(&self, rev: &Rev) -> Result<Diff, Error> {
|
||||
let commit = self.rev_to_commit(rev)?;
|
||||
pub fn initial_diff<R: Revision>(&self, rev: R) -> Result<Diff, Error> {
|
||||
let commit = self.get_git2_commit(rev.object_id(self)?)?;
|
||||
self.diff_commits(None, None, &commit)
|
||||
.and_then(|diff| Diff::try_from(diff).map_err(Error::from))
|
||||
}
|
||||
|
||||
/// Get the diff introduced by a particlar rev.
|
||||
pub fn diff_from_parent<C: ToCommit>(&self, commit: C) -> Result<Diff, Error> {
|
||||
let commit = commit.to_commit(self)?;
|
||||
match commit.parents.first() {
|
||||
Some(parent) => self.diff(*parent, commit.id),
|
||||
None => self.initial_diff(commit.id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an [`Oid`] from the given string.
|
||||
pub fn oid(&self, oid: &str) -> Result<Oid, Error> {
|
||||
Ok(self.repo_ref.revparse_single(oid)?.id().into())
|
||||
}
|
||||
|
||||
/// Gets a snapshot of the repo as a Directory.
|
||||
pub fn snapshot(&self, rev: &Rev) -> Result<Directory, Error> {
|
||||
let commit = self.rev_to_commit(rev)?;
|
||||
self.directory_of_commit(&commit)
|
||||
pub fn snapshot<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
|
||||
let commit = commit.to_commit(self)?;
|
||||
let git2_commit = self.repo_ref.find_commit((commit.id).into())?;
|
||||
self.directory_of_commit(&git2_commit)
|
||||
}
|
||||
|
||||
/// Returns the last commit, if exists, for a `path` in the history of
|
||||
/// `rev`.
|
||||
pub fn last_commit(&self, path: file_system::Path, rev: &Rev) -> Result<Option<Commit>, Error> {
|
||||
let git2_commit = self.rev_to_commit(rev)?;
|
||||
let commit = Commit::try_from(git2_commit)?;
|
||||
let file_history = self.file_history(&path, CommitHistory::Last, commit)?;
|
||||
Ok(file_history.first().cloned())
|
||||
let history = self.history(rev)?;
|
||||
history.by_path(path).next().transpose()
|
||||
}
|
||||
|
||||
/// Retrieves `Commit` identified by `oid`.
|
||||
pub fn get_commit(&self, oid: Oid) -> Result<Commit, Error> {
|
||||
let git2_commit = self.get_git2_commit(oid)?;
|
||||
Commit::try_from(git2_commit)
|
||||
/// Returns a commit for `rev` if exists.
|
||||
pub fn commit<R: Revision>(&self, rev: R) -> Result<Commit, Error> {
|
||||
let oid = rev.object_id(self)?;
|
||||
match self.repo_ref.find_commit(oid.into()) {
|
||||
Ok(commit) => Commit::try_from(commit),
|
||||
Err(e) => Err(Error::Git(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets stats of `rev`.
|
||||
pub fn get_stats(&self, rev: &Rev) -> Result<Stats, Error> {
|
||||
/// Gets stats of `commit`.
|
||||
pub fn get_commit_stats<C: ToCommit>(&self, commit: C) -> Result<Stats, Error> {
|
||||
let branches = self.list_branches(RefScope::Local)?.len();
|
||||
let history = self.history(rev.clone())?;
|
||||
let commits = history.len();
|
||||
let history = self.history(commit)?;
|
||||
let mut commits = 0;
|
||||
|
||||
let contributors = history
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|commit| (commit.author.name, commit.author.email))
|
||||
.filter_map(|commit| match commit {
|
||||
Ok(commit) => {
|
||||
commits += 1;
|
||||
Some((commit.author.name, commit.author.email))
|
||||
},
|
||||
Err(_) => None,
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
Ok(Stats {
|
||||
@@ -247,62 +244,23 @@ impl<'a> RepositoryRef<'a> {
|
||||
Ok(head_commit.id().into())
|
||||
}
|
||||
|
||||
/// Returns the Oid of `rev`.
|
||||
pub fn rev_oid(&self, rev: &Rev) -> Result<Oid, Error> {
|
||||
let commit = self.rev_to_commit(rev)?;
|
||||
Ok(commit.id().into())
|
||||
}
|
||||
|
||||
pub(super) fn rev_to_commit(&self, rev: &Rev) -> Result<git2::Commit, Error> {
|
||||
match rev {
|
||||
Rev::Oid(oid) => Ok(self.repo_ref.find_commit((*oid).into())?),
|
||||
Rev::Ref(reference) => Ok(reference.find_ref(self)?.peel_to_commit()?),
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch to a `namespace`
|
||||
pub fn switch_namespace(&self, namespace: &str) -> Result<(), Error> {
|
||||
Ok(self.repo_ref.set_namespace(namespace)?)
|
||||
}
|
||||
|
||||
/// Returns a full reference name with namespace(s) included.
|
||||
pub(crate) fn namespaced_refname(&self, refname: &str) -> Result<String, Error> {
|
||||
let fullname = match self.which_namespace()? {
|
||||
Some(namespace) => namespace.append_refname(refname),
|
||||
None => refname.to_string(),
|
||||
};
|
||||
Ok(fullname)
|
||||
}
|
||||
|
||||
/// Get a particular `git2::Commit` of `oid`.
|
||||
pub fn get_git2_commit(&self, oid: Oid) -> Result<git2::Commit<'a>, Error> {
|
||||
let commit = self.repo_ref.find_commit(oid.into())?;
|
||||
Ok(commit)
|
||||
}
|
||||
|
||||
/// Turn a [`git2::Reference`] into a [`History`] by completing
|
||||
/// a revwalk over the first commit in the reference.
|
||||
pub(super) fn to_history(&self, history: &git2::Reference<'a>) -> Result<History, Error> {
|
||||
let head = history.peel_to_commit()?;
|
||||
self.commit_to_history(head)
|
||||
}
|
||||
|
||||
/// Turn a [`git2::Reference`] into a [`History`] by completing
|
||||
/// a revwalk over the first commit in the reference.
|
||||
pub(super) fn commit_to_history(&self, head: git2::Commit) -> Result<History, Error> {
|
||||
let head_id = head.id();
|
||||
let mut commits = NonEmpty::new(Commit::try_from(head)?);
|
||||
let mut revwalk = self.repo_ref.revwalk()?;
|
||||
|
||||
// Set the revwalk to the head commit
|
||||
revwalk.push(head_id)?;
|
||||
|
||||
for commit_result_id in revwalk {
|
||||
// The revwalk iter returns results so
|
||||
// we unpack these and push them to the history
|
||||
let commit_id = commit_result_id?;
|
||||
|
||||
// Skip the head commit since we have processed it
|
||||
if commit_id == head_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let commit = Commit::try_from(self.repo_ref.find_commit(commit_id)?)?;
|
||||
commits.push(commit);
|
||||
}
|
||||
|
||||
Ok(vcs::History(commits))
|
||||
pub(crate) fn get_git2_commit(&self, oid: Oid) -> Result<git2::Commit, Error> {
|
||||
self.repo_ref.find_commit(oid.into()).map_err(Error::Git)
|
||||
}
|
||||
|
||||
/// Extract the signature from a commit
|
||||
@@ -363,37 +321,19 @@ impl<'a> RepositoryRef<'a> {
|
||||
Ok(other == git2_oid || is_descendant)
|
||||
}
|
||||
|
||||
/// Get the history of the file system where the head of the [`NonEmpty`] is
|
||||
/// the latest commit.
|
||||
pub(super) fn file_history(
|
||||
pub(crate) fn get_commit_file(
|
||||
&self,
|
||||
path: &file_system::Path,
|
||||
commit_history: CommitHistory,
|
||||
commit: Commit,
|
||||
) -> Result<Vec<Commit>, Error> {
|
||||
let mut revwalk = self.repo_ref.revwalk()?;
|
||||
let mut commits = vec![];
|
||||
|
||||
// Set the revwalk to the head commit
|
||||
revwalk.push(commit.id.into())?;
|
||||
|
||||
for commit in revwalk {
|
||||
let parent_id = commit?;
|
||||
let parent = self.repo_ref.find_commit(parent_id)?;
|
||||
let paths = self.diff_commit_and_parents(path, &parent)?;
|
||||
if let Some(_path) = paths {
|
||||
commits.push(Commit::try_from(parent)?);
|
||||
match &commit_history {
|
||||
CommitHistory::Last => break,
|
||||
CommitHistory::_Full => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(commits)
|
||||
git2_commit: &git2::Commit,
|
||||
path: file_system::Path,
|
||||
) -> Result<directory::File, Error> {
|
||||
let git2_tree = git2_commit.tree()?;
|
||||
let entry = git2_tree.get_path(PathBuf::from(&path).as_ref())?;
|
||||
let object = entry.to_object(self.repo_ref)?;
|
||||
let blob = object.as_blob().ok_or(Error::PathNotFound(path))?;
|
||||
Ok(directory::File::new(blob.content()))
|
||||
}
|
||||
|
||||
fn diff_commit_and_parents(
|
||||
pub(crate) fn diff_commit_and_parents(
|
||||
&self,
|
||||
path: &file_system::Path,
|
||||
commit: &git2::Commit,
|
||||
@@ -530,15 +470,9 @@ impl<'a> RepositoryRef<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the history of `rev`.
|
||||
pub fn history(&self, rev: Rev) -> Result<History, Error> {
|
||||
match rev {
|
||||
Rev::Ref(reference) => self.ref_history(reference),
|
||||
Rev::Oid(oid) => {
|
||||
let commit = self.get_git2_commit(oid)?;
|
||||
self.commit_to_history(commit)
|
||||
},
|
||||
}
|
||||
/// Returns the history with the `head` commit.
|
||||
pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error> {
|
||||
History::new(*self, head)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +496,7 @@ impl Repository {
|
||||
|
||||
/// Since our operations are read-only when it comes to surfing a repository
|
||||
/// we have a separate struct called [`RepositoryRef`]. This turns an owned
|
||||
/// [`Repository`], the one returend by [`Repository::new`], into a
|
||||
/// [`Repository`], the one returned by [`Repository::new`], into a
|
||||
/// [`RepositoryRef`].
|
||||
pub fn as_ref(&'_ self) -> RepositoryRef<'_> {
|
||||
RepositoryRef { repo_ref: &self.0 }
|
||||
|
||||
@@ -106,6 +106,11 @@ impl Tag {
|
||||
Self::Annotated { name, .. } => name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the full ref name of the tag.
|
||||
pub fn refname(&self) -> String {
|
||||
format!("refs/tags/{}", self.name().name())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'repo> TryFrom<git2::Tag<'repo>> for Tag {
|
||||
|
||||
+88
-69
@@ -18,9 +18,7 @@ const GIT_PLATINUM: &str = "../data/git-platinum";
|
||||
// An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54
|
||||
fn test_submodule_failure() {
|
||||
let repo = Repository::new("../..").unwrap();
|
||||
repo.as_ref()
|
||||
.snapshot(&Branch::local("main").into())
|
||||
.unwrap();
|
||||
repo.as_ref().snapshot(&Branch::local("main")).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -32,11 +30,11 @@ mod namespace {
|
||||
fn switch_to_banana() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let history_master = repo.history(Branch::local("master").into())?;
|
||||
let history_master = repo.history(&Branch::local("master"))?;
|
||||
repo.switch_namespace("golden")?;
|
||||
let history_banana = repo.history(Branch::local("banana").into())?;
|
||||
let history_banana = repo.history(&Branch::local("banana"))?;
|
||||
|
||||
assert_ne!(history_master, history_banana);
|
||||
assert_ne!(history_master.head(), history_banana.head());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -45,15 +43,15 @@ mod namespace {
|
||||
fn me_namespace() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(Branch::local("master").into())?;
|
||||
let history = repo.history(&Branch::local("master"))?;
|
||||
|
||||
assert_eq!(repo.which_namespace(), Ok(None));
|
||||
|
||||
repo.switch_namespace("me")?;
|
||||
assert_eq!(repo.which_namespace(), Ok(Some(Namespace::try_from("me")?)));
|
||||
|
||||
let history_feature = repo.history(Branch::local("feature/#1194").into())?;
|
||||
assert_eq!(history, history_feature);
|
||||
let history_feature = repo.history(&Branch::local("feature/#1194"))?;
|
||||
assert_eq!(history.head(), history_feature.head());
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local("feature/#1194")];
|
||||
let mut branches = repo.list_branches(RefScope::Local)?;
|
||||
@@ -76,7 +74,7 @@ mod namespace {
|
||||
fn golden_namespace() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(Branch::local("master").into())?;
|
||||
let history = repo.history(&Branch::local("master"))?;
|
||||
|
||||
assert_eq!(repo.which_namespace(), Ok(None));
|
||||
|
||||
@@ -87,8 +85,8 @@ mod namespace {
|
||||
Ok(Some(Namespace::try_from("golden")?))
|
||||
);
|
||||
|
||||
let golden_history = repo.history(Branch::local("master").into())?;
|
||||
assert_eq!(history, golden_history);
|
||||
let golden_history = repo.history(&Branch::local("master"))?;
|
||||
assert_eq!(history.head(), golden_history.head());
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local("banana"), Branch::local("master")];
|
||||
let mut branches = repo.list_branches(RefScope::Local)?;
|
||||
@@ -115,7 +113,7 @@ mod namespace {
|
||||
fn silver_namespace() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(Branch::local("master").into())?;
|
||||
let history = repo.history(&Branch::local("master"))?;
|
||||
|
||||
assert_eq!(repo.which_namespace(), Ok(None));
|
||||
|
||||
@@ -124,8 +122,8 @@ mod namespace {
|
||||
repo.which_namespace(),
|
||||
Ok(Some(Namespace::try_from("golden/silver")?))
|
||||
);
|
||||
let silver_history = repo.history(Branch::local("master").into())?;
|
||||
assert_ne!(history, silver_history);
|
||||
let silver_history = repo.history(&Branch::local("master"))?;
|
||||
assert_ne!(history.head(), silver_history.head());
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local("master")];
|
||||
let mut branches = repo.list_branches(RefScope::All)?;
|
||||
@@ -157,17 +155,11 @@ mod rev {
|
||||
fn _master() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(Branch::remote("master", "origin").into())?;
|
||||
let mut history = repo.history(&Branch::remote("master", "origin"))?;
|
||||
|
||||
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
assert!(
|
||||
history
|
||||
.find(|commit| if commit.id == commit1 {
|
||||
Some(commit.clone())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.is_some(),
|
||||
history.any(|commit| commit.unwrap().id == commit1),
|
||||
"commit_id={}, history =\n{:#?}",
|
||||
commit1,
|
||||
&history
|
||||
@@ -175,13 +167,7 @@ mod rev {
|
||||
|
||||
let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
|
||||
assert!(
|
||||
history
|
||||
.find(|commit| if commit.id == commit2 {
|
||||
Some(commit.clone())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.is_some(),
|
||||
history.any(|commit| commit.unwrap().id == commit2),
|
||||
"commit_id={}, history =\n{:#?}",
|
||||
commit2,
|
||||
&history
|
||||
@@ -193,17 +179,12 @@ mod rev {
|
||||
#[test]
|
||||
fn commit() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
|
||||
let history = repo.as_ref().history(rev)?;
|
||||
let mut history = repo.history(&rev)?;
|
||||
|
||||
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
assert!(history
|
||||
.find(|commit| if commit.id == commit1 {
|
||||
Some(commit.clone())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.is_some());
|
||||
assert!(history.any(|commit| commit.unwrap().id == commit1));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -211,9 +192,10 @@ mod rev {
|
||||
#[test]
|
||||
fn commit_parents() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
|
||||
let history = repo.as_ref().history(rev)?;
|
||||
let commit = history.first();
|
||||
let history = repo.history(&rev)?;
|
||||
let commit = history.head();
|
||||
|
||||
assert_eq!(
|
||||
commit.parents,
|
||||
@@ -226,17 +208,12 @@ mod rev {
|
||||
#[test]
|
||||
fn commit_short() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let rev: Rev = repo.as_ref().oid("3873745c8")?.into();
|
||||
let history = repo.as_ref().history(rev)?;
|
||||
let repo = repo.as_ref();
|
||||
let rev: Rev = repo.oid("3873745c8")?.into();
|
||||
let mut history = repo.history(&rev)?;
|
||||
|
||||
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
assert!(history
|
||||
.find(|commit| if commit.id == commit1 {
|
||||
Some(commit.clone())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.is_some());
|
||||
assert!(history.any(|commit| commit.unwrap().id == commit1));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -244,11 +221,12 @@ mod rev {
|
||||
#[test]
|
||||
fn tag() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let rev: Rev = TagName::new("v0.2.0").into();
|
||||
let history = repo.as_ref().history(rev)?;
|
||||
let history = repo.history(&rev)?;
|
||||
|
||||
let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
|
||||
assert_eq!(history.first().id, commit1);
|
||||
assert_eq!(history.head().id, commit1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -373,12 +351,23 @@ mod last_commit {
|
||||
|
||||
let expected_oid = repo
|
||||
.as_ref()
|
||||
.history(Branch::local("master").into())
|
||||
.history(&Branch::local("master"))
|
||||
.unwrap()
|
||||
.first()
|
||||
.head()
|
||||
.id;
|
||||
assert_eq!(root_last_commit_id, Some(expected_oid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_file() {
|
||||
let repo = Repository::new(GIT_PLATINUM)
|
||||
.expect("Could not retrieve ./data/git-platinum as git repository");
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(&Branch::local("dev")).unwrap();
|
||||
let file_commit = history.by_path(unsound::path::new("~/bin/cat")).next();
|
||||
assert!(file_commit.is_some());
|
||||
println!("file commit: {:?}", &file_commit);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -389,15 +378,13 @@ mod diff {
|
||||
|
||||
#[test]
|
||||
fn test_initial_diff() -> Result<(), Error> {
|
||||
let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let commit = repo.get_git2_commit(oid).unwrap();
|
||||
let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
|
||||
let commit = repo.commit(oid).unwrap();
|
||||
assert!(commit.parents.is_empty());
|
||||
|
||||
assert!(commit.parents().count() == 0);
|
||||
assert!(commit.parent(0).is_err());
|
||||
|
||||
let diff = repo.initial_diff(&oid.into())?;
|
||||
let diff = repo.initial_diff(oid)?;
|
||||
|
||||
let expected_diff = Diff {
|
||||
created: vec![CreateFile {
|
||||
@@ -424,15 +411,26 @@ mod diff {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff_of_rev() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let diff = repo.diff_from_parent("80bacafba303bf0cdf6142921f430ff265f25095")?;
|
||||
assert_eq!(diff.created.len(), 0);
|
||||
assert_eq!(diff.deleted.len(), 0);
|
||||
assert_eq!(diff.moved.len(), 0);
|
||||
assert_eq!(diff.modified.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let oid = Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?;
|
||||
let commit = repo.get_git2_commit(oid).unwrap();
|
||||
let parent = commit.parent(0)?;
|
||||
let parent_oid: Oid = parent.id().into();
|
||||
let diff = repo.diff(&parent_oid.into(), &oid.into())?;
|
||||
let oid = "80bacafba303bf0cdf6142921f430ff265f25095";
|
||||
let commit = repo.commit(oid).unwrap();
|
||||
let parent_oid = commit.parents.get(0).unwrap();
|
||||
let diff = repo.diff(*parent_oid, oid)?;
|
||||
|
||||
let expected_diff = Diff {
|
||||
created: vec![],
|
||||
@@ -463,10 +461,7 @@ mod diff {
|
||||
fn test_branch_diff() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let diff = repo.diff(
|
||||
&Branch::local("master").into(),
|
||||
&Branch::local("dev").into(),
|
||||
)?;
|
||||
let diff = repo.diff(&Branch::local("master"), &Branch::local("dev"))?;
|
||||
|
||||
println!("Diff two branches: master -> dev");
|
||||
println!(
|
||||
@@ -824,7 +819,7 @@ mod code_browsing {
|
||||
fn iterate_root_dir_recursive() {
|
||||
let repo = Repository::new(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let root_dir = repo.snapshot(&Branch::local("master").into()).unwrap();
|
||||
let root_dir = repo.snapshot(&Branch::local("master")).unwrap();
|
||||
let count = println_dir(&root_dir, 0);
|
||||
assert_eq!(count, 36); // Check total file count.
|
||||
|
||||
@@ -844,4 +839,28 @@ mod code_browsing {
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag_snapshot() {
|
||||
let repo = Repository::new(GIT_PLATINUM).unwrap();
|
||||
let repo_ref = repo.as_ref();
|
||||
let tags = repo_ref.list_tags(RefScope::Local).unwrap();
|
||||
assert_eq!(tags.len(), 6);
|
||||
let root_dir = repo_ref.snapshot(&tags[0]).unwrap();
|
||||
assert_eq!(root_dir.contents().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_history() {
|
||||
let repo = Repository::new(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(&Branch::local("dev")).unwrap();
|
||||
let path = unsound::path::new("README.md");
|
||||
let mut file_history = history.by_path(path);
|
||||
let commit = file_history.next().unwrap().unwrap();
|
||||
let file = commit
|
||||
.get_file(&repo, unsound::path::new("README.md"))
|
||||
.unwrap();
|
||||
assert_eq!(file.size(), 67);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user