diff --git a/apps/servers/octopus/Cargo.lock b/apps/servers/octopus/Cargo.lock index 42f0f4d1df0..24f6c0c539c 100644 --- a/apps/servers/octopus/Cargo.lock +++ b/apps/servers/octopus/Cargo.lock @@ -507,6 +507,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" +[[package]] +name = "atomptr" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a29d2348bcb8ee04660963dbe624a1bbbd1ea49ca6ab51138aecb057135e3cb" + [[package]] name = "atty" version = "0.2.14" @@ -1826,6 +1832,7 @@ name = "supergit" version = "0.1.0" dependencies = [ "async-std", + "atomptr", "git2", ] diff --git a/apps/servers/octopus/supergit/Cargo.toml b/apps/servers/octopus/supergit/Cargo.toml index 029503e87c4..6de86dd640d 100644 --- a/apps/servers/octopus/supergit/Cargo.toml +++ b/apps/servers/octopus/supergit/Cargo.toml @@ -7,4 +7,5 @@ edition = "2018" [dependencies] git2 = "0.11" -async-std = { version = "1.0", features = ["unstable"] } \ No newline at end of file +async-std = { version = "1.0", features = ["unstable"] } +atomptr = "1.0" \ No newline at end of file diff --git a/apps/servers/octopus/supergit/src/bin/test.rs b/apps/servers/octopus/supergit/src/bin/test.rs index 25652ed0bd3..c4fcc2fbb3a 100644 --- a/apps/servers/octopus/supergit/src/bin/test.rs +++ b/apps/servers/octopus/supergit/src/bin/test.rs @@ -13,31 +13,14 @@ fn main() { }; let repo = Repository::open(path.as_str()).unwrap(); - - let (tx, rx) = channel(); let branches = repo.branches().unwrap(); - - branches + let main = branches .into_iter() - .filter(|b| b.name == Some("main".to_string())) - .for_each(|b| tx.send(b.get_all()).unwrap()); - - // Iterate over all branch iterators we get - while let Some(biter) = rx.recv().ok() { - use BranchCommit::*; + .filter(|b| b.name() == Some("master".to_string())) + .nth(0) + .unwrap(); - biter.for_each(|bc| match bc { - Commit(c) => println!("{}: {}", c.id_str(), c.summary()), - Merge(c, _b) => { - println!("{}: {}", c.id_str(), c.summary()); - tx.send(_b.get_all()).unwrap(); - } - Octopus(c, branches) => { - println!("{}: {}", c.id_str(), c.summary()); - for _b in branches { - tx.send(_b.get_all()).unwrap(); - } - } - }); - } + let head = main.get_head(); + let tree = head.get_tree(); + } diff --git a/apps/servers/octopus/supergit/src/branch.rs b/apps/servers/octopus/supergit/src/branch.rs index fe555d24292..dd92aea93cb 100644 --- a/apps/servers/octopus/supergit/src/branch.rs +++ b/apps/servers/octopus/supergit/src/branch.rs @@ -8,8 +8,8 @@ use std::{mem, sync::Arc}; #[derive(Clone)] pub struct Branch { repo: Arc, - pub name: Option, - pub head: HashId, + name: Option, + head: HashId, } impl Branch { @@ -77,6 +77,16 @@ impl Branch { SegLimit::Length(0, num), ) } + + /// Get the commit pointed at by HEAD + pub fn get_head(&self) -> Commit { + Commit::new(&self.repo, self.head.clone()).unwrap() + } + + /// Get the branch name, if it exists + pub fn name(&self) -> Option { + self.name.clone() + } } /// A branch segment iterator diff --git a/apps/servers/octopus/supergit/src/commit.rs b/apps/servers/octopus/supergit/src/commit.rs index 41c827f64d8..14f2d9bafdc 100644 --- a/apps/servers/octopus/supergit/src/commit.rs +++ b/apps/servers/octopus/supergit/src/commit.rs @@ -1,4 +1,4 @@ -use crate::HashId; +use crate::{FileTree, HashId}; use git2::Repository; use std::sync::Arc; @@ -14,21 +14,24 @@ pub struct Commit { impl Commit { /// Create a commit object and check if it exists in the repo - pub fn new(r: &Arc, id: HashId) -> Option { + pub(crate) fn new(r: &Arc, id: HashId) -> Option { r.find_commit(id.to_oid()).ok().map(|_| Self { id, repo: Arc::clone(r), }) } + /// Get a utf-8 string representation of the commit ID pub fn id_str(&self) -> String { self.id.to_string() } + /// Get the summary line as a utf-7 string pub fn summary(&self) -> String { self.find().summary().unwrap().into() } + /// Get the number of parents pub fn parent_count(&self) -> usize { self.repo .find_commit(self.id.to_oid()) @@ -44,6 +47,7 @@ impl Commit { .and_then(|c| Self::new(&self.repo, c.id().into())) } + /// Get a specific parent, if it exists pub fn parent(&self, num: usize) -> Option { self.find() .parent(num) @@ -58,6 +62,11 @@ impl Commit { .collect() } + /// Get the file tree for this commit + pub fn get_tree(&self) -> Arc { + FileTree::new(&self.repo, self.id.clone()) + } + fn find(&self) -> git2::Commit { self.repo.find_commit(self.id.to_oid()).unwrap() } diff --git a/apps/servers/octopus/supergit/src/files.rs b/apps/servers/octopus/supergit/src/files.rs index 681b8877256..2a1b69a3456 100644 --- a/apps/servers/octopus/supergit/src/files.rs +++ b/apps/servers/octopus/supergit/src/files.rs @@ -1,7 +1,146 @@ +use crate::{Branch, BranchIter, Commit, HashId}; +use git2::{ObjectType, TreeWalkMode, TreeWalkResult}; +use atomptr::AtomPtr; +use std::collections::BTreeMap; +use std::{path::PathBuf, sync::Arc}; -pub type FileId = usize; +/// A tree of files +pub struct FileTree { + repo: Arc, + tree: AtomPtr>, +} + +impl FileTree { + /// Utility function to create a tree, and then parse it too + pub(crate) fn new(repo: &Arc, commit: HashId) -> Arc { + Arc::new(Self { + repo: Arc::clone(repo), + tree: AtomPtr::new(BTreeMap::new()), + }) + .parse(commit) + } + + /// Parse a tree from a specific commit + pub(crate) fn parse(self: Arc, commit: HashId) -> Arc { + let mut new_tree = BTreeMap::new(); + + let tree = (&self.repo) + .find_commit(commit.to_oid()) + .unwrap() + .tree() + .unwrap(); + + tree.walk(TreeWalkMode::PreOrder, |what, entry| { + let path_segs: Vec<_> = what.split("/").filter(|s| s != &"").collect(); + let path = if path_segs.len() == 0 { + None + } else { + Some(path_segs) + }; + + println!("{:?} {}", path, entry.name().unwrap()); + TreeWalkResult::Ok + }) + .unwrap(); + drop(tree); + + // Atomicly swap new tree into place + self.tree.swap(new_tree); + + self + } +} + +/// An entry in a file tree +/// +/// It's variants can either be a file (leaf), or a subtree, with it's +/// own path handles, and children. +pub enum TreeEntry { + /// A single file + File(File), + /// A sub-tree + Dir(Directory), +} + +impl TreeEntry { + /// Create a tree entry from a path and `git2::TreeEntry` + fn generate(root: PathBuf, path_segments: Option>, entry: git2::TreeEntry) -> Self { + let path = path_segments.map_or("".into(), |p| path_segs_join(p)); + + match entry.kind() { + Some(ObjectType::Blob) => Self::File(File::new(root, path)), + Some(ObjectType::Tree) => Self::Dir(Directory::new(root, path)), + _ => unimplemented!(), + } + } +} /// A file to have ever existed in a git repo pub struct File { - id: FileId, + root: PathBuf, + path: String, +} + +impl File { + pub(crate) fn new(root: PathBuf, path: String) -> Self { + Self { root, path } + } + + /// Get the history of a file from a branch iterator + pub fn get_history(&self, branch: BranchIter) -> Vec { + todo!() + } +} + +/// A subdirectory in a file tree +/// +/// A directory has a set of children, which can either be Files, or +/// other directories. Many of the functions to retrieve metadata +/// (such as the last commit, count, etc) will be deferred to the +/// children of this directory. +pub struct Directory { + root: PathBuf, + path: String, +} + +impl Directory { + pub(crate) fn new(root: PathBuf, path: String) -> Self { + Self { root, path } + } +} + +/// Take a vector of path segments, and turn it into a valid offset path +/// +/// There are tests to make sure this function works properly. +/// Following are some example transformations. +/// +/// * vec![] -> "" +/// * vec!["foo"] -> "foo" +/// * vec!["foo", "bar", "baz"] -> "foo/bar/baz" +fn path_segs_join(segments: Vec) -> String { + segments + .into_iter() + .fold(PathBuf::new(), |buf, seg| buf.join(seg)) + .as_path() + .to_str() + .unwrap() + .to_owned() +} + +#[test] +fn empty_path() { + assert_eq!(path_segs_join(vec![]), String::from("")); +} + +#[test] +fn one_path() { + assert_eq!(path_segs_join(vec!["foo".into()]), String::from("foo")); +} + +#[test] +fn nested_path() { + assert_eq!( + path_segs_join(vec!["foo".into(), "bar".into(), "baz".into()]), + String::from("foo/bar/baz") + ); } diff --git a/apps/servers/octopus/supergit/src/lib.rs b/apps/servers/octopus/supergit/src/lib.rs index e839ab635dd..df30d996ebe 100644 --- a/apps/servers/octopus/supergit/src/lib.rs +++ b/apps/servers/octopus/supergit/src/lib.rs @@ -1,15 +1,20 @@ -//! Strongly typed git repository explorer library +//! Read-only git repository explorer library. //! -//! This library exposes a read-only view into a git repository. To -//! get started, open an existing bare repo, and then call `sync()` to -//! build a cache of it. Every time you want your view of the repo to -//! update, call `sync()` again. If you want the sync operation to be -//! blocking, call `sync_blocking()` instead. +//! This library provides a more Rustic interface for libgit2, built +//! on the `git2` bindings. If you want more low-level access to your +//! repository, consider using that library instead. //! +//! supergit aims to make queries into a git repo as typed and easy as +//! possible. Start by creating a [`Repository`](), and enumerating +//! or fetching [`Branch`]()es that you are interested in. //! +//! Unlike `libgit2`, this library can resolve reverse dependencies +//! between files, and their commit history. Some of these functions +//! are very computationally intensive, and will be marked with their +//! runtime cost. mod branch; -pub use branch::{Branch, BranchCommit}; +pub use branch::{Branch, BranchIter, BranchCommit}; mod commit; pub use commit::Commit; @@ -18,8 +23,11 @@ mod diff; pub use diff::Diff; mod repo; -pub use repo::Repository; pub(crate) use repo::HashId; +pub use repo::Repository; + +mod files; +pub use files::{File, FileTree}; use async_std::sync::{Arc, RwLock}; use std::sync::atomic::{AtomicUsize, Ordering};