~skye/git/src/act.rs
view raw
/*
* actions
*
* router fns, basically
*/
use axum::{
extract::{Path, Query},
response::{AppendHeaders, Html, IntoResponse, Redirect},
Form,
};
use axum_extra::{headers::Cookie, TypedHeader};
//use colored::Colorize;
use git2::{Repository, RepositoryInitOptions};
use html_escape::encode_text;
use http::header::CONTENT_TYPE;
use lazy_static::lazy_static;
use pwhash::bcrypt;
use regex::Regex;
use serde::Deserialize;
use tokio::time::Duration;
use crate::{
com::{self, basename, trim_left},
db::{self, db_err_fmt, Database, DbErr, Settings, User},
macros::*,
now, CFG,
};
use std::{
collections::HashMap,
fmt::Debug,
fs::{self, DirEntry, File},
io::{self, Read},
process::Command,
time::SystemTime,
};
/** unwrap a cookie to get a user
*
* ```
* $c: Option<TypedHeader<Cookie>>,
* $d: Database
* ``` */
//#[macro_export]
macro_rules! whoami {
( $c:expr, $d:expr ) => {{
if let Some(ref c) = $c {
let x: Option<&str> = c.get("s");
if let Some(s) = x {
match $d.get_session(s) {
Ok(s) => match $d.get_user_by_id(s.uid) {
Ok(x) => Ok(x),
Err(ref e) => {
err_fmt!("error getting user: {}", db_err_fmt(e))
}
},
Err(e) => err_fmt!("error getting session: {e}"),
}
} else {
err_fmt!("failed to retrieve session from cookie")
}
} else {
err_fmt!("failed to unwrap cookie")
}
}};
}
/** generate a table from the repositories owned by a user $u
*
* ```
* $u: User
* $d: Database
* ``` */
macro_rules! mkrepotable {
( $u:expr, $d:expr) => {{
let n = &$u.name;
match $d.get_user_repos($u.id) {
Ok(x) if x.len() > 0 => x
.iter()
.map(|y| {
format!(
r#"
<tr>
<td>
<a href="/{u}">{u}</a>/<a href="/{u}/{n}">{n}</a>
</td>
<td>{d}</td>
</tr>
"#,
u = format!("~{}", encode_text(&n)),
n = encode_text(&y.name),
d = encode_text(&y.desc)
)
})
.collect::<String>(),
/* the list is empty */
Ok(_) => String::from("<tr><td>no repos yet!</td></tr>"),
Err(ref e) => {
let f = db_err_fmt(e);
format!("error getting repos: {f}")
}
}
}};
}
macro_rules! mkpinned {
( $u:expr, $d:expr) => {{
match $d.get_pinned_repos($u.id) {
Ok(x) if x.len() > 0 => x
.iter()
.map(|y| {
format!(
r#"
<tr>
<td>
<a href="/{u}">{u}</a>/<a href="/{u}/{n}">{n}</a>
</td>
<td>{d}</td>
</td>
"#,
u = format!("~{}", encode_text(&$u.name)),
n = encode_text(&y.name),
d = encode_text(&y.desc)
)
})
.collect::<String>(),
Ok(_) => String::new(),
Err(ref e) => format!("error getting repos: {e:?}"),
}
}};
}
/** log the page when it's loaded */
macro_rules! log_page {
($x:expr) => {{
log!("{} {}", "loaded page".yellow().bold(), $x);
}};
}
type HtmlResult<T> = Result<T, Html<String>>;
/** home page */
pub async fn index(c: Option<TypedHeader<Cookie>>) -> HtmlResult<Html<String>> {
log_page!("index");
match Database::new("run/git.db") {
Ok(db) => {
let stranger = page_fmt! {
("home"),
(
r#"
<h1>badboy git</h1>
<p>
lightweight git hosting for
<a href="http://badboy.institute">
badboy.institute
</a>.
</p>
<p>
you aren't logged in.
do you want to register or log in?
</p>
<form method="post" action="/register">
<table>
<tr>
<td>name</td>
<td>
<input type="text" name="name">
</td>
</tr>
<tr>
<td>password</td>
<td>
<input
type="password"
name="pass"
>
</td>
</tr>
</table>
<input type="submit" value="register">
</form>
<br>
<form method="post" action="/login">
<table>
<tr>
<td>name</td>
<td>
<input
type="text"
name="name"
>
</td>
</tr>
<tr>
<td>password</td>
<td>
<input
type="password"
name="pass"
>
</td>
</tr>
</table>
<input type="submit" value="login">
</form>
<h3>repos on this server</h3>
<table>
{rt}
</table>
"#,
rt = {
match db.get_users() {
Ok(v) => v
.into_iter()
.map(|u| mkrepotable!(u, db))
.collect::<Vec<String>>()
.join("\n"),
Err(DbErr::UserNotFound(e)) => {
elog!("failed getting user: {e}");
String::new()
}
Err(DbErr::QueryFailed(e)) => {
elog!("failed getting user: {e}");
String::new()
}
}
}
)
};
/* display dashboard if we're logged in,
* otherwise login forms */
match whoami!(c, db) {
Ok(u) => {
let n = u.name.clone();
let r = mkrepotable!(u, db);
Ok(page_fmt! {
("home :: {}", encode_text(&n)),
(
r#"
<h1>
hello,
<a href="/{u}">{u}</a>
</h1>
<h3>new repository</h3>
<form method="post" action="/create">
<table>
<tr>
<td>name</td>
<td>
<input
type="text"
name="name"
>
</td>
</tr>
<tr>
<td>description</td>
<td>
<input
type="text"
name="desc"
>
</td>
</tr>
</table>
<input type="submit" value="create">
</form>
<h3>repositories</h3>
<table>
{r}
</table>
"#,
u = encode_text(&format!("~{n}"))
)
})
}
Err(_) => Ok(stranger),
}
}
Err(e) => err_fmt_html!("failed to open database: {e}"),
}
}
/** view a user's profile */
pub async fn view_user(
Path(n): Path<String>,
c: Option<TypedHeader<Cookie>>,
) -> HtmlResult<Html<String>> {
log_page!("view_user");
match Database::new("run/git.db") {
Ok(db) => match db.get_user(&n) {
Ok(u) => Ok(page_fmt! {
("{}", &n),
(
r#"
<h1>~{n}</h1>
<h3>repositories</h3>
{pin}
<h3>pinned repos</h3>
<table>{p}</table>
<hr>
<h3>all repos</h3>
<table>{t}</table>
"#,
pin = match whoami!(c, db) {
Ok(x) => if &u.name == &x.name {
fmt!(
r#"
<h3>pin a repository</h3>
<form action="/pin" method="post">
<table>
<tr>
<td>
<input type="text" name="name">
</td>
<td>
<input
type="submit"
value="pin"
>
</td>
</tr>
</table>
<input
type="hidden"
name="uid"
value="{u}"
>
</form>
"#,
u = x.id
)
} else {
String::new()
},
Err(_) => String::new()
},
p = mkpinned!(u, db),
t = mkrepotable!(u, db)
)
}),
Err(DbErr::UserNotFound(e)) => {
err_fmt_html!("error getting user: {e}")
}
Err(DbErr::QueryFailed(e)) => {
err_fmt_html!("error getting user: {e}")
}
},
Err(e) => err_fmt_html!("failed to open database: {e}"),
}
}
/** get the path of a file in a repo.
* n: username, r: repo name, p: path
*
* returns: Result<(path (String), Database, Repo, User)>
*/
macro_rules! get_repo_file {
($n:expr, $r:expr, $p:expr) => {{
match Database::new("run/git.db") {
Ok(db) => {
match db.get_user($n) {
Ok(u) => {
/* repo path */
let rp = mkrepopath!(&u.name, $r);
/* repo struct */
let rs = match db.get_repo(u.id, $r) {
Ok(x) => x,
Err(e) => {
let r = repo_name($n, $r);
return err_fmt_into!(
"repo {r} not found: {e}"
);
}
};
Ok((format!("{rp}.full/{}", $p), db, rs, u))
}
Err(ref e) => {
let f = db_err_fmt(e);
err_fmt!("failed to get user: {f}")
}
}
}
Err(e) => err_fmt!("failed to open database: {e}"),
}
}};
}
/** get the path of a repo
* n: username, r: repo name
*
* returns: Result<(path (String), Database, Repo, User)>
*/
macro_rules! get_repo {
($n:expr, $r:expr) => {{
match Database::new("run/git.db") {
Ok(db) => {
match db.get_user($n) {
Ok(u) => {
/* repo path */
let rp = mkrepopath!(&u.name, $r);
/* repo struct */
let rs = match db.get_repo(u.id, $r) {
Ok(x) => x,
Err(e) => {
let r = repo_name($n, $r);
return {
let f = format!("repo {r} not found: {e}");
Err(f.into())
};
}
};
Ok((format!("{rp}.full"), db, rs, u))
}
Err(ref e) => {
let f = db_err_fmt(e);
err_fmt!("error getting user: {f}")
}
}
}
Err(e) => err_fmt!("failed to open database: {e}"),
}
}};
}
/** view a repo's index */
pub async fn view_src_index(
Path((n, r)): Path<(String, String)>,
) -> HtmlResult<Html<String>> {
log_page!("view_src_index");
/* just redirect to view the base dir */
view_src(Path((n, r, "".to_string()))).await
}
lazy_static! {
/** regex to extract paths in view_src.
* global var just so we don't have to use a bunch of space in the fn */
static ref PATH_REGEX: String =
format!(
"/home/(?<u>{s})/(?<r>{s})\\.full/(?<p>[a-zA-Z0-9\\.\\/-]*)$",
/* match a string part of a path */
s = "[a-zA-Z0-9\\.-_]+"
);
}
/** format a repo name */
fn repo_name<T>(u: T, r: T) -> String
where
String: From<T>,
{
let (u, r) = (String::from(u), String::from(r));
format!("~{u}/{r}")
}
/** format a repo name into html with links */
fn repo_name_html<T>(u: T, r: T) -> String
where
String: From<T>,
{
let (u, r) = (format!("~{}", encode_text(str_from!(u))), String::from(r));
format!(
r#"<a href="/{u}">{u}</a>/<a href="/{u}/{r}">{r}</a>"#,
r = encode_text(&r),
u = encode_text(&u),
)
}
/** view source of a repo */
pub async fn view_src(
Path((n, r, p)): Path<(String, String, String)>,
) -> HtmlResult<Html<String>> {
log_page!("view_src");
match get_repo_file!(&n, &r, &p) {
Ok((f, _db, _rs, _u)) => {
/* username with the tilde */
let u = format!("~{}", encode_text(&n));
/* split the path and add the links to each file */
let ps =
p.split("/").map(|x| String::from(x)).scan(vec![], |x, y| {
x.push(y);
Some(x.clone())
});
/* the path encoded with html */
let hp = ps
.map(|x| {
let mut v = Vec::new();
for i in (0..x.len()).into_iter() {
/* create the link for each file
* in the path */
let a = (&x[0..i + 1])
.into_iter()
.map(|_| {
format!(
r#"<a href="/{u}/{r}/src/{p}">{f}</a>"#,
r = encode_text(&r),
f = x[x.len() - 1],
p = (&x[0..i + 1]).join("/")
)
})
.collect::<Vec<String>>();
v.push(a[a.len() - 1].clone());
}
v[v.len() - 1].clone()
})
.collect::<Vec<String>>()
.join("/");
/* title */
let t = fmt!("{}/{}", repo_name(&n, &r), encode_text(&p));
/* encodede html title */
let ht = format!("{}/{hp}", repo_name_html(&n, &r));
let path = std::path::Path::new(&f);
if path.is_file() {
/* link to raw file */
let raw = format!(
"/~{}/{}/raw/{}",
encode_text(&n),
encode_text(&r),
encode_text(&p)
);
match fs::read_to_string(f) {
Ok(c) => Ok(page_fmt! {
("{t} :: view file"),
(
r#"
<p>{ht}</p>
<p><a href="{raw}">view raw</a></p>
<hr>
<pre>{c}</pre>
"#,
c = encode_text(&c),
)
}),
Err(e) => err_fmt_html!("failed to read file: {e}"),
}
} else if path.is_dir() {
log!("displaying dir ~{n}/{r}/{p}: {f}");
/* dir */
let d = match fs::read_dir(&f) {
Ok(x) => x,
Err(e) => {
return err_fmt_html!("failed to read dir: {e}");
}
};
/* file list */
let fl = d
/* git rid of .git */
.filter(|x| match x {
Ok(x) => match x.path().as_os_str().to_str() {
Some(x) => basename(x) != ".git",
None => false,
},
Err(e) => {
elog!("error getting file: {e}");
false
}
})
/* get the file <tr> */
.map(|x| match display_file(&n, &r, x) {
Ok(x) => x,
Err(e) => {
format!("<p>file err: {}</p>", encode_text(&e))
}
})
.collect::<Vec<String>>()
.join("\n");
Ok(page_fmt! {
("{t} :: view directory"),
(r#"
{ht}
<hr>
<table>
{fl}
</table>
"#)
})
} else {
err_fmt_html!("~{n}/{r}/{p} is an unsupported file type")
}
}
Err(e) => err_fmt_html!("failed to get file: {e}"),
}
}
/** redirect via headers for typing reasons */
fn redirect<T>(x: T) -> impl IntoResponse
where
String: From<T>,
{
let x = String::from(x);
log!("redirecting to {x}");
Redirect::to(&x)
}
/** format a redirect */
macro_rules! redirect_fmt {
($($t:tt)*) => {{
redirect(format!($($t)*))
}};
}
/** display a file, for use in view_src */
fn display_file(
n: &str,
r: &str,
x: io::Result<DirEntry>,
) -> Result<String, String> {
match x {
Ok(x) => {
/* shadowing because fuck you */
let p = x.path();
let p = p.as_os_str().to_str();
let p = match p {
Some(x) => x,
None => "",
};
/* file path */
let q = match Regex::new(&PATH_REGEX) {
Ok(r) => {
let c = match r.captures(p) {
Some(x) => x,
None => {
return err_fmt!("failed to capture regex for {p}")
}
};
String::from(&c["p"])
}
Err(e) => return err_fmt!("failed to mk regex: {e}"),
};
/* file */
let f = trim_left(basename(p), '/');
/* href */
let h = com::trim_right(
format!("/{}/src/{}", repo_name(n, r), encode_text(&q)),
'/',
);
/* link (<a>) */
let a = format!(
r#"<a href="{h}">{f}</a>"#,
f = encode_text(&f),
h = encode_text(&h)
);
ok_fmt!("<tr><td>{a}</td></tr>")
}
Err(e) => err_fmt!("{e}"),
}
}
/** view a raw file */
pub async fn view_raw(
Path((n, r, p)): Path<(String, String, String)>,
) -> HtmlResult<impl IntoResponse> {
log_page!("view_raw");
match get_repo_file!(&n, &r, &p) {
Ok((f, _, _, _)) => {
let path = std::path::Path::new(&f);
Ok(if path.is_file() {
redirect_fmt!("/~{}/{}/show/{}", &n, &r, &p)
} else {
redirect_fmt!("/~{}/{}/src/{}", &n, &r, &p)
})
}
Err(e) => {
err_fmt_html!("failed to read file {n}/{r}/{p}: {e}")
}
}
}
/** show a file as raw text
* this is its own function separate from view_raw for typing reasons
* view_raw has to return a redirect, so we just send a form and display the
* file that way */
pub async fn show_raw(
Path((n, r, p)): Path<(String, String, String)>,
c: Option<TypedHeader<Cookie>>,
) -> HtmlResult<impl IntoResponse> {
log_page!("show_raw");
match get_repo_file!(&n, &r, &p) {
Ok((f, mut db, rs, u)) => {
let path = std::path::Path::new(&f);
if path.is_file() {
dbg_log!("displaying raw file: {f}");
/* repo name */
let rn = repo_name(&n, &r);
/* error for if it's private */
let prv = err_fmt_html!("{rn} is private");
/* the file content */
let fc = match fs::read_to_string(f) {
Ok(c) => c,
Err(e) => return err_fmt_html!("error reading file: {e}"),
};
/* the redirect */
let to = Ok((
AppendHeaders([(CONTENT_TYPE, "text/plain".to_string())]),
fc,
));
/* settings */
let s = match db.get_settings(rs.id) {
Ok(s) => s,
Err(e) => {
elog!("failed to get settings for {rn}: {e}");
Settings::new(rs.id)
}
};
if com::i64_to_bool(s.private) {
let me = match whoami!(c, db) {
Ok(x) => x,
Err(e) => {
return err_fmt_html!("err getting login: {e}")
}
};
if me.name == u.name {
log!("{rn}: displaying private repo");
to
} else {
prv
}
} else {
log!("{rn}: repo isn't private");
to
}
} else {
err_fmt_html!("user {n} not found")
}
}
Err(e) => err_fmt_html!("error reading file {n}/{r}/{p}: {e}"),
}
}
/** the admin panel */
pub async fn admin(
Path((n, r)): Path<(String, String)>,
c: Option<TypedHeader<Cookie>>,
) -> HtmlResult<Html<String>> {
log_page!("admin");
match get_repo!(&n, &r) {
Ok((_f, mut db, rs, u)) => {
/* settings, is default */
let (s, _is_def) = match db.get_settings(rs.id) {
Ok(x) => (x, false),
Err(e) => {
elog!(
"{}: error getting settings: {e}",
"error getting repo".red().bold()
);
(Settings::new(rs.id), true)
}
};
match whoami!(c, db) {
Ok(me) => {
if &me.name == &u.name {
/* repo name */
let rn =
format!("~{}/{}", encode_text(&n), encode_text(&r));
Ok(page_fmt! {
("admin panel :: {rn}"),
(
r#"
<h1>
admin panel
:: <a href="/{rn}">{rn}</a>
</h1>
<div id="settings">
<h3>settings</h3>
<form action="/settings" method="post">
<table>
<tr>
<td>private?</td>
<td>{p:?}</td>
<td><input
type="checkbox"
name="private"
{prv}
></td>
</tr>
</table>
<input
type="hidden"
name="repo"
value="{r}"
>
<input
type="submit"
value="submit"
>
</form>
</div>
"#,
/* is private? */
prv = if s.private == 0 {
""
} else {
r#"checked="checked""#
},
p = {
let r = s.private;
log!(
"private?: r: {r:?}: to bool: {}",
com::i64_to_bool(r)
);
com::i64_to_bool(r)
},
r = encode_text(&r),
)
})
} else {
err_fmt_html!("you don't own this repository")
}
}
Err(e) => {
err_fmt_html!("failed to get user info: {e}")
}
}
}
Err(e) => err_fmt_html!("error getting repo: {e}"),
}
}
/** do we have permission to view a repo? */
fn has_permission(
c: &Option<TypedHeader<Cookie>>,
db: &mut Database,
u: &User,
i: i64,
) -> Result<bool, String> {
match db.repo_is_private(i) {
Ok(true) => match whoami!(c, db) {
Ok(me) => Ok(me.id == u.id),
Err(_e) => Ok(false),
},
Ok(false) => Ok(true),
Err(e) => err_fmt!("failed to query db: {e}")
}
}
/** view repo page */
pub async fn view_repo(
Path((n, r)): Path<(String, String)>,
c: Option<TypedHeader<Cookie>>,
) -> HtmlResult<Html<String>> {
log_page!("view_repo");
match Database::new("run/git.db") {
Ok(mut db) => {
match (&mut db).get_user(&n) {
Ok(u) => {
let _me = match whoami!(c, db) {
Ok(x) => Some(x),
Err(e) => {
elog!("whoami! returned err: {e}");
None
}
};
/* repo struct */
let rs = match db.get_repo(u.id, &r) {
Ok(x) => x,
Err(e) => {
return err_fmt_html!("failed to get repo: {e}")
}
};
match has_permission(&c, &mut db, &u, rs.id) {
Ok(x) if !x => {
return err_fmt_html!("permission denied")
}
Ok(_) => (),
Err(e) => {
return err_fmt_html!("failed to check perms: {e}")
}
}
/* repo path */
let rp = mkrepopath!(n, r);
/* the repo itself */
let repo = match Repository::open_bare(rp) {
Ok(x) => x,
Err(e) => {
return err_fmt_html!("failed to open repo: {e}")
}
};
/* repo name */
let rn = format!(
r#"<a href="/~{n}">~{n}</a>/{r}"#,
n = encode_text(&n),
r = encode_text(&r)
);
/* readme */
let f = format!("{}.full/README", mkrepopath!(n, r));
let rm = match File::open(f.clone()) {
Ok(mut f) => {
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => s,
Err(e) => {
dbg_elog!("error reading {f:?}: {e}");
String::new()
}
}
}
Err(e) => {
dbg_elog!("error opening {f}: {e}");
String::new()
}
};
/* read the config
* it's ok to unwrap because we know the config would
* have already caused a crash */
let addr = &CFG["git_addr"].as_str().unwrap();
let port = &CFG["git_port"].as_str().unwrap();
/* branches */
let b = match repo.branches(None) {
Ok(x) => x
.map(|x| match x {
Ok(x) => match x.0.name() {
Ok(n) => {
if let Some(n) = n {
encode_fmt!("{}", &n)
} else {
"branch name is None".to_string()
}
}
Err(e) => {
fmt!("branch name err: {e}")
}
},
Err(e) => {
fmt!("failed to get branch: {e}")
}
})
.collect::<Vec<_>>()
.join("<br>"),
Err(e) => format!("no repos found: {e}"),
};
log!("&r: {r}");
Ok(page_fmt! {
("{}", repo_name(&n, &r)),
(
r#"
<h1>{rn}</h1>
<p>{rd}</p>
<h3>cloning</h3>
<table class="clone">
<tr>
<td>read only</td>
<td>
git://{a}/~{n}/{r}
</td>
</tr>
<tr>
<td>read/write</td>
<td>
ssh://{n}@{a}:{p}/~{n}/{r}
</td>
</tr>
</table>
<p>
<a href="/~{n}/{r}/src">
view source
</a>
</p>
<p>{admin}</p>
<hr>
<pre id="readme">{rm}</pre>
<h3>branches</h3>
<p>{b}</p>
"#,
rd = encode_text(&rs.desc),
rm = encode_text(&rm),
n = encode_text(&n),
r = encode_text(&r),
a = addr,
p = port,
/* admin panel */
admin = match whoami!(c, db) {
Ok(me) =>
if &me.name == &u.name {
format!(
r#"
<a href="/{}/admin">
admin panel
</a>
"#,
repo_name(&n, &r)
)
} else {
String::new()
},
Err(_e) => {
String::new()
}
}
)
})
}
Err(DbErr::UserNotFound(e)) => {
err_fmt_html!("user not found: {e}")
}
Err(DbErr::QueryFailed(e)) => {
err_fmt_html!("failed to query database: {e}")
}
}
}
Err(e) => err_fmt_html!("failed to open database: {e}"),
}
}
/** display an error */
pub async fn err(Query(q): Query<HashMap<String, String>>) -> Html<String> {
log_page!("err");
let m = match q.get("m") {
Some(x) => x,
None => "",
};
let goto = match q.get("goto") {
Some(x) => x,
None => "/",
};
page_fmt! {
("error :: {m}"),
(
r#"
<h1>{m}</h1>
<p><a href="{goto}">go back</a>.</p>
"#
)
}
}
#[derive(Deserialize, Debug)]
pub struct UserForm {
pub name: String,
pub pass: String,
}
/** duh */
pub async fn register(
Form(f): Form<UserForm>
) -> HtmlResult<impl IntoResponse> {
log_page!("register");
let db = match Database::new("run/git.db") {
Ok(x) => x,
Err(e) => return err_fmt_html!("failed getting database: {e}"),
};
let n = &f.name;
match db.add_user(n, &f.pass) {
Ok(_) => Ok(redirect_fmt!("/")),
Err(e) => err_fmt_html!("error adding user: {e}"),
}
}
pub async fn login(Form(f): Form<UserForm>) -> HtmlResult<impl IntoResponse> {
log_page!("login");
if let Ok(db) = Database::new("run/git.db") {
let n = &f.name;
let u = match db.get_user(n) {
Ok(x) => x,
Err(ref e) => {
let f = db_err_fmt(e);
return err_fmt_html!("{f}");
}
};
log!("logging in user: {:?}", u);
if bcrypt::verify(f.pass, &u.hash) {
let h = match db.add_session(u.id) {
Ok(x) => String::from(x),
Err(e) => {
return err_fmt_html!("failed to add session: {e}");
}
};
dbg_log!("created hash: {h}");
Ok((
AppendHeaders([(
"Set-Cookie",
format!(
"s={}; SameSite=Strict; Expires={}; Max-Age=172800; HttpOnly",
h,
httpdate::fmt_http_date(SystemTime::now() + Duration::from_secs(172800))
),
)]),
Redirect::to("/"),
))
} else {
err_fmt_html!("incorrect password")
}
} else {
err_fmt_html!("failed to open database")
}
}
#[derive(Deserialize, Debug)]
pub struct SettingsForm {
pub repo: String,
pub private: Option<String>,
}
/** modify a repo's settings */
pub async fn settings(
c: Option<TypedHeader<Cookie>>,
Form(f): Form<SettingsForm>,
) -> HtmlResult<impl IntoResponse> {
log_page!("settings");
match Database::new("run/git.db") {
Ok(mut db) => {
let me = match whoami!(c, db) {
Ok(x) => x,
Err(e) => return err_fmt_html!("error getting login: {e}"),
};
/* repo struct */
let rs = match db.get_repo(me.id, &f.repo) {
Ok(x) => x,
Err(e) => {
return err_fmt_html!("failed to get repo from db: {e}")
}
};
/* old settings */
let _s = match db.get_settings(rs.settings) {
Ok(x) => x,
Err(_) => db.new_settings(rs.id)?,
};
/* make the new settings */
let s = Settings {
id: _s.id,
private: com::bool_to_i64(if let Some(p) = f.private {
log!("p: {p}");
if p == "checked" || p == "on" {
true
} else {
return err_fmt_html!(
"private value is not checked or on: {p}"
);
}
} else {
false
}),
oid: rs.id,
};
match db.set_settings(s) {
Ok(_) => Ok(redirect_fmt!("/~{}/{}/admin", &me.name, &rs.name)),
Err(e) => err_fmt_html!("failed to set settings: {e}"),
}
}
Err(e) => err_fmt_html!("failed to open database: {e}"),
}
}
/** actually run the code to make the repo */
fn mkrepo<T>(n: &T, d: &T, u: &db::User) -> HtmlResult<impl IntoResponse>
where
String: From<T>,
T: Clone,
{
/* lol! */
let (n, d) = (String::from(n.clone()), String::from(d.clone()));
let r = format!("/home/{}/{n}", u.name);
/* copy hooks */
for x in ["post-update", "post-receive"].iter() {
let f = format!("etc/{x}.sample");
let p = format!("{r}/hooks/{x}");
match fs::copy(&f, &p) {
Ok(_) => log!("successfully copied hooks"),
Err(e) => {
elog!("failed to copy git hooks!!: {e}");
return err_fmt_html!(
r#"
somehow, we were unable to copy the files
required to create your repository.
delete it and try again.
<br>
error message: {e}
"#
);
}
};
match Command::new("chmod").arg("a+x").arg(&p).output() {
Ok(_) => (),
Err(e) => {
elog!("failed to chmod {p}");
return err_fmt_html!(
r#"
we were unable to change ownership of your
repository files. delete this repository and
create it again.
<br>
error message: {e}
"#
);
}
};
}
/* should we just pass the db as an arg? */
let db = match Database::new("run/git.db") {
Ok(x) => x,
Err(e) => return err_fmt_html!("failed to open database: {e}"),
};
match db.add_repo(&n, &d, u.id) {
Ok(_) => {
match Command::new("etc/chown-repo.sh")
.arg(&r)
.arg(&u.name)
.output()
{
Ok(_) => Ok(Redirect::to("/")),
Err(e) => {
err_fmt_html!("failed to own repository: {e}")
}
}
}
Err(e) => err_fmt_html!("failed to add repo: {e}"),
}
}
#[derive(Deserialize, Debug)]
pub struct Pin {
name: String,
uid: i64,
}
/** pin a repository to your profile */
pub async fn pin(
c: Option<TypedHeader<Cookie>>,
Form(f): Form<Pin>,
) -> HtmlResult<impl IntoResponse> {
log_page!("pin");
match Database::new("run/git.db") {
Ok(db) => match whoami!(c, db) {
Ok(me) => {
if me.id == f.uid {
match db.get_repo(f.uid, &f.name) {
Ok(r) => match db.add_pin(me.id, r.id) {
Ok(_) => Ok(redirect_fmt!("/~{}", me.name)),
Err(e) => {
err_fmt_html!("failed to add pin: {e}")
}
},
Err(e) => err_fmt_html!("repo not found: {e}"),
}
} else {
err_fmt_html!("you don't own this repository")
}
}
Err(e) => err_fmt_html!("not logged in: {e}"),
},
Err(e) => err_fmt_html!("failed to open database: {e}"),
}
}
#[derive(Deserialize, Debug)]
pub struct Create {
name: String,
desc: String,
}
/** create a new repo; post request */
pub async fn create(
c: Option<TypedHeader<Cookie>>,
Form(f): Form<Create>,
) -> HtmlResult<impl IntoResponse> {
log_page!("create");
if let Some(c) = c {
if let Some(s) = c.get("s") {
if let Ok(db) = Database::new("run/git.db") {
match db.get_session(s) {
Ok(sesh) => {
let uid = sesh.uid;
match db.get_user_by_id(uid) {
Ok(u) => {
if !db.repo_exists(u.id, &f.name) {
let n = &f.name;
let d = &f.desc;
let p = &mkrepopath!(&u.name, n);
match Repository::init_opts(
p,
RepositoryInitOptions::new()
.bare(true)
.mkdir(true)
.no_reinit(true)
.description(d),
) {
Ok(_) => {
let n = n.to_string();
let d = d.to_string();
mkrepo(&n, &d, &u)
}
Err(e) => {
err_fmt_html!(
"failed to git init: {e}"
)
}
}
} else {
err_fmt_html!("repo already exists")
}
}
Err(DbErr::UserNotFound(e)) => {
err_fmt_html!("{e}")
}
Err(DbErr::QueryFailed(e)) => {
err_fmt_html!("{e}")
}
}
}
Err(e) => err_fmt_html!("failed to fetch session: {e}"),
}
} else {
err_fmt_html!("failed to open database")
}
} else {
err_fmt_html!("not logged in")
}
} else {
err_fmt_html!("not logged in")
}
}