mirror of
https://forge.pointfixe.fr/hubert/gitust.git
synced 2026-02-04 20:37:28 +01:00
Compare commits
39 Commits
b76338f802
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6beae6629f | |||
|
|
ba19d67f66 | ||
|
|
f9bc5b2e39 | ||
|
|
dbba11a416 | ||
|
|
edf6f9f81a | ||
|
|
5f6f7b7efe | ||
|
|
1f4d08aff1 | ||
|
|
f71e8ff1b3 | ||
|
|
4134982739 | ||
|
|
1e7c773c19 | ||
|
|
9832f30360 | ||
|
|
22836e1f3a | ||
|
|
90ea298a61 | ||
|
|
e3008262fc | ||
|
|
abc1f367bb | ||
| 6d87adb2e6 | |||
| aa131d009f | |||
|
|
9af4fa56e1 | ||
|
|
90bc6d6d4f | ||
|
|
381347e66a | ||
| 234e2ccaa3 | |||
|
|
2bc387920c | ||
|
|
07f11b238e | ||
|
|
0875a78cf8 | ||
| 510bbe7381 | |||
| 1950a5312e | |||
|
|
a562c4616c | ||
|
|
06fcc46a15 | ||
| 35537c6688 | |||
| aa656d3a19 | |||
| 82757ef9eb | |||
| 2278b46a88 | |||
|
|
a3bf4afe51 | ||
|
|
a2dcab2ea8 | ||
|
|
dc26021c43 | ||
|
|
ecd0837490 | ||
|
|
5f89a95587 | ||
|
|
8a0b525649 | ||
|
|
7fbd18047e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
/.idea/
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ env_logger = "0.8.4"
|
|||||||
serde = "1.0.126"
|
serde = "1.0.126"
|
||||||
futures = "0.3.15"
|
futures = "0.3.15"
|
||||||
tokio = {version = "0.2.25", features = ["time", "process", "io-util"]}
|
tokio = {version = "0.2.25", features = ["time", "process", "io-util"]}
|
||||||
|
git2 = "0.13.20"
|
||||||
|
|||||||
31
src/error.rs
Normal file
31
src/error.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use actix_web::dev::HttpResponseBuilder;
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web_httpauth::extractors::{basic, AuthenticationError};
|
||||||
|
|
||||||
|
pub enum Error {
|
||||||
|
BadGateway(String),
|
||||||
|
Unauthorized(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<git2::Error> for Error {
|
||||||
|
fn from(giterr: git2::Error) -> Self {
|
||||||
|
Error::BadGateway(format!("{}", giterr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(ioerr: std::io::Error) -> Self {
|
||||||
|
Error::BadGateway(format!("{}", ioerr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for actix_web::error::Error {
|
||||||
|
fn from(e: Error) -> Self {
|
||||||
|
match e {
|
||||||
|
Error::BadGateway(msg) => {HttpResponseBuilder::new(StatusCode::BAD_GATEWAY).body(msg).into()}
|
||||||
|
Error::Unauthorized(realm) => {AuthenticationError::from(basic::Config::default().realm(realm)).into()}}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/git.rs
66
src/git.rs
@@ -1,65 +1,19 @@
|
|||||||
use std::fmt::{Display, Formatter, Result};
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::ops::Add;
|
||||||
|
|
||||||
pub struct GitRepo {}
|
use git2::{Commit, ObjectType, Oid, Reference, Repository, Tree};
|
||||||
|
|
||||||
impl GitRepo {
|
use crate::gitutils::gitdir::GitDir;
|
||||||
|
use crate::gitutils::gitfile::GitFile;
|
||||||
pub fn new() -> GitRepo {
|
|
||||||
GitRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn browse(&self, commit : &GitCommit, dir: &GitBrowseDir) -> Vec<GitBrowseEntry> {
|
|
||||||
vec!(GitBrowseEntry::EGitDir(GitBrowseDir("src/".to_string())), GitBrowseEntry::EGitFile(GitBrowseFile("pom.xml".to_string())))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct GitCommit (String);
|
pub enum GitBrowseEntry<'a> {
|
||||||
|
EGitFile(GitFile),
|
||||||
impl GitCommit {
|
EGitDir(GitDir<'a>)
|
||||||
pub fn new(s : String) -> GitCommit {
|
|
||||||
GitCommit(s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for GitCommit {
|
impl Display for GitBrowseEntry<'_> {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GitBrowseFile (String);
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GitBrowseDir (String);
|
|
||||||
|
|
||||||
impl GitBrowseDir {
|
|
||||||
pub fn new(s : String) -> GitBrowseDir {
|
|
||||||
GitBrowseDir(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for GitBrowseFile {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for GitBrowseDir {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum GitBrowseEntry {
|
|
||||||
EGitFile(GitBrowseFile),
|
|
||||||
EGitDir(GitBrowseDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for GitBrowseEntry {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
|
||||||
match self {
|
match self {
|
||||||
GitBrowseEntry::EGitFile(file) => {write!(f, "{}", file)}
|
GitBrowseEntry::EGitFile(file) => {write!(f, "{}", file)}
|
||||||
GitBrowseEntry::EGitDir(dir) => {write!(f, "{}", dir)}
|
GitBrowseEntry::EGitDir(dir) => {write!(f, "{}", dir)}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub struct Gitust {
|
pub struct Gitust {
|
||||||
pub repo_root_path : String,
|
pub repo_root_path : String,
|
||||||
|
pub session_key : String,
|
||||||
}
|
}
|
||||||
28
src/gitutils/gitcommit.rs
Normal file
28
src/gitutils/gitcommit.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum GitRef{
|
||||||
|
Commit(String),
|
||||||
|
Branch(String),
|
||||||
|
Head,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitRef {
|
||||||
|
pub fn new_commit(s : String) -> GitRef {
|
||||||
|
GitRef::Commit(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_branch(s : String) -> GitRef {
|
||||||
|
GitRef::Branch(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for GitRef {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", match &self {
|
||||||
|
GitRef::Commit(s) => {s.as_str()}
|
||||||
|
GitRef::Branch(s) => {s.as_str()}
|
||||||
|
GitRef::Head => {"HEAD"}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/gitutils/gitdir.rs
Normal file
14
src/gitutils/gitdir.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GitDir<'a>{
|
||||||
|
pub fullname: PathBuf,
|
||||||
|
pub tree : git2::Tree<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for GitDir<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.fullname.display())
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/gitutils/gitfile.rs
Normal file
21
src/gitutils/gitfile.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::path::{PathBuf};
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GitFile(pub PathBuf);
|
||||||
|
|
||||||
|
impl GitFile {
|
||||||
|
pub(crate) fn new(fullname : PathBuf) -> GitFile {
|
||||||
|
GitFile(fullname)
|
||||||
|
}
|
||||||
|
pub fn name(&self) -> Option<&OsStr> {
|
||||||
|
self.0.file_name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for GitFile {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0.display())
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/gitutils/gitproto.rs
Normal file
109
src/gitutils/gitproto.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io;
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
use actix_web::{HttpRequest, web, HttpResponse};
|
||||||
|
use actix_web::http::{header, StatusCode};
|
||||||
|
use actix_web::web::Buf;
|
||||||
|
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
||||||
|
use tokio::process::{Child, Command};
|
||||||
|
|
||||||
|
|
||||||
|
use crate::gitust::Gitust;
|
||||||
|
use crate::reader::ToStream;
|
||||||
|
use crate::error;
|
||||||
|
use crate::webutils::auth;
|
||||||
|
|
||||||
|
//#[get("/git/{owner}/{repo}.git/{path:.*}")]
|
||||||
|
pub async fn git_proto<T : auth::AuthValidator>(
|
||||||
|
mut payload : web::Payload,
|
||||||
|
web::Path((owner, reponame, path)): web::Path<(String, String, String)>,
|
||||||
|
mut req: HttpRequest,
|
||||||
|
gitust : web::Data<Gitust>,
|
||||||
|
authenticator : web::Data<T>,
|
||||||
|
auth : Option<BasicAuth>,
|
||||||
|
) -> Result<HttpResponse, error::Error>{
|
||||||
|
let user = auth.and_then(|a| authenticator.check_basic(&a)).ok_or(error::Error::Unauthorized("git_proto".to_string()))?;
|
||||||
|
//println!("enter git_proto");
|
||||||
|
let mut cmd = Command::new("git");
|
||||||
|
cmd.arg("http-backend");
|
||||||
|
|
||||||
|
// Required environment variables
|
||||||
|
cmd.env("REQUEST_METHOD", req.method().as_str());
|
||||||
|
cmd.env("GIT_PROJECT_ROOT", &gitust.repo_root_path);
|
||||||
|
cmd.env("PATH_INFO", format!("/{}/{}.git/{}",owner, reponame, path));
|
||||||
|
cmd.env("REMOTE_USER", user.get_name());
|
||||||
|
//cmd.env("REMOTE_ADDR", req.remote_addr().to_string());
|
||||||
|
cmd.env("QUERY_STRING", req.query_string());
|
||||||
|
cmd.env("CONTENT_TYPE", header(&req, header::CONTENT_TYPE));
|
||||||
|
cmd.env("GIT_HTTP_EXPORT_ALL", "");
|
||||||
|
cmd.stderr(Stdio::inherit())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stdin(Stdio::piped());
|
||||||
|
let mut p: Child = cmd.spawn()?;
|
||||||
|
let mut input = p.stdin.take().unwrap();
|
||||||
|
//println!("Displaying request...");
|
||||||
|
while let Some(Ok(bytes)) = payload.next().await {
|
||||||
|
//println!("request body : {}", String::from_utf8_lossy(bytes.bytes()));
|
||||||
|
input.write_all(bytes.bytes()).await;
|
||||||
|
}
|
||||||
|
//println!("input sent");
|
||||||
|
let mut rdr = tokio::io::BufReader::new(p.stdout.take().unwrap());
|
||||||
|
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
let len = rdr.read_line(&mut line).await?;
|
||||||
|
// println!("line : \"{}\"", line);
|
||||||
|
if line.len() == 2 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parts = line.splitn(2, ':');
|
||||||
|
let key = parts.next().unwrap();
|
||||||
|
let value = parts.next().unwrap();
|
||||||
|
let value_len = value.len();
|
||||||
|
let value = &value[1..value_len-2];
|
||||||
|
headers
|
||||||
|
.entry(key.to_string())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(value.to_string());
|
||||||
|
}
|
||||||
|
//println!("response headers : {:?}", headers);
|
||||||
|
|
||||||
|
let status_code : u16 = {
|
||||||
|
let line = headers.remove("Status").unwrap_or_default();
|
||||||
|
// println!("{:?}", &line);
|
||||||
|
let line = line.into_iter().next().unwrap_or_default();
|
||||||
|
let parts : Vec<&str> = line.split(' ').collect();
|
||||||
|
parts.into_iter().next().unwrap_or("").parse().unwrap_or(200)
|
||||||
|
};
|
||||||
|
// println!("status code {}", status_code);
|
||||||
|
|
||||||
|
let statusCode = match StatusCode::from_u16(status_code) {
|
||||||
|
Ok(v) => {Ok(v)}
|
||||||
|
Err(ioe) => {Err(io::Error::new(ErrorKind::Other, "Invalid HTTP status code"))}
|
||||||
|
};
|
||||||
|
let mut builder = HttpResponse::build(statusCode?);
|
||||||
|
for (name, vec) in headers.iter() {
|
||||||
|
for value in vec {
|
||||||
|
// println!("entry : ({}, {})", name, value.clone());
|
||||||
|
builder.header(name, value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// println!("Write body...");
|
||||||
|
|
||||||
|
let response = builder.streaming(ToStream(rdr));
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header(req: &HttpRequest, name: header::HeaderName) -> &str {
|
||||||
|
req.headers()
|
||||||
|
.get(name)
|
||||||
|
.map(|value| value.to_str().unwrap_or_default())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
105
src/gitutils/gitrepo.rs
Normal file
105
src/gitutils/gitrepo.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use std::ops::Add;
|
||||||
|
|
||||||
|
use git2::{ObjectType, Oid, Repository, BranchType};
|
||||||
|
|
||||||
|
use crate::git::GitBrowseEntry;
|
||||||
|
use crate::gitutils::gitcommit::GitRef;
|
||||||
|
use crate::gitutils::gitdir::GitDir;
|
||||||
|
use crate::gitutils::gitfile::GitFile;
|
||||||
|
use std::path::{PathBuf, Path};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
pub struct GitRepo {
|
||||||
|
git2 : Repository,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitRepo {
|
||||||
|
|
||||||
|
pub fn new(path : &str) -> Result<GitRepo, git2::Error> {
|
||||||
|
Repository::open(path).map(|git2| GitRepo{git2})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tree<'a, 'b, 'c> (&'a self, commit : &'b GitRef, path : &'c Path) -> Result<GitDir<'a>, git2::Error> {
|
||||||
|
let commit = match commit {
|
||||||
|
GitRef::Commit(commit) => {
|
||||||
|
// println!("commit : {}", commit.as_str());
|
||||||
|
let oid = Oid::from_str(commit.as_str())?;
|
||||||
|
// println!("{}", oid);
|
||||||
|
self.git2.find_commit(oid)?
|
||||||
|
}
|
||||||
|
GitRef::Branch(branch) => {
|
||||||
|
let branch = self.git2.find_branch(branch.as_str(), BranchType::Local)?;
|
||||||
|
branch.get().peel_to_commit()?
|
||||||
|
}
|
||||||
|
GitRef::Head => {
|
||||||
|
// println!("coucou1");
|
||||||
|
let head = self.git2.head()?;
|
||||||
|
// println!("coucou");
|
||||||
|
head.peel_to_commit()?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let root_tree = commit.tree()?;
|
||||||
|
let tree = if path.as_os_str().len() == 0 {
|
||||||
|
root_tree
|
||||||
|
} else {
|
||||||
|
let oid1 = root_tree.get_path(path)?.id();
|
||||||
|
// println!("{}", oid1);
|
||||||
|
self.git2.find_tree(oid1)?
|
||||||
|
};
|
||||||
|
let fullname = path.to_path_buf();
|
||||||
|
Ok(GitDir { fullname, tree })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn browse<'a>(&'a self, dir: &GitDir<'a>) -> Result<Vec<GitBrowseEntry<'a>>, git2::Error> {
|
||||||
|
let mut res = Vec::new();
|
||||||
|
for entry in dir.tree.iter() {
|
||||||
|
match entry.kind() {
|
||||||
|
None => {Err(git2::Error::from_str("each tree entry must be well defined"))?;}
|
||||||
|
Some(kind) => match kind {
|
||||||
|
ObjectType::Any => {Err(git2::Error::from_str("tree entry cannot be of kind Any"))?;}
|
||||||
|
ObjectType::Commit => {Err(git2::Error::from_str("tree entry cannot be of kind Commit"))?;}
|
||||||
|
ObjectType::Tree => {
|
||||||
|
let name = entry.name().ok_or(git2::Error::from_str("entry must have valid utf8 name"))?;
|
||||||
|
let mut fullname = dir.fullname.clone();
|
||||||
|
fullname.push(name);
|
||||||
|
let subtree = self.git2.find_tree(entry.id())?;
|
||||||
|
res.push(GitBrowseEntry::EGitDir(GitDir {fullname, tree : subtree}));
|
||||||
|
}
|
||||||
|
ObjectType::Blob => {
|
||||||
|
let name = entry.name().ok_or(git2::Error::from_str("entry must have valid utf8 name"))?;
|
||||||
|
let mut fullname = dir.fullname.clone();
|
||||||
|
fullname.push(name);
|
||||||
|
res.push(GitBrowseEntry::EGitFile(GitFile::new(fullname)));
|
||||||
|
}
|
||||||
|
ObjectType::Tag => {Err(git2::Error::from_str("tree entry cannot be of kind tag"))?;}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_revisions_nbr(&self) -> u32 {
|
||||||
|
let mut res = 0;
|
||||||
|
let mut revwalk = self.git2.revwalk().expect("revisions walk is not defined");
|
||||||
|
revwalk.push_head(); // todo : voir ce qu'il faut réellement pousser si on veux avoir TOUS les commits (genre par exemple si le HEAD n'est pas fils de tous les commits)
|
||||||
|
for _ in revwalk{
|
||||||
|
res = res + 1;
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_branches_nbr(&self) -> u32 {
|
||||||
|
let mut res = 0;
|
||||||
|
for _ in self.git2.branches(Some(BranchType::Local)){
|
||||||
|
res = res + 1;
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tags_nbr(&self) -> u32 {
|
||||||
|
let mut res = 0;
|
||||||
|
self.git2.tag_foreach(|_,_| {res = res + 1;true});
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
5
src/gitutils/mod.rs
Normal file
5
src/gitutils/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod gitdir;
|
||||||
|
pub mod gitrepo;
|
||||||
|
pub mod gitcommit;
|
||||||
|
pub mod gitfile;
|
||||||
|
pub mod gitproto;
|
||||||
282
src/main.rs
282
src/main.rs
@@ -1,40 +1,58 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io;
|
||||||
|
use std::io::{BufRead, ErrorKind, Read, Write};
|
||||||
|
use std::ops::Add;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
use actix_files::Files;
|
||||||
|
use actix_session::{CookieSession, Session};
|
||||||
|
use actix_web::{App, Error, get, HttpMessage, HttpRequest, HttpResponse, HttpServer, post, Responder};
|
||||||
|
use actix_web::client::PayloadError;
|
||||||
|
use actix_web::dev::{ServiceRequest, Service};
|
||||||
|
use actix_web::http::{header, StatusCode};
|
||||||
|
use actix_web::http::header::Header;
|
||||||
|
use actix_web::http::header::IntoHeaderValue;
|
||||||
|
use actix_web::middleware::Logger;
|
||||||
|
use actix_web::web as webx;
|
||||||
|
use actix_web::web::{Buf, Bytes};
|
||||||
|
use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractor};
|
||||||
|
use actix_web_httpauth::extractors::basic::{BasicAuth, Config};
|
||||||
|
use actix_web_httpauth::headers::authorization::{Authorization, Basic};
|
||||||
|
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||||
|
use askama_actix::Template;
|
||||||
|
use env_logger::Env;
|
||||||
|
use futures::{future, pin_mut, Stream, stream, StreamExt, TryFutureExt, TryStreamExt};
|
||||||
|
use git2::ObjectType;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
||||||
|
use tokio::process::{Child, ChildStdout, Command};
|
||||||
|
|
||||||
|
use gitutils::gitcommit::GitRef;
|
||||||
|
use gitutils::gitdir::GitDir;
|
||||||
|
use gitutils::gitfile::GitFile;
|
||||||
|
use gitutils::gitproto;
|
||||||
|
use gitutils::gitrepo::GitRepo;
|
||||||
|
use web::repo;
|
||||||
|
use webutils::auth;
|
||||||
|
|
||||||
|
use crate::git::GitBrowseEntry;
|
||||||
|
use crate::gitust::Gitust;
|
||||||
|
use crate::ite::SuperIterator;
|
||||||
|
use crate::reader::ToStream;
|
||||||
|
use crate::writer::Writer;
|
||||||
|
use crate::web::repo::GitWebQ;
|
||||||
|
use crate::webutils::auth::{TestValidator, AuthValidator, User};
|
||||||
|
|
||||||
mod git;
|
mod git;
|
||||||
mod ite;
|
mod ite;
|
||||||
mod reader;
|
mod reader;
|
||||||
mod writer;
|
mod writer;
|
||||||
mod gitust;
|
mod gitust;
|
||||||
|
mod error;
|
||||||
use actix_files::Files;
|
mod web;
|
||||||
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, Error, HttpRequest, HttpMessage};
|
mod gitutils;
|
||||||
use askama_actix::Template;
|
mod webutils;
|
||||||
use actix_session::{Session, CookieSession};
|
|
||||||
use actix_web_httpauth::headers::authorization::{Authorization, Basic};
|
|
||||||
use actix_web::http::header::Header;
|
|
||||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
|
||||||
use actix_web::dev::ServiceRequest;
|
|
||||||
use actix_web_httpauth::extractors::basic::{BasicAuth, Config};
|
|
||||||
use actix_web_httpauth::extractors::AuthenticationError;
|
|
||||||
use crate::git::{GitBrowseEntry, GitRepo, GitCommit, GitBrowseDir};
|
|
||||||
use actix_web::middleware::Logger;
|
|
||||||
use env_logger::Env;
|
|
||||||
use crate::ite::SuperIterator;
|
|
||||||
use std::ops::Add;
|
|
||||||
use std::path::{PathBuf, Path};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use tokio::process::{Command, Child, ChildStdout};
|
|
||||||
use tokio::io::{AsyncWriteExt, AsyncBufReadExt, AsyncReadExt, BufReader};
|
|
||||||
use std::process::Stdio;
|
|
||||||
use actix_web::http::{header, StatusCode};
|
|
||||||
use std::io;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::{Read, BufRead, Write, ErrorKind};
|
|
||||||
use futures::{Stream, StreamExt, TryStreamExt, future, stream, TryFutureExt, pin_mut};
|
|
||||||
use actix_web::web::{Buf, Bytes};
|
|
||||||
use actix_web::http::header::IntoHeaderValue;
|
|
||||||
use actix_web::client::PayloadError;
|
|
||||||
use crate::reader::ToStream;
|
|
||||||
use crate::writer::Writer;
|
|
||||||
use crate::gitust::Gitust;
|
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "hello.html")]
|
#[template(path = "hello.html")]
|
||||||
@@ -42,31 +60,6 @@ struct HelloTemplate<'a> {
|
|||||||
name: &'a str,
|
name: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct User {
|
|
||||||
name : String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Owner {
|
|
||||||
name : String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Repository {
|
|
||||||
name : String,
|
|
||||||
owner : Owner,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "git.html")]
|
|
||||||
struct GitMainTemplate<TS, ROOT>
|
|
||||||
where for <'a> &'a TS : IntoIterator<Item = &'a GitBrowseEntry>,
|
|
||||||
for <'a> &'a ROOT : IntoIterator<Item = &'a (String, String)>,
|
|
||||||
{
|
|
||||||
repo : Repository,
|
|
||||||
browse : TS,
|
|
||||||
root : ROOT,
|
|
||||||
user_opt : Option<User>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn hello() -> impl Responder {
|
async fn hello() -> impl Responder {
|
||||||
HttpResponse::Ok().body("Bonjour monde !")
|
HttpResponse::Ok().body("Bonjour monde !")
|
||||||
@@ -102,13 +95,6 @@ async fn hello_auth(req : HttpRequest) -> impl Responder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct GitWebQ {
|
|
||||||
commit: Option<String>,
|
|
||||||
path: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[get("/chunk")]
|
#[get("/chunk")]
|
||||||
async fn chunk() -> HttpResponse {
|
async fn chunk() -> HttpResponse {
|
||||||
let (tx, rx) = actix_utils::mpsc::channel::<Result<Bytes, Error>>();
|
let (tx, rx) = actix_utils::mpsc::channel::<Result<Bytes, Error>>();
|
||||||
@@ -127,120 +113,6 @@ async fn chunk() -> HttpResponse {
|
|||||||
HttpResponse::Ok().streaming(rx)
|
HttpResponse::Ok().streaming(rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/git/{owner}/{repo}.git")]
|
|
||||||
async fn git_main(
|
|
||||||
web::Path((owner, reponame)): web::Path<(String, String)>,
|
|
||||||
web::Query(GitWebQ{commit : commitnameopt, path : pathopt}) : web::Query<GitWebQ>,
|
|
||||||
gitust : web::Data<Gitust>
|
|
||||||
) -> impl Responder {
|
|
||||||
let commitname = match commitnameopt {
|
|
||||||
None => {"master".to_string()}
|
|
||||||
Some(s) => {s}
|
|
||||||
};
|
|
||||||
let rootname = match pathopt {
|
|
||||||
None => {"/".to_string()}
|
|
||||||
Some(s) => {s}
|
|
||||||
};
|
|
||||||
let repo = GitRepo::new();
|
|
||||||
let path : Vec<(String, String)> = rootname.split("/").map_accum("/git/".to_string() + &owner + "/" + &reponame + "/" + &commitname, |str_ref, b| {
|
|
||||||
let href = b + "/" + str_ref;
|
|
||||||
((str_ref.to_string(), href.clone()), href)
|
|
||||||
}).collect();
|
|
||||||
let commit = GitCommit::new(commitname);
|
|
||||||
let root = GitBrowseDir::new(rootname);
|
|
||||||
let browse = repo.browse(&commit, &root).await;
|
|
||||||
let owner = Owner { name : owner};
|
|
||||||
let repo = Repository {name : reponame, owner};
|
|
||||||
let user = User { name : "Hubert".to_string()};
|
|
||||||
GitMainTemplate { repo, browse : browse, root : path, user_opt : Some(user)}
|
|
||||||
}
|
|
||||||
//#[get("/git/{owner}/{repo}.git/{path:.*}")]
|
|
||||||
async fn git_proto(
|
|
||||||
mut payload : web::Payload,
|
|
||||||
web::Path((owner, reponame)): web::Path<(String, String)>,
|
|
||||||
mut req: HttpRequest,
|
|
||||||
gitust : web::Data<Gitust>
|
|
||||||
) -> io::Result<HttpResponse>{
|
|
||||||
//println!("enter git_proto");
|
|
||||||
let mut cmd = Command::new("git");
|
|
||||||
cmd.arg("http-backend");
|
|
||||||
|
|
||||||
// Required environment variables
|
|
||||||
cmd.env("REQUEST_METHOD", req.method().as_str());
|
|
||||||
cmd.env("GIT_PROJECT_ROOT", &gitust.repo_root_path);
|
|
||||||
cmd.env("PATH_INFO", format!("/{}/{}.git",owner, reponame));
|
|
||||||
cmd.env("REMOTE_USER", "");
|
|
||||||
//cmd.env("REMOTE_ADDR", req.remote_addr().to_string());
|
|
||||||
cmd.env("QUERY_STRING", req.query_string());
|
|
||||||
cmd.env("CONTENT_TYPE", header(&req, header::CONTENT_TYPE));
|
|
||||||
cmd.stderr(Stdio::inherit())
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stdin(Stdio::piped());
|
|
||||||
let mut p: Child = cmd.spawn()?;
|
|
||||||
let mut input = p.stdin.take().unwrap();
|
|
||||||
//println!("Displaying request...");
|
|
||||||
while let Some(Ok(bytes)) = payload.next().await {
|
|
||||||
//println!("request body : {}", String::from_utf8_lossy(bytes.bytes()));
|
|
||||||
input.write_all(bytes.bytes()).await;
|
|
||||||
}
|
|
||||||
//println!("input sent");
|
|
||||||
let mut rdr = tokio::io::BufReader::new(p.stdout.take().unwrap());
|
|
||||||
|
|
||||||
let mut headers = HashMap::new();
|
|
||||||
loop {
|
|
||||||
let mut line = String::new();
|
|
||||||
let len = rdr.read_line(&mut line).await?;
|
|
||||||
// println!("line : \"{}\"", line);
|
|
||||||
if line.len() == 2 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut parts = line.splitn(2, ':');
|
|
||||||
let key = parts.next().unwrap();
|
|
||||||
let value = parts.next().unwrap();
|
|
||||||
let value_len = value.len();
|
|
||||||
let value = &value[1..value_len-2];
|
|
||||||
headers
|
|
||||||
.entry(key.to_string())
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push(value.to_string());
|
|
||||||
}
|
|
||||||
//println!("response headers : {:?}", headers);
|
|
||||||
|
|
||||||
let status_code : u16 = {
|
|
||||||
let line = headers.remove("Status").unwrap_or_default();
|
|
||||||
// println!("{:?}", &line);
|
|
||||||
let line = line.into_iter().next().unwrap_or_default();
|
|
||||||
let parts : Vec<&str> = line.split(' ').collect();
|
|
||||||
parts.into_iter().next().unwrap_or("").parse().unwrap_or(200)
|
|
||||||
};
|
|
||||||
// println!("status code {}", status_code);
|
|
||||||
|
|
||||||
let statusCode = match StatusCode::from_u16(status_code) {
|
|
||||||
Ok(v) => {Ok(v)}
|
|
||||||
Err(ioe) => {Err(io::Error::new(ErrorKind::Other, "Invalid HTTP status code"))}
|
|
||||||
};
|
|
||||||
let mut builder = HttpResponse::build(statusCode?);
|
|
||||||
for (name, vec) in headers.iter() {
|
|
||||||
for value in vec {
|
|
||||||
// println!("entry : ({}, {})", name, value.clone());
|
|
||||||
builder.header(name, value.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// println!("Write body...");
|
|
||||||
|
|
||||||
let response = builder.streaming(ToStream(rdr));
|
|
||||||
return Ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn header(req: &HttpRequest, name: header::HeaderName) -> &str {
|
|
||||||
req.headers()
|
|
||||||
.get(name)
|
|
||||||
.map(|value| value.to_str().unwrap_or_default())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/echo")]
|
#[post("/echo")]
|
||||||
async fn echo(req_body: String) -> impl Responder {
|
async fn echo(req_body: String) -> impl Responder {
|
||||||
HttpResponse::Ok().body(req_body)
|
HttpResponse::Ok().body(req_body)
|
||||||
@@ -251,7 +123,7 @@ async fn manual_hello() -> impl Responder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/{id}/{name}/index.html")]
|
#[get("/{id}/{name}/index.html")]
|
||||||
async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder {
|
async fn index(webx::Path((id, name)): webx::Path<(u32, String)>) -> impl Responder {
|
||||||
format!("Hello {}! id:{}", name, id)
|
format!("Hello {}! id:{}", name, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,31 +146,61 @@ async fn basic_auth_validator(req: ServiceRequest, credentials: BasicAuth) -> Re
|
|||||||
|
|
||||||
fn validate_credentials(user_id: &str, user_password: Option<&str>) -> Result<bool, std::io::Error>
|
fn validate_credentials(user_id: &str, user_password: Option<&str>) -> Result<bool, std::io::Error>
|
||||||
{
|
{
|
||||||
if user_id.eq("karl") && user_password.eq(&Option::Some("password")) {
|
if user_id.eq("hubert") && user_password.eq(&Option::Some("hubert")) {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Authentication failed!"));
|
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Authentication failed!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LoginQ {
|
||||||
|
login: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
std::env::set_var("RUST_LOG", "actix_web=info");
|
std::env::set_var("RUST_LOG", "actix_web=info");
|
||||||
env_logger::from_env(Env::default().default_filter_or("info")).init();
|
env_logger::from_env(Env::default().default_filter_or("info")).init();
|
||||||
HttpServer::new(|| {
|
HttpServer::new(|| {
|
||||||
let auth = HttpAuthentication::basic(basic_auth_validator);
|
let auth = HttpAuthentication::basic(basic_auth_validator);
|
||||||
|
let gitust = Gitust {
|
||||||
|
repo_root_path: "/home/hubert/gitust".to_string(),
|
||||||
|
session_key: "oWe0ait9bi2Ohyiod2eeXirao1Oochie".to_string(),
|
||||||
|
};
|
||||||
|
let session_key = (&gitust.session_key).as_bytes();
|
||||||
App::new()
|
App::new()
|
||||||
.data(Gitust {
|
|
||||||
repo_root_path: "/home/hubert/gitust".to_string(),
|
|
||||||
})
|
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
// .wrap(Logger::new("%a %{User-Agent}i"))
|
// .wrap(Logger::new("%a %{User-Agent}i"))
|
||||||
.wrap(CookieSession::signed(&[0; 32]).secure(false))
|
.wrap(CookieSession::signed(session_key).secure(false))
|
||||||
|
.data(gitust)
|
||||||
|
.data(auth::TestValidator)
|
||||||
.service(hello)
|
.service(hello)
|
||||||
.service(echo)
|
.service(echo)
|
||||||
.service(hello_test)
|
.service(hello_test)
|
||||||
.service(index)
|
.service(index)
|
||||||
.service(askama)
|
.service(askama)
|
||||||
.service(git_main)
|
.service(
|
||||||
|
webx::resource("/git/{owner}/{repo}.git")
|
||||||
|
.wrap_fn(|req, serv| {
|
||||||
|
let query : Result<webx::Query<LoginQ>, _> = webx::Query::from_query(req.query_string());
|
||||||
|
let authFut = BasicAuth::from_service_request(&req);
|
||||||
|
let resFut = serv.call(req);
|
||||||
|
async {
|
||||||
|
let webx::Query(LoginQ{login}) = query?;
|
||||||
|
match login {
|
||||||
|
Some(true) => {
|
||||||
|
let auth = authFut.await?;
|
||||||
|
match TestValidator.check_basic(&auth) {
|
||||||
|
None => {Err(AuthenticationError::from(Config::default().realm("auth")).into())}
|
||||||
|
Some(_) => {resFut.await}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {resFut.await}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.route(webx::get().to(repo::git_main::<auth::TestValidator>))
|
||||||
|
)
|
||||||
.service(hello_session)
|
.service(hello_session)
|
||||||
.service(chunk)
|
.service(chunk)
|
||||||
//.service(git_proto)
|
//.service(git_proto)
|
||||||
@@ -308,13 +210,13 @@ async fn main() -> std::io::Result<()> {
|
|||||||
// .route("/", web::get().to(hello_auth))
|
// .route("/", web::get().to(hello_auth))
|
||||||
// )
|
// )
|
||||||
.service(
|
.service(
|
||||||
web::resource("/auth")
|
webx::resource("/auth")
|
||||||
.wrap(auth)
|
.wrap(auth)
|
||||||
.route(web::get().to(hello_auth))
|
.route(webx::get().to(hello_auth))
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/git/{user}/{repo}.git/{path:.*}")
|
webx::resource("/git/{user}/{repo}.git/{path:.*}")
|
||||||
.route(web::route().to(git_proto))
|
.route(webx::route().to(gitproto::git_proto::<auth::TestValidator>))
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
Files::new("/static", "static")
|
Files::new("/static", "static")
|
||||||
@@ -322,7 +224,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.use_etag(true)
|
.use_etag(true)
|
||||||
// .show_files_listing()
|
// .show_files_listing()
|
||||||
)
|
)
|
||||||
.route("/hey", web::get().to(manual_hello))
|
.route("/hey", webx::get().to(manual_hello))
|
||||||
})
|
})
|
||||||
.bind("127.0.0.1:8080")?
|
.bind("127.0.0.1:8080")?
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
1
src/web/mod.rs
Normal file
1
src/web/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod repo;
|
||||||
188
src/web/repo.rs
Normal file
188
src/web/repo.rs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
use crate::auth::AuthValidator;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use actix_web::web;
|
||||||
|
use actix_web_httpauth::extractors::basic;
|
||||||
|
|
||||||
|
use actix_web::get;
|
||||||
|
use askama_actix::Template;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
|
||||||
|
use crate::error;
|
||||||
|
use crate::git::GitBrowseEntry;
|
||||||
|
use crate::gitust::Gitust;
|
||||||
|
use crate::gitutils::gitcommit::GitRef;
|
||||||
|
use crate::gitutils::gitdir::GitDir;
|
||||||
|
use crate::gitutils::gitfile::GitFile;
|
||||||
|
use crate::gitutils::gitrepo::GitRepo;
|
||||||
|
use crate::ite::SuperIterator;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
struct User {
|
||||||
|
name : String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Owner {
|
||||||
|
name : String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Repository {
|
||||||
|
name : String,
|
||||||
|
owner : Owner,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Entry {
|
||||||
|
Dir(String),
|
||||||
|
File(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "git.html")]
|
||||||
|
pub struct GitMainTemplate<TS, BREADCRUMB>
|
||||||
|
where for <'a> &'a TS : IntoIterator<Item = &'a Entry>,
|
||||||
|
for <'a> &'a BREADCRUMB: IntoIterator<Item = &'a (String, String)>,
|
||||||
|
{
|
||||||
|
repo : Repository,
|
||||||
|
browse : TS,
|
||||||
|
query : GitWebQ,
|
||||||
|
breadcrumb: BREADCRUMB,
|
||||||
|
user_opt : Option<User>,
|
||||||
|
revisions_nbr : u32,
|
||||||
|
branches_nbr : u32,
|
||||||
|
tags_nbr : u32,
|
||||||
|
size : String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct GitWebQ {
|
||||||
|
commit: Option<String>,
|
||||||
|
path: Option<String>,
|
||||||
|
branch: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitWebQ {
|
||||||
|
fn clone_with_commit(&self, commit : Option<String>) -> GitWebQ {
|
||||||
|
let mut res = self.clone();
|
||||||
|
res.commit = commit;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_with_path(&self, path : Option<String>) -> GitWebQ {
|
||||||
|
let mut res = self.clone();
|
||||||
|
res.path = path;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_with_branch(&self, branch : Option<String>) -> GitWebQ {
|
||||||
|
let mut res = self.clone();
|
||||||
|
res.branch = branch;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_query(&self) -> GitWebQ {
|
||||||
|
self.clone_with_path(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_path(&self, name : &&String) -> GitWebQ {
|
||||||
|
match &self.path {
|
||||||
|
None => {self.clone_with_path(Some(name.to_string()))}
|
||||||
|
Some(path) => {if path.ends_with("/") {
|
||||||
|
self.clone_with_path(Some(path.clone() + name))
|
||||||
|
} else {
|
||||||
|
self.clone_with_path(Some(path.clone() + "/" + name))
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for GitWebQ{
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "?{}{}{}",
|
||||||
|
match &self.commit {
|
||||||
|
None => {"".to_string()}
|
||||||
|
Some(c) => {format!("commit={}", c)}
|
||||||
|
},
|
||||||
|
match &self.path {
|
||||||
|
None => {"".to_string()}
|
||||||
|
Some(p) => {format!("&path={}", p)}
|
||||||
|
},
|
||||||
|
match &self.branch {
|
||||||
|
None => {"".to_string()}
|
||||||
|
Some(b) => {format!("&branch={}", b)}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#[get("/git/{owner}/{repo}.git")]
|
||||||
|
pub async fn git_main<T : AuthValidator>(
|
||||||
|
web::Path((ownername, reponame)): web::Path<(String, String)>,
|
||||||
|
query : web::Query<GitWebQ>,
|
||||||
|
gitust : web::Data<Gitust>,
|
||||||
|
auth : Option<basic::BasicAuth>,
|
||||||
|
validator : web::Data<T>,
|
||||||
|
//auth : BasicAuth,
|
||||||
|
) -> Result<GitMainTemplate<Vec<Entry>, Vec<(String, String)>>, error::Error> {
|
||||||
|
// let authtorization = auth.ok_or(error::Error::Unauthorized("safe repo".to_string()))?;
|
||||||
|
//let authorization = auth.and_then(|cred| validator.check_basic(&cred)).ok_or(error::Error::Unauthorized("safe repo".to_string()))?;
|
||||||
|
let web::Query(query_struct) = query;
|
||||||
|
// let GitWebQ{commit : commitnameopt, path : pathopt, branch : branchopt} = query_struct;
|
||||||
|
let rootname = match &query_struct.path {
|
||||||
|
None => {"".to_string()}
|
||||||
|
Some(s) => {
|
||||||
|
if s.starts_with("/") {
|
||||||
|
(&s[1..]).to_string()
|
||||||
|
} else {
|
||||||
|
s.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let commit = match &query_struct.commit {
|
||||||
|
None => {match &query_struct.branch {
|
||||||
|
None => {GitRef::Head}
|
||||||
|
Some(s) => {GitRef::Branch(s.clone())}
|
||||||
|
}}
|
||||||
|
Some(s) => {GitRef::Commit(s.clone())}
|
||||||
|
};
|
||||||
|
let owner = Owner { name : ownername};
|
||||||
|
let repo = Repository {name : reponame, owner};
|
||||||
|
// let user = User { name : "Hubert".to_string()};
|
||||||
|
let user = auth.map(|auth| User{name : auth.user_id().to_string()});
|
||||||
|
// il faut ajouter le commit/branch dans la query
|
||||||
|
let path = rootname.split("/").map_accum(query_struct.clone_with_path(Some("".to_string())), |direname, b| {
|
||||||
|
let newpath = (&b.path).as_ref().map(|path| {path.clone() + "/" + direname});
|
||||||
|
let newb = b.clone_with_path(newpath);
|
||||||
|
((direname.to_string(), format!("{}", newb)), newb)
|
||||||
|
}).collect();
|
||||||
|
let gitrepo = GitRepo::new(format!("{}/{}/{}.git", &gitust.repo_root_path, &repo.owner.name, &repo.name).as_str())?;
|
||||||
|
let root = gitrepo.get_tree(&commit, Path::new(rootname.as_str()))?;
|
||||||
|
let browse = gitrepo.browse(&root).await?;
|
||||||
|
let mut entries : Vec<Entry> = Vec::new();
|
||||||
|
for entry in browse {
|
||||||
|
match entry {
|
||||||
|
GitBrowseEntry::EGitFile(GitFile(fullname)) => {
|
||||||
|
let osstr = fullname.file_name().ok_or(git2::Error::from_str("file name not defined"))?;
|
||||||
|
entries.push(Entry::File(format!("{}",osstr.to_string_lossy())));
|
||||||
|
}
|
||||||
|
GitBrowseEntry::EGitDir(GitDir{fullname, .. }) => {
|
||||||
|
let osstr = fullname.file_name().ok_or(git2::Error::from_str("file name not defined"))?;
|
||||||
|
entries.push(Entry::Dir(format!("{}",osstr.to_string_lossy())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(GitMainTemplate {
|
||||||
|
repo,
|
||||||
|
browse : entries,
|
||||||
|
query : query_struct,
|
||||||
|
breadcrumb: path,
|
||||||
|
user_opt : user,
|
||||||
|
revisions_nbr : gitrepo.get_revisions_nbr(),
|
||||||
|
branches_nbr : gitrepo.get_branches_nbr(),
|
||||||
|
tags_nbr : gitrepo.get_tags_nbr(),
|
||||||
|
size : "16 KiB".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
48
src/webutils/auth.rs
Normal file
48
src/webutils/auth.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use actix_session::Session;
|
||||||
|
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||||
|
|
||||||
|
pub trait AuthValidator {
|
||||||
|
fn check_user(&self, name : &String, pwd : &String) -> bool;
|
||||||
|
|
||||||
|
fn check_basic(&self, basic : &BasicAuth) -> Option<User> {
|
||||||
|
match basic.password() {
|
||||||
|
Option::Some(pwd) => {
|
||||||
|
let username = basic.user_id().to_string();
|
||||||
|
if self.check_user(&username, &pwd.to_string()) {
|
||||||
|
Some(User(username))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Option::None => {None}
|
||||||
|
}
|
||||||
|
//basic.password().and_then(|pwd| self.check_user(&basic.user_id().to_string(), &pwd.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_session(&self, session : &Session) -> Option<User> {
|
||||||
|
let result = session.get::<String>("user");
|
||||||
|
match result {
|
||||||
|
Ok(username) => {username.map(|u| User(u))}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestValidator;
|
||||||
|
|
||||||
|
impl AuthValidator for TestValidator {
|
||||||
|
fn check_user(&self, name: &String, pwd: &String) -> bool {
|
||||||
|
pwd.eq(&(name.clone() + "pwd"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct User(String);
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub fn get_name(&self) -> String {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/webutils/mod.rs
Normal file
2
src/webutils/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod auth;
|
||||||
|
pub mod user;
|
||||||
0
src/webutils/user.rs
Normal file
0
src/webutils/user.rs
Normal file
235
static/w3/w3.css
Normal file
235
static/w3/w3.css
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% when None %}
|
{% when None %}
|
||||||
<a href="#" class="w3-bar-item w3-button w3-right"><i class="fa fa-sign-in"></i> Log in</a>
|
<a href="?login=true" class="w3-bar-item w3-button w3-right"><i class="fa fa-sign-in"></i> Log in</a>
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
</div>
|
</div>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|||||||
@@ -26,16 +26,16 @@
|
|||||||
|
|
||||||
<div class="w3-panel w3-border w3-cell-row">
|
<div class="w3-panel w3-border w3-cell-row">
|
||||||
<div class="w3-center w3-cell">
|
<div class="w3-center w3-cell">
|
||||||
<i class="fa fa-history"></i> 112 revisions
|
<i class="fa fa-history"></i> {{ revisions_nbr }} revisions
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-center w3-cell">
|
<div class="w3-center w3-cell">
|
||||||
<i class="fa fa-code-branch"></i> 10 branches
|
<i class="fa fa-code-branch"></i> {{ branches_nbr }} branches
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-center w3-cell">
|
<div class="w3-center w3-cell">
|
||||||
<i class="fa fa-tag"></i> 5 tags
|
<i class="fa fa-tag"></i> {{ tags_nbr }} tags
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-center w3-cell">
|
<div class="w3-center w3-cell">
|
||||||
<i class="fa fa-database"></i> 10 KiB
|
<i class="fa fa-database"></i> {{ size }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
<div class="w3-cell-row">
|
<div class="w3-cell-row">
|
||||||
<div class="w3-cell">
|
<div class="w3-cell">
|
||||||
<ul class="breadcrumb">
|
<ul class="breadcrumb">
|
||||||
<li><a href="#">{{repo.name}}</a></li>
|
<li><a href="{{query.root_query()}}">{{repo.name}}.git</a></li>
|
||||||
{% for (dir, prev) in root %}
|
{% for (dir, prev) in breadcrumb %}
|
||||||
<li><a href="{{prev}}">{{dir}}</a></li>
|
<li><a href="{{prev}}">{{dir}}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -75,7 +75,12 @@
|
|||||||
|
|
||||||
<ul class="filebrowser">
|
<ul class="filebrowser">
|
||||||
{% for entry in browse %}
|
{% for entry in browse %}
|
||||||
<li><i class="fa fa-folder"></i> {{ entry }}</li>
|
{% match entry %}
|
||||||
|
{% when Entry::Dir with (name) %}
|
||||||
|
<li><i class="fa fa-folder"></i> <a href="{{query.add_path(name)}}">{{ name }}</a></li>
|
||||||
|
{% when Entry::File with (name) %}
|
||||||
|
<li><i class="fa fa-file"></i> {{ name }}</li>
|
||||||
|
{% endmatch %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user