diff --git a/Cargo.lock b/Cargo.lock index 39d95ca..6459e77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,15 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d663a8e9a99154b5fb793032533f6328da35e23aac63d5c152279aa8ba356825" +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + [[package]] name = "ascii" version = "0.9.3" @@ -95,6 +104,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytecount" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0017894339f586ccb943b01b9555de56770c11cda818e7e3d8bd93f4ed7f46e" + [[package]] name = "byteorder" version = "1.3.4" @@ -180,6 +195,23 @@ dependencies = [ "unreachable", ] +[[package]] +name = "comrak" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17058cc536cf290563e88787d7b9e6030ce4742943017cc2ffb71f88034021c" +dependencies = [ + "clap", + "entities", + "lazy_static", + "pest", + "pest_derive", + "regex", + "twoway", + "typed-arena", + "unicode_categories", +] + [[package]] name = "crossbeam-channel" version = "0.4.2" @@ -228,6 +260,12 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + [[package]] name = "fake-simd" version = "0.1.2" @@ -239,12 +277,15 @@ name = "fedhub" version = "0.1.0" dependencies = [ "anyhow", + "comrak", "futures", "git2", "hyper", "lazy_static", + "packer", "parking_lot", "redis", + "rsass", "serde", "serde_derive", "serde_json", @@ -382,6 +423,12 @@ dependencies = [ "url", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "globset" version = "0.4.5" @@ -579,6 +626,19 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890" +dependencies = [ + "arrayvec", + "cfg-if", + "rustc_version", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.68" @@ -726,6 +786,23 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "num-integer" version = "0.1.42" @@ -736,6 +813,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.11" @@ -761,6 +849,29 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +[[package]] +name = "packer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "546814b26ef40511a1475217e799571ce49c2154fe41bd0b61766cb5c2dfb9a9" +dependencies = [ + "lazy_static", + "packer_derive", +] + +[[package]] +name = "packer_derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb687c5bdc13808a293f1077d65907aa0c169f694bf613faa2f8ef8357d5eb3a" +dependencies = [ + "glob", + "proc-macro2", + "quote", + "syn", + "walkdir", +] + [[package]] name = "parking_lot" version = "0.10.2" @@ -889,9 +1000,9 @@ checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" [[package]] name = "proc-macro-error" -version = "0.4.12" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" dependencies = [ "proc-macro-error-attr", "proc-macro2", @@ -902,9 +1013,9 @@ dependencies = [ [[package]] name = "proc-macro-error-attr" -version = "0.4.12" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" +checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" dependencies = [ "proc-macro2", "quote", @@ -1016,6 +1127,29 @@ version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" +[[package]] +name = "rsass" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c78e9b85b8816881cad9aa1142eec337020cfd79c6983e338811f5c521a80463" +dependencies = [ + "bytecount", + "lazy_static", + "nom", + "num-rational", + "num-traits", + "rand", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.3" @@ -1037,6 +1171,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.105" @@ -1126,6 +1275,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "static_assertions" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" + [[package]] name = "strsim" version = "0.8.0" @@ -1134,9 +1289,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8faa2719539bbe9d77869bfb15d4ee769f99525e707931452c97b693b3f159d" +checksum = "ff6da2e8d107dfd7b74df5ef4d205c6aebee0706c647f6bc6a2d5789905c00fb" dependencies = [ "clap", "lazy_static", @@ -1145,9 +1300,9 @@ dependencies = [ [[package]] name = "structopt-derive" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88b8e18c69496aad6f9ddf4630dd7d585bcaf765786cb415b9aec2fe5a0430" +checksum = "a489c87c08fbaf12e386665109dd13470dcc9c4583ea3e10dd2b4523e5ebd9ac" dependencies = [ "heck", "proc-macro-error", @@ -1313,6 +1468,22 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" +[[package]] +name = "twoway" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc" +dependencies = [ + "memchr", + "unchecked-index", +] + +[[package]] +name = "typed-arena" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" + [[package]] name = "typenum" version = "1.12.0" @@ -1325,6 +1496,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + [[package]] name = "unic-char-property" version = "0.9.0" @@ -1411,6 +1588,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unreachable" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index d127f35..7ce781a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,18 +7,23 @@ edition = "2018" [workspace] [dependencies] +anyhow = "1.0.28" +comrak = "0.7.0" +futures = { version = "0.3.4", default-features = false } git2 = { version = "0.13.0", default-features = false } +hyper = "0.13.5" +lazy_static = "1.4.0" +parking_lot = "0.10.2" +redis = "0.15.1" serde = "1.0.105" serde_derive = "1.0.105" serde_json = "1.0.48" -structopt = "0.3.12" +structopt = "0.3.13" tera = "1.2.0" -toml = "0.5.6" tokio = { version = "0.2.18", features = ["full"] } -anyhow = "1.0.28" +toml = "0.5.6" walkdir = "2.3.1" -lazy_static = "1.4.0" -parking_lot = "0.10.2" -hyper = "0.13.5" -redis = "0.15.1" -futures = { version = "0.3.4", default-features = false } +packer = "0.5.3" + +[build-dependencies] +rsass = "0.13.0" diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..e69de29 diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/config.toml b/config.toml index 788df74..888772b 100644 --- a/config.toml +++ b/config.toml @@ -1,3 +1,3 @@ addr = "127.0.0.1:3000" repo_root = "./repos" -redis_url = "redis://127.0.0.1" \ No newline at end of file +redis_url = "redis://127.0.0.1" diff --git a/dev-server.old.rs b/dev-server.old.rs new file mode 100644 index 0000000..8897975 --- /dev/null +++ b/dev-server.old.rs @@ -0,0 +1,55 @@ +use std::convert::Infallible; +use std::sync::mpsc::{self}; +use std::time::Duration; +use std::thread; +use std::path::PathBuf; + +use anyhow::Result; +use hyper::{ + service::{make_service_fn, service_fn}, + Body, Response, Server, +}; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; + +#[tokio::main] +async fn main() -> Result<()> { + let (watch_tx, watch_rx) = mpsc::channel(); + let mut watcher = RecommendedWatcher::new(watch_tx, Duration::from_secs(3))?; + + let path = PathBuf::from("."); + watcher.watch(&path, RecursiveMode::Recursive)?; + + thread::spawn(move || { + loop { + match watch_rx.recv() { + Ok(evt) => { + println!("event: {:?}", evt); + } + Err(err) => eprintln!("watch error: {:?}", err), + } + } + }); + + let make_svc = make_service_fn(move |_conn| { + let fedhub = fedhub.clone(); + let main = move |req| { + let fedhub = fedhub.clone(); + future::ready(match fedhub.handle(req) { + Ok(res) => Ok::<_, Infallible>(res), + Err(err) => { + eprintln!("Error: {:?}", err); + Ok(Response::new(Body::from(format!("Error: {:?}", err)))) + } + }) + }; + + future::ok::<_, Infallible>(service_fn(main)) + }); + + let server = Server::bind(&addr).serve(make_svc); + if let Err(e) = server.await { + eprintln!("server error: {}", e); + } + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 698ec53..92d6680 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { + pub site_name: Option, pub addr: SocketAddr, pub repo_root: PathBuf, pub redis_url: String, diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9a46633 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,344 @@ +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate serde_json; + +mod config; + +use std::path::{Component as PathComponent, Path, PathBuf}; +use std::sync::Arc; + +use anyhow::Result; +use git2::{ObjectType, Oid, Repository, Tree}; +use hyper::{Body, Request, Response, StatusCode}; +use lazy_static::lazy_static; +use packer::Packer; +use parking_lot::RwLock; +use redis::{Client as RedisClient, Cmd as RedisCmd}; +use tera::{Context as TeraContext, Tera}; +use walkdir::WalkDir; + +pub use crate::config::Config; + +macro_rules! template { + ($file:expr) => { + ($file, include_str!(concat!("../templates/", $file))) + }; +} + +lazy_static! { + static ref TERA: Tera = { + let mut tera = Tera::default(); + tera.add_raw_templates(vec![ + template!("layout.html"), + template!("index.html"), + template!("repo_blob.html"), + template!("repo_index.html"), + template!("repo_tree.html"), + ]) + .unwrap(); + tera + }; +} + +#[derive(Packer)] +#[packer(source = "static", prefixed = false)] +pub struct Statics; + +#[derive(Clone)] +pub struct Fedhub { + config: Config, + state: Arc>, +} + +#[derive(Clone)] +struct State { + cache: RedisClient, +} + +impl Fedhub { + pub fn new(config: Config) -> Result { + let cache = RedisClient::open(config.redis_url.as_ref())?; + Ok(Fedhub { + config, + state: Arc::new(RwLock::new(State { cache })), + }) + } + + pub fn context(&self) -> TeraContext { + let mut ctx = TeraContext::new(); + let site_name = self + .config + .site_name + .as_ref() + .map(|s| s.as_ref()) + .unwrap_or_else(|| "Fedhub"); + ctx.insert( + "fedhub", + &json!({ + "site_name": site_name, + }), + ); + ctx + } + + pub fn get_dir_list(&self) -> Result> { + let state = self.state.read(); + let mut con = state.cache.get_connection()?; + if RedisCmd::exists("repos").query(&mut con)? { + let path_strs: Vec = RedisCmd::smembers("repos").query(&mut con)?; + return Ok(path_strs.into_iter().map(|s| PathBuf::from(s)).collect()); + } else { + let repo_root = self.config.repo_root.clone(); + let mut dir_iter = WalkDir::new(&repo_root).into_iter(); + let mut dirs = Vec::new(); + loop { + let entry = match dir_iter.next() { + None => break, + Some(Err(err)) => panic!("error: {:?}", err), + Some(Ok(entry)) => entry, + }; + let path = entry.path(); + if let Ok(_) = Repository::open(path) { + let new_path = path.strip_prefix(&repo_root).unwrap(); + dirs.push(new_path.to_path_buf()); + dir_iter.skip_current_dir(); + } + } + RedisCmd::sadd( + "repos", + dirs.iter() + .map(|path| path.to_str().unwrap().to_string()) + .collect::>(), + ) + .execute(&mut con); + Ok(dirs) + } + } + + pub fn render_index(&self) -> Result> { + let directories = self.get_dir_list()?; + let mut ctx = self.context(); + ctx.insert("repos", &directories); + Ok(Response::new(TERA.render("index.html", &ctx)?.into())) + } + + pub fn render_repo_index(&self, path: PathBuf, repo: &Repository) -> Result> { + let mut ctx = self.context(); + ctx.insert("repo_name", &path); + let mut branches = Vec::new(); + for branch in repo.branches(None)? { + let (branch, _) = branch?; + let name = branch.name()?.unwrap().to_string(); + let branch_ref = branch.into_reference(); + let url = format!( + "/{}/+/{}", + path.to_str().unwrap(), + branch_ref.name().unwrap() + ); + branches.push(json!({ + "name": name, + "url": url, + })); + } + ctx.insert("branches", &branches); + return Ok(Response::new(TERA.render("repo_index.html", &ctx)?.into())); + } + + pub fn render_repo_tree( + &self, + path: PathBuf, + repo: &Repository, + tree_id: Oid, + tree_name: String, + filepath: Option, + ) -> Result> { + let tree = repo.find_tree(tree_id)?; + let mut ctx = self.context(); + ctx.insert("repo_name", &path); + ctx.insert("tree_name", &tree_name); + ctx.insert("filepath", &filepath); + let mut entries = Vec::new(); + for entry in tree.iter() { + let url = match &filepath { + Some(filepath) => format!( + "/{}/+/{}/{}/{}", + path.to_str().unwrap(), + tree_name, + filepath.to_str().unwrap(), + entry.name().unwrap() + ), + None => format!( + "/{}/+/{}/{}", + path.to_str().unwrap(), + tree_name, + entry.name().unwrap() + ), + }; + entries.push(json!({ + "name": entry.name().unwrap(), + "url": url, + })); + } + ctx.insert("entries", &entries); + return Ok(Response::new(TERA.render("repo_tree.html", &ctx)?.into())); + } + + pub fn render_repo_blob( + &self, + path: PathBuf, + repo: &Repository, + tree_name: String, + filepath: impl AsRef, + blob_id: Oid, + ) -> Result> { + let filepath = filepath.as_ref(); + let blob = repo.find_blob(blob_id)?; + let mut ctx = self.context(); + ctx.insert("repo_name", &path); + ctx.insert("tree_name", &tree_name); + ctx.insert( + "blob", + &json!({ + "name": filepath, + "binary": blob.is_binary(), + "contents": if blob.is_binary() { + json!(null) + } else { + json!(String::from_utf8(blob.content().to_vec())?) + }, + }), + ); + return Ok(Response::new(TERA.render("repo_blob.html", &ctx)?.into())); + } + + pub fn handle(self, req: Request) -> Result> { + let repo_root = self.config.repo_root.clone(); + let uri = req.uri(); + let path = uri.path(); + + if path == "/" { + return self.render_index(); + } + + if path.starts_with("/static") { + let rest = path.trim_start_matches("/static/"); + println!("REST: {:?}", rest); + println!("LIST: {:?}", Statics::list().collect::>()); + return match Statics::get(rest) { + Some(data) => Ok(Response::builder().body(data.into())?), + None => Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("not found".into())?), + }; + } + + let repo_info = { + let mut repo_info = None; + for repo_dir in self.get_dir_list()? { + let path = PathBuf::from(path.trim_start_matches("/")); + if path.starts_with(&repo_dir) { + let repo = Repository::open(repo_root.join(&repo_dir))?; + let remainder = PathBuf::from("/").join(path.strip_prefix(&repo_dir)?); + repo_info = Some((repo_dir, repo, remainder)); + } + } + repo_info + }; + + if let Some((path, repo, remainder)) = repo_info { + if remainder == PathBuf::from("/") { + return self.render_repo_index(path, &repo); + } else if remainder.starts_with("/+") { + let mut iter = remainder.strip_prefix("/+").unwrap().components(); + let tree_name = iter.next(); + if let Some(PathComponent::Normal(tree_name)) = tree_name { + let tree_name = tree_name.to_str().unwrap().to_string(); + let tree_name = if tree_name == "refs" { + let second = iter.next().unwrap(); + let third = iter.next().unwrap(); + format!( + "{}/{}/{}", + tree_name, + second.as_os_str().to_str().unwrap(), + third.as_os_str().to_str().unwrap() + ) + } else { + tree_name + }; + let (tree_id, shortname) = { + let (tree, shortname) = get_tree_from_name(&repo, tree_name)?; + (tree.id(), shortname) + }; + let filepath = iter.collect::(); + println!("filepath: {:?}", filepath); + if filepath.iter().collect::>().len() == 0 { + return self.render_repo_tree(path, &repo, tree_id, shortname, None); + } else { + let (object_id, object_type) = + get_path_from_tree(&repo, tree_id, &filepath)?; + println!("{:?} {:?}", object_id, object_type); + match object_type { + ObjectType::Tree => { + return self.render_repo_tree( + path, + &repo, + object_id, + shortname, + Some(filepath), + ) + } + ObjectType::Blob => { + return self + .render_repo_blob(path, &repo, shortname, filepath, object_id) + } + _ => unimplemented!(), + } + } + } + } + } + + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("not found".into())?) + } +} + +fn get_tree_from_name(repo: &Repository, name: impl AsRef) -> Result<(Tree, String)> { + Ok( + if let Ok(commit_ref) = repo.resolve_reference_from_short_name(name.as_ref()) { + let tree_name = commit_ref.shorthand().unwrap().to_string(); + (commit_ref.peel_to_commit()?.tree()?, tree_name) + } else if let Ok(commit) = repo.find_commit(Oid::from_str(name.as_ref())?) { + let tree_name = name.as_ref().to_string(); + (commit.tree()?, tree_name) + } else { + anyhow::bail!("Fuck!"); + }, + ) +} + +fn get_path_from_tree<'a>( + repo: &Repository, + curr_oid: Oid, + path: impl AsRef, +) -> Result<(Oid, ObjectType)> { + let mut components = path.as_ref().components(); + let tree = repo.find_tree(curr_oid)?; + if let Some(next_file) = components.next() { + if let Some(entry) = tree.get_name(next_file.as_os_str().to_str().unwrap()) { + let kind = entry.kind().unwrap(); + match kind { + ObjectType::Tree => { + get_path_from_tree(repo, entry.id(), components.collect::()) + } + _ => Ok((entry.id(), kind)), + } + } else { + anyhow::bail!("doesn't exist bro: {:?}", next_file); + } + } else { + Ok((tree.id(), ObjectType::Tree)) + } +} diff --git a/src/main.rs b/src/main.rs index 9c2e9f9..8278aba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,329 +1,20 @@ -#![feature(async_closure)] - -#[macro_use] -extern crate serde_derive; -#[macro_use] -extern crate serde_json; - -mod config; - use std::convert::Infallible; - -use std::path::{Component as PathComponent, Path, PathBuf}; -use std::sync::Arc; +use std::fs::File; +use std::io::Read; use anyhow::Result; - -use futures::future::{self}; -use git2::{ObjectType, Oid, Repository, Tree}; +use fedhub::{Config, Fedhub}; +use futures::future; use hyper::{ service::{make_service_fn, service_fn}, - Body, Request, Response, Server, StatusCode, + Body, Response, Server, }; -use lazy_static::lazy_static; -use parking_lot::RwLock; -use redis::{Client as RedisClient, Cmd as RedisCmd}; -use tera::{Context as TeraContext, Tera}; -use tokio::{fs::File, io::AsyncReadExt}; -use walkdir::WalkDir; - -use crate::config::Config; - -macro_rules! template { - ($file:expr) => { - ($file, include_str!(concat!("../templates/", $file))) - }; -} - -lazy_static! { - static ref TERA: Tera = { - let mut tera = Tera::default(); - tera.add_raw_templates(vec![ - template!("index.html"), - template!("repo_blob.html"), - template!("repo_index.html"), - template!("repo_tree.html"), - ]) - .unwrap(); - tera - }; -} - -#[derive(Clone)] -struct Fedhub { - config: Config, - state: Arc>, -} - -#[derive(Clone)] -struct State { - cache: RedisClient, -} - -impl Fedhub { - pub fn new(config: Config) -> Result { - let cache = RedisClient::open(config.redis_url.as_ref())?; - Ok(Fedhub { - config, - state: Arc::new(RwLock::new(State { cache })), - }) - } - - pub fn get_dir_list(&self) -> Result> { - let state = self.state.read(); - let mut con = state.cache.get_connection()?; - if RedisCmd::exists("repos").query(&mut con)? { - let path_strs: Vec = RedisCmd::smembers("repos").query(&mut con)?; - return Ok(path_strs.into_iter().map(|s| PathBuf::from(s)).collect()); - } else { - let repo_root = self.config.repo_root.clone(); - let mut dir_iter = WalkDir::new(&repo_root).into_iter(); - let mut dirs = Vec::new(); - loop { - let entry = match dir_iter.next() { - None => break, - Some(Err(err)) => panic!("error: {:?}", err), - Some(Ok(entry)) => entry, - }; - let path = entry.path(); - if let Ok(_) = Repository::open(path) { - let new_path = path.strip_prefix(&repo_root).unwrap(); - dirs.push(new_path.to_path_buf()); - dir_iter.skip_current_dir(); - } - } - RedisCmd::sadd( - "repos", - dirs.iter() - .map(|path| path.to_str().unwrap().to_string()) - .collect::>(), - ) - .execute(&mut con); - Ok(dirs) - } - } - - pub fn render_index(&self) -> Result> { - let directories = self.get_dir_list()?; - let mut ctx = TeraContext::new(); - ctx.insert("repos", &directories); - Ok(Response::new(TERA.render("index.html", &ctx)?.into())) - } - - pub fn render_repo_index(&self, path: PathBuf, repo: &Repository) -> Result> { - let mut ctx = TeraContext::new(); - ctx.insert("repo_name", &path); - let mut branches = Vec::new(); - for branch in repo.branches(None)? { - let (branch, _) = branch?; - let name = branch.name()?.unwrap().to_string(); - let branch_ref = branch.into_reference(); - let url = format!( - "/{}/+/{}", - path.to_str().unwrap(), - branch_ref.name().unwrap() - ); - branches.push(json!({ - "name": name, - "url": url, - })); - } - ctx.insert("branches", &branches); - return Ok(Response::new(TERA.render("repo_index.html", &ctx)?.into())); - } - - pub fn render_repo_tree( - &self, - path: PathBuf, - repo: &Repository, - tree_id: Oid, - tree_name: String, - filepath: Option, - ) -> Result> { - let tree = repo.find_tree(tree_id)?; - let mut ctx = TeraContext::new(); - ctx.insert("repo_name", &path); - ctx.insert("tree_name", &tree_name); - ctx.insert("filepath", &filepath); - let mut entries = Vec::new(); - for entry in tree.iter() { - let url = match &filepath { - Some(filepath) => format!( - "/{}/+/{}/{}/{}", - path.to_str().unwrap(), - tree_name, - filepath.to_str().unwrap(), - entry.name().unwrap() - ), - None => format!( - "/{}/+/{}/{}", - path.to_str().unwrap(), - tree_name, - entry.name().unwrap() - ), - }; - entries.push(json!({ - "name": entry.name().unwrap(), - "url": url, - })); - } - ctx.insert("entries", &entries); - return Ok(Response::new(TERA.render("repo_tree.html", &ctx)?.into())); - } - - pub fn render_repo_blob( - &self, - path: PathBuf, - repo: &Repository, - tree_name: String, - filepath: impl AsRef, - blob_id: Oid, - ) -> Result> { - let filepath = filepath.as_ref(); - let blob = repo.find_blob(blob_id)?; - let mut ctx = TeraContext::new(); - ctx.insert("repo_name", &path); - ctx.insert("tree_name", &tree_name); - ctx.insert( - "blob", - &json!({ - "name": filepath, - "binary": blob.is_binary(), - "contents": if blob.is_binary() { - json!(null) - } else { - json!(String::from_utf8(blob.content().to_vec())?) - }, - }), - ); - return Ok(Response::new(TERA.render("repo_blob.html", &ctx)?.into())); - } - - pub fn handle(self, req: Request) -> Result> { - let repo_root = self.config.repo_root.clone(); - let uri = req.uri(); - let path = uri.path(); - - if path == "/" { - return self.render_index(); - } - - let repo_info = { - let mut repo_info = None; - for repo_dir in self.get_dir_list()? { - let path = PathBuf::from(path.trim_start_matches("/")); - if path.starts_with(&repo_dir) { - let repo = Repository::open(repo_root.join(&repo_dir))?; - let remainder = PathBuf::from("/").join(path.strip_prefix(&repo_dir)?); - repo_info = Some((repo_dir, repo, remainder)); - } - } - repo_info - }; - - if let Some((path, repo, remainder)) = repo_info { - if remainder == PathBuf::from("/") { - return self.render_repo_index(path, &repo); - } else if remainder.starts_with("/+") { - let mut iter = remainder.strip_prefix("/+").unwrap().components(); - let tree_name = iter.next(); - if let Some(PathComponent::Normal(tree_name)) = tree_name { - let tree_name = tree_name.to_str().unwrap().to_string(); - let tree_name = if tree_name == "refs" { - let second = iter.next().unwrap(); - let third = iter.next().unwrap(); - format!( - "{}/{}/{}", - tree_name, - second.as_os_str().to_str().unwrap(), - third.as_os_str().to_str().unwrap() - ) - } else { - tree_name - }; - let (tree_id, shortname) = { - let (tree, shortname) = get_tree_from_name(&repo, tree_name)?; - (tree.id(), shortname) - }; - let filepath = iter.collect::(); - println!("filepath: {:?}", filepath); - if filepath.iter().collect::>().len() == 0 { - return self.render_repo_tree(path, &repo, tree_id, shortname, None); - } else { - let (object_id, object_type) = - get_path_from_tree(&repo, tree_id, &filepath)?; - println!("{:?} {:?}", object_id, object_type); - match object_type { - ObjectType::Tree => { - return self.render_repo_tree( - path, - &repo, - object_id, - shortname, - Some(filepath), - ) - } - ObjectType::Blob => { - return self - .render_repo_blob(path, &repo, shortname, filepath, object_id) - } - _ => unimplemented!(), - } - } - } - } - } - - Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .body("not found".into()) - .unwrap()) - } -} - -fn get_tree_from_name(repo: &Repository, name: impl AsRef) -> Result<(Tree, String)> { - Ok( - if let Ok(commit_ref) = repo.resolve_reference_from_short_name(name.as_ref()) { - let tree_name = commit_ref.shorthand().unwrap().to_string(); - (commit_ref.peel_to_commit()?.tree()?, tree_name) - } else if let Ok(commit) = repo.find_commit(Oid::from_str(name.as_ref())?) { - let tree_name = name.as_ref().to_string(); - (commit.tree()?, tree_name) - } else { - anyhow::bail!("Fuck!"); - }, - ) -} - -fn get_path_from_tree<'a>( - repo: &Repository, - curr_oid: Oid, - path: impl AsRef, -) -> Result<(Oid, ObjectType)> { - let mut components = path.as_ref().components(); - let tree = repo.find_tree(curr_oid)?; - if let Some(next_file) = components.next() { - if let Some(entry) = tree.get_name(next_file.as_os_str().to_str().unwrap()) { - let kind = entry.kind().unwrap(); - match kind { - ObjectType::Tree => { - get_path_from_tree(repo, entry.id(), components.collect::()) - } - _ => Ok((entry.id(), kind)), - } - } else { - anyhow::bail!("doesn't exist bro: {:?}", next_file); - } - } else { - Ok((tree.id(), ObjectType::Tree)) - } -} #[tokio::main] async fn main() -> Result<()> { - let mut config_file = File::open("config.toml").await?; + let mut config_file = File::open("config.toml")?; let mut config_str = String::new(); - config_file.read_to_string(&mut config_str).await?; + config_file.read_to_string(&mut config_str)?; let config = toml::from_str::(&config_str)?; let addr = config.addr.clone(); diff --git a/static/main.css b/static/main.css new file mode 100644 index 0000000..78715f0 --- /dev/null +++ b/static/main.css @@ -0,0 +1,46 @@ +:root { + --background-color: #15202B; + --text-color: #D4D4D4; + --link-color: lightskyblue; + --sans-font: "Helvetica", "Arial", "Liberation Sans", sans-serif; +} + +body { + max-width: 850px; + margin: auto; + min-height: 100%; + padding-bottom: 20px; + font-family: var(--sans-font); + + background-color: var(--background-color); + color: var(--text-color); +} + +header { + margin: auto 12px; +} + +header #header { + border-bottom: 2px solid var(--link-color); + box-sizing: border-box; + padding: 20px; + margin-bottom: 12px; +} + +header #header #title { + font-size: 2em; + color: var(--text-color); +} + +header #header #title:hover { + text-decoration: none; +} + +a { + color: var(--link-color); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} diff --git a/templates/index.html b/templates/index.html index d291fa8..72ec53a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,14 +1,7 @@ - - - +{% extends "layout.html" %} - -

Fedhub

- -
    - {% for repo in repos %} -
  • {{ repo }}
  • - {% endfor %} -
- - +{% block content %} + {% for repo in repos %} +
  • {{ repo }}
  • + {% endfor %} +{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..8ba92b6 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,25 @@ + + + + + + {% block title %}{% endblock %} + + + + + +
    + +
    + +
    + {% block content %}{% endblock %} +
    + + diff --git a/templates/repo_blob.html b/templates/repo_blob.html index fbca800..78207d9 100644 --- a/templates/repo_blob.html +++ b/templates/repo_blob.html @@ -1,10 +1,7 @@ - - - +{% extends "layout.html" %} - -

    {{ repo_name }}: {{ tree_name }}: {{ blob.name }}

    +{% block content %} +

    {{ repo_name }}: {{ tree_name }}: {{ blob.name }}

    -
    {{ blob.contents }}
    - - +
    {{ blob.contents }}
    +{% endblock %} diff --git a/templates/repo_index.html b/templates/repo_index.html index e441618..983e3cf 100644 --- a/templates/repo_index.html +++ b/templates/repo_index.html @@ -1,15 +1,12 @@ - - - +{% extends "layout.html" %} - -

    {{ repo_name }}

    +{% block content %} +

    {{ repo_name }}

    -

    branches:

    - - - +

    branches:

    + +{% endblock %} diff --git a/templates/repo_tree.html b/templates/repo_tree.html index 864ca95..73c7b77 100644 --- a/templates/repo_tree.html +++ b/templates/repo_tree.html @@ -1,15 +1,12 @@ - - - +{% extends "layout.html" %} - -

    {{ repo_name }}: {{ tree_name }}{% if filepath %}: {{ filepath }}{% endif %}

    +{% block content %} +

    {{ repo_name }}: {{ tree_name }}{% if filepath %}: {{ filepath }}{% endif %}

    -

    entries:

    - - - +

    entries:

    + +{% endblock %}