src/application.cpp
browsing at commit = 2aba3f2d16b6d85c2eea5ee133677e3b14a0f57b
#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 <atomic>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <thread>
namespace {
constexpr int LOG_LIMIT = 200;
const std::string SKIN = "git";
// Cache directory resolution order: GIT_CACHE_ROOT env (e.g. set by the
// Dockerfile), config key "git.cache_root", then XDG_CACHE_HOME/git_frontend,
// then $HOME/.cache/git_frontend, falling back to a temp directory.
std::string resolve_cache_root(const std::string &configured) {
if (const char *env = std::getenv("GIT_CACHE_ROOT"); env && *env)
return env;
if (!configured.empty())
return configured;
if (const char *xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg)
return std::string(xdg) + "/git_frontend";
if (const char *home = std::getenv("HOME"); home && *home)
return std::string(home) + "/.cache/git_frontend";
return "/tmp/git_frontend";
}
// Splits a comma- or whitespace-separated host list into trimmed, non-empty
// entries. Used for the GitHub Enterprise host allowlist.
std::vector<std::string> split_hosts(const std::string &list) {
std::vector<std::string> hosts;
std::string current;
for (const char c : list) {
if (c == ',' || c == ' ' || c == '\t' || c == '\n') {
if (!current.empty())
hosts.push_back(current);
current.clear();
} else {
current += c;
}
}
if (!current.empty())
hosts.push_back(current);
return hosts;
}
// GitHub Enterprise hosts come from the GITHUB_ENTERPRISE_HOSTS env var (e.g.
// set by the Dockerfile) or the config key "git.github_enterprise_hosts".
std::string resolve_github_hosts(const std::string &configured) {
if (const char *env = std::getenv("GITHUB_ENTERPRISE_HOSTS"); env && *env)
return env;
return configured;
}
// 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");
cache_root_ = resolve_cache_root(settings().get<std::string>("git.cache_root", ""));
github_hosts_ = split_hosts(resolve_github_hosts(
settings().get<std::string>("git.github_enterprise_hosts", "")));
// 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/(.+)\\.tar\\.gz$", &GitApp::tag_archive, this, 1, 2);
dispatcher().assign("^/repos/([^/]+)/tags/(.+)$", &GitApp::tag, this, 1, 2);
dispatcher().assign("^/repos/([^/]+)/branches/?$", &GitApp::branches, this, 1);
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();
const util::GitHubClone clone =
util::github_clone_urls(repo->remote_url(), github_hosts_);
c.clone_ssh = clone.ssh;
c.clone_https = clone.https;
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();
c.tags = repo->tags_for_commit(commit);
c.branches = repo->branches_for_commit(commit);
for (const BranchInfo &b : c.branches) {
if (b.is_default) {
c.on_default_branch = true;
break;
}
}
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);
c.tags = repo->tags_for_commit(commit);
c.branches = repo->branches_for_commit(commit);
for (const BranchInfo &b : c.branches) {
if (b.is_default) {
c.on_default_branch = true;
break;
}
}
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);
for (const CommitInfo &ci : c.commits) {
if (!ci.body.empty()) {
c.has_bodies = true;
break;
}
}
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::tag_archive(std::string name, std::string tag_name) {
std::unique_ptr<GitRepo> repo = open(name);
if (!repo)
return;
std::string oid;
try {
// tag_detail validates that the tag exists and resolves its commit.
oid = repo->tag_detail(tag_name).target_oid;
} catch (const GitError &e) {
show_error(404, e.what());
return;
}
if (oid.empty()) {
show_error(404, "tag does not resolve to a commit: " + tag_name);
return;
}
const std::string base =
util::sanitize_filename(name) + "_" + util::sanitize_filename(tag_name);
const std::string download_name = base + ".tar.gz";
// The commit oid is part of the cache key so a moved tag never serves stale
// contents.
const std::string cache_file = cache_root_ + "/" + base + "_" + oid + ".tar.gz";
std::error_code ec;
std::filesystem::create_directories(cache_root_, ec);
if (ec) {
show_error(500, "cannot create cache directory: " + ec.message());
return;
}
if (!std::filesystem::exists(cache_file)) {
// Build to a unique temp file, then rename atomically so a crashed or
// concurrent build never leaves a truncated archive at the cache path.
static std::atomic<uint64_t> counter{0};
const std::string tmp = cache_file + ".tmp." +
std::to_string(counter.fetch_add(1)) + "." +
std::to_string(std::hash<std::thread::id>{}(
std::this_thread::get_id()));
try {
repo->write_archive(oid, base, tmp);
} catch (const std::exception &e) {
std::filesystem::remove(tmp, ec);
show_error(500, std::string("failed to build archive: ") + e.what());
return;
}
std::filesystem::rename(tmp, cache_file, ec);
if (ec) {
// A concurrent request may have produced it first; drop our copy.
std::filesystem::remove(tmp, ec);
if (!std::filesystem::exists(cache_file)) {
show_error(500, "failed to store archive");
return;
}
}
}
std::ifstream file(cache_file, std::ios::binary);
if (!file) {
show_error(500, "archive unavailable");
return;
}
response().content_type("application/gzip");
response().set_header("Content-Disposition",
"attachment; filename=\"" + download_name + "\"");
response().out() << file.rdbuf();
}
void GitApp::branches(std::string name) {
std::unique_ptr<GitRepo> repo = open(name);
if (!repo)
return;
try {
content::branches_view c;
c.repo = name;
c.title = "branches " + name;
c.branches = repo->branches();
render(SKIN, "branches_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);
c.branches = repo->branches_for_commit(c.commit.oid);
for (const BranchInfo &b : c.branches) {
if (b.is_default) {
c.on_default_branch = true;
break;
}
}
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());
}
}