~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")
    }
}