git repos / sago_web_git

src/application.cpp

browsing at commit = 7df4218f1bb7efd0913733a5ae4f641268f5e8af

raw · blame · history

#include "application.h"

#include "content.h"
#include "git_repo.h"
#include "repo_scanner.h"
#include "util.h"

#include <cppcms/http_request.h>
#include <cppcms/http_response.h>
#include <cppcms/service.h>
#include <cppcms/url_dispatcher.h>

#include <fstream>

namespace {
constexpr int LOG_LIMIT = 200;
const std::string SKIN = "git";

// Minimal content-type table for the assets we ship.
std::string mime_for(const std::string &path) {
    const size_t dot = path.find_last_of('.');
    const std::string ext = dot == std::string::npos ? "" : path.substr(dot + 1);
    if (ext == "css") return "text/css";
    if (ext == "js") return "application/javascript";
    if (ext == "svg") return "image/svg+xml";
    if (ext == "png") return "image/png";
    if (ext == "ico") return "image/x-icon";
    return "application/octet-stream";
}
} // namespace

GitApp::GitApp(cppcms::service &srv, const RepoScanner *scanner)
    : cppcms::application(srv), scanner_(scanner) {
    static_root_ = settings().get<std::string>("git.static_root", "./static");

    // Root arrives as "" because script_name "/" is stripped; accept both.
    dispatcher().assign("^/?$", &GitApp::frontpage, this);
    dispatcher().assign("^/static/(.+)$", &GitApp::static_file, this, 1);
    dispatcher().assign("^/repos/?$", &GitApp::repos_list, this);
    dispatcher().assign("^/repos/([^/]+)/tree/?(.*)$", &GitApp::tree, this, 1, 2);
    dispatcher().assign("^/repos/([^/]+)/raw/(.+)$", &GitApp::raw, this, 1, 2);
    dispatcher().assign("^/repos/([^/]+)/blame/(.+)$", &GitApp::blame, this, 1, 2);
    dispatcher().assign("^/repos/([^/]+)/object/([0-9a-f]+)$", &GitApp::object, this, 1, 2);
    dispatcher().assign("^/repos/([^/]+)/log/?$", &GitApp::log, this, 1);
    dispatcher().assign("^/repos/([^/]+)/commit/([0-9a-f]+)\\.patch$", &GitApp::commit_patch, this, 1, 2);
    dispatcher().assign("^/repos/([^/]+)/commit/([0-9a-f]+)$", &GitApp::commit, this, 1, 2);
    dispatcher().assign("^/repos/([^/]+)/tags/?$", &GitApp::tags, this, 1);
    dispatcher().assign("^/repos/([^/]+)/tags/(.+)$", &GitApp::tag, this, 1, 2);
    dispatcher().assign("^/repos/([^/]+)/?$", &GitApp::repo_home, this, 1);
}

void GitApp::main(std::string url) {
    response().content_type("text/html; charset=utf-8");
    cppcms::application::main(url);
}

void GitApp::static_file(std::string rel) {
    if (rel.find("..") != std::string::npos) {
        show_error(403, "Forbidden");
        return;
    }
    const std::string full = static_root_ + "/" + rel;
    std::ifstream file(full, std::ios::binary);
    if (!file) {
        show_error(404, "Not found: " + rel);
        return;
    }
    response().content_type(mime_for(rel));
    response().out() << file.rdbuf();
}

std::unique_ptr<GitRepo> GitApp::open(const std::string &name) {
    const std::string path = scanner_->path_for(name);
    if (path.empty()) {
        show_error(404, "No such repository: " + name);
        return nullptr;
    }
    try {
        return std::make_unique<GitRepo>(path);
    } catch (const GitError &e) {
        show_error(500, e.what());
        return nullptr;
    }
}

std::string GitApp::commit_from_request(const GitRepo &repo) {
    const std::string id = request().get("id");
    if (id.empty())
        return repo.resolve_commit("");
    if (!util::is_hex_oid(id))
        throw GitError("invalid id parameter");
    return repo.resolve_commit(id);
}

void GitApp::show_error(int code, const std::string &message) {
    response().status(code);
    response().content_type("text/plain; charset=utf-8");
    response().out() << message << "\n";
}

void GitApp::frontpage() {
    content::frontpage c;
    c.title = "Multi Git Frontend";
    c.repo_count = scanner_->names().size();
    render(SKIN, "frontpage", c);
}

void GitApp::repos_list() {
    content::repo_list c;
    c.title = "Repositories";
    c.repos = scanner_->names();
    render(SKIN, "repo_list", c);
}

void GitApp::repo_home(std::string name) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        content::repo_home c;
        c.title = name;
        c.repo = name;
        c.commit_oid = commit_from_request(*repo);
        c.query = request().get("id").empty() ? "" : "?id=" + c.commit_oid;
        c.default_branch = repo->default_branch();
        c.readme_name = repo->find_readme(c.commit_oid);
        if (!c.readme_name.empty()) {
            bool is_binary = false;
            repo->read_file(c.commit_oid, c.readme_name, c.readme_text, is_binary);
            if (is_binary)
                c.readme_text.clear();
        }
        render(SKIN, "repo_home", c);
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}

void GitApp::tree(std::string name, std::string path) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        const std::string commit = commit_from_request(*repo);
        const std::string query = request().get("id").empty() ? "" : "?id=" + commit;
        const std::string clean = util::trim_slashes(path);

        // A path pointing at a file shows the file; otherwise it is a directory.
        std::string text;
        bool is_binary = false;
        if (!clean.empty() && repo->read_file(commit, clean, text, is_binary)) {
            content::file_view c;
            c.title = name + "/" + clean;
            c.repo = name;
            c.query = query;
            c.commit_oid = commit;
            c.path = clean;
            c.filename = util::basename(clean);
            c.is_binary = is_binary;
            c.is_image = !util::image_mime_type(clean).empty();
            c.is_markdown = !is_binary && !c.is_image && util::is_markdown_path(clean);
            // Binary images carry no displayable source; text images (SVG) keep
            // their source so it can be shown below the preview.
            c.text = is_binary ? "" : text;
            c.size = text.size();
            render(SKIN, "file_view", c);
            return;
        }

        content::tree_view c;
        c.title = name + (clean.empty() ? "" : "/" + clean);
        c.repo = name;
        c.query = query;
        c.commit_oid = commit;
        c.path = clean;
        c.is_root = clean.empty();
        c.parent_path = util::parent_path(clean);
        c.entries = repo->list_tree(commit, clean);
        render(SKIN, "tree_view", c);
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}

void GitApp::raw(std::string name, std::string path) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        const std::string commit = commit_from_request(*repo);
        const std::string clean = util::trim_slashes(path);
        std::string content;
        bool is_binary = false;
        if (!repo->read_file(commit, clean, content, is_binary)) {
            show_error(404, "No such file: " + path);
            return;
        }
        const std::string image_mime = util::image_mime_type(clean);
        if (!image_mime.empty())
            response().content_type(image_mime);
        else
            response().content_type(is_binary ? "application/octet-stream"
                                               : "text/plain; charset=utf-8");
        response().out().write(content.data(), static_cast<std::streamsize>(content.size()));
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}

void GitApp::blame(std::string name, std::string path) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        const std::string commit = commit_from_request(*repo);
        content::blame_view c;
        c.repo = name;
        c.query = request().get("id").empty() ? "" : "?id=" + commit;
        c.commit_oid = commit;
        c.path = util::trim_slashes(path);
        c.filename = util::basename(c.path);
        c.title = "blame " + name + "/" + c.path;
        c.lines = repo->blame(commit, c.path);
        render(SKIN, "blame_view", c);
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}

void GitApp::log(std::string name) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        const std::string commit = commit_from_request(*repo);
        content::log_view c;
        c.repo = name;
        c.query = request().get("id").empty() ? "" : "?id=" + commit;
        c.commit_oid = commit;
        c.title = "log " + name;
        c.commits = repo->log(commit, LOG_LIMIT);
        render(SKIN, "log_view", c);
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}

void GitApp::object(std::string name, std::string oid) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        content::object_view c;
        c.repo = name;
        c.title = "object " + oid;
        c.object = repo->inspect(oid);
        render(SKIN, "object_view", c);
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}

void GitApp::tags(std::string name) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        content::tags_view c;
        c.repo = name;
        c.title = "tags " + name;
        c.tags = repo->tags();
        render(SKIN, "tags_view", c);
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}

void GitApp::tag(std::string name, std::string tag_name) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        content::tag_view c;
        c.repo = name;
        c.title = "tag " + tag_name;
        c.tag = repo->tag_detail(tag_name);
        render(SKIN, "tag_view", c);
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}

void GitApp::commit(std::string name, std::string oid) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        content::commit_view c;
        c.repo = name;
        c.title = "commit " + oid;
        c.commit = repo->commit_detail(oid);
        render(SKIN, "commit_view", c);
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}

void GitApp::commit_patch(std::string name, std::string oid) {
    std::unique_ptr<GitRepo> repo = open(name);
    if (!repo)
        return;
    try {
        const std::string patch = repo->commit_patch(oid);
        response().content_type("text/plain; charset=utf-8");
        response().out().write(patch.data(), static_cast<std::streamsize>(patch.size()));
    } catch (const GitError &e) {
        show_error(404, e.what());
    }
}