This commit is contained in:
Michael Zhang 2020-04-21 12:33:14 -05:00
parent dd446e25a1
commit a0f56e58cf
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
15 changed files with 715 additions and 380 deletions

199
Cargo.lock generated
View file

@ -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"

View file

@ -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"

0
Justfile Normal file
View file

1
build.rs Normal file
View file

@ -0,0 +1 @@
fn main() {}

View file

@ -1,3 +1,3 @@
addr = "127.0.0.1:3000"
repo_root = "./repos"
redis_url = "redis://127.0.0.1"
redis_url = "redis://127.0.0.1"

55
dev-server.old.rs Normal file
View file

@ -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(())
}

View file

@ -3,6 +3,7 @@ use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Config {
pub site_name: Option<String>,
pub addr: SocketAddr,
pub repo_root: PathBuf,
pub redis_url: String,

344
src/lib.rs Normal file
View file

@ -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<RwLock<State>>,
}
#[derive(Clone)]
struct State {
cache: RedisClient,
}
impl Fedhub {
pub fn new(config: Config) -> Result<Self> {
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<Vec<PathBuf>> {
let state = self.state.read();
let mut con = state.cache.get_connection()?;
if RedisCmd::exists("repos").query(&mut con)? {
let path_strs: Vec<String> = 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::<Vec<_>>(),
)
.execute(&mut con);
Ok(dirs)
}
}
pub fn render_index(&self) -> Result<Response<Body>> {
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<Response<Body>> {
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<PathBuf>,
) -> Result<Response<Body>> {
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<Path>,
blob_id: Oid,
) -> Result<Response<Body>> {
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<Body>) -> Result<Response<Body>> {
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::<Vec<_>>());
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::<PathBuf>();
println!("filepath: {:?}", filepath);
if filepath.iter().collect::<Vec<_>>().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<str>) -> 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<Path>,
) -> 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::<PathBuf>())
}
_ => Ok((entry.id(), kind)),
}
} else {
anyhow::bail!("doesn't exist bro: {:?}", next_file);
}
} else {
Ok((tree.id(), ObjectType::Tree))
}
}

View file

@ -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<RwLock<State>>,
}
#[derive(Clone)]
struct State {
cache: RedisClient,
}
impl Fedhub {
pub fn new(config: Config) -> Result<Self> {
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<Vec<PathBuf>> {
let state = self.state.read();
let mut con = state.cache.get_connection()?;
if RedisCmd::exists("repos").query(&mut con)? {
let path_strs: Vec<String> = 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::<Vec<_>>(),
)
.execute(&mut con);
Ok(dirs)
}
}
pub fn render_index(&self) -> Result<Response<Body>> {
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<Response<Body>> {
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<PathBuf>,
) -> Result<Response<Body>> {
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<Path>,
blob_id: Oid,
) -> Result<Response<Body>> {
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<Body>) -> Result<Response<Body>> {
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::<PathBuf>();
println!("filepath: {:?}", filepath);
if filepath.iter().collect::<Vec<_>>().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<str>) -> 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<Path>,
) -> 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::<PathBuf>())
}
_ => 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>(&config_str)?;
let addr = config.addr.clone();

46
static/main.css Normal file
View file

@ -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;
}

View file

@ -1,14 +1,7 @@
<html>
<head>
</head>
{% extends "layout.html" %}
<body>
<h1>Fedhub</h1>
<ul>
{% for repo in repos %}
<li><a href="/{{ repo }}">{{ repo }}</a></li>
{% endfor %}
</ul>
</body>
</html>
{% block content %}
{% for repo in repos %}
<li><a href="/{{ repo }}">{{ repo }}</a></li>
{% endfor %}
{% endblock %}

25
templates/layout.html Normal file
View file

@ -0,0 +1,25 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/static/main.css" />
</head>
<body>
<header>
<div id="header" class="container">
<a href="/" id="title" class="title nocolorlink">{{ fedhub.site_name }}</a>
<div id="nav">
</div>
</div>
</header>
<div class="container" style="padding: 5px 40px;">
{% block content %}{% endblock %}
</div>
</body>
</html>

View file

@ -1,10 +1,7 @@
<html>
<head>
</head>
{% extends "layout.html" %}
<body>
<h1>{{ repo_name }}: {{ tree_name }}: {{ blob.name }}</h1>
{% block content %}
<h1>{{ repo_name }}: {{ tree_name }}: {{ blob.name }}</h1>
<pre>{{ blob.contents }}</pre>
</body>
</html>
<pre>{{ blob.contents }}</pre>
{% endblock %}

View file

@ -1,15 +1,12 @@
<html>
<head>
</head>
{% extends "layout.html" %}
<body>
<h1>{{ repo_name }}</h1>
{% block content %}
<h1>{{ repo_name }}</h1>
<h3>branches:</h3>
<ul>
{% for branch in branches %}
<a href="{{ branch.url }}">{{ branch.name }}</a>
{% endfor %}
</ul>
</body>
</html>
<h3>branches:</h3>
<ul>
{% for branch in branches %}
<a href="{{ branch.url }}">{{ branch.name }}</a>
{% endfor %}
</ul>
{% endblock %}

View file

@ -1,15 +1,12 @@
<html>
<head>
</head>
{% extends "layout.html" %}
<body>
<h1>{{ repo_name }}: {{ tree_name }}{% if filepath %}: {{ filepath }}{% endif %}</h1>
{% block content %}
<h1>{{ repo_name }}: {{ tree_name }}{% if filepath %}: {{ filepath }}{% endif %}</h1>
<h3>entries:</h3>
<ul>
{% for entry in entries %}
<li><a href="{{ entry.url }}">{{ entry.name }}</a></li>
{% endfor %}
</ul>
</body>
</html>
<h3>entries:</h3>
<ul>
{% for entry in entries %}
<li><a href="{{ entry.url }}">{{ entry.name }}</a></li>
{% endfor %}
</ul>
{% endblock %}