git repos / sago_web_git

commit 7df4218f

Poul Sander · 2026-06-13 10:47
7df4218f1bb7efd0913733a5ae4f641268f5e8af patch · browse files
parent 4fe276c400f51581a3858e55f7b9a48a1f90f419

added help to some filetype detection
Now collapses single child dirs
Shows images for image file types

Changed files

M src/application.cpp before
M src/content.h before
M src/git_repo.cpp before
M src/util.cpp before
M src/util.h before
M static/app.js before
A static/cmake.min.js
M static/style.css before
M templates/skin.tmpl before
diff --git a/src/application.cpp b/src/application.cpp index e6abbd6..4738c64 100644 --- a/src/application.cpp +++ b/src/application.cpp
@@ -157,7 +157,10 @@ void GitApp::tree(std::string name, std::string path) {
c.path = clean;
c.filename = util::basename(clean);
c.is_binary = is_binary;
- c.is_markdown = !is_binary && util::is_markdown_path(clean);
+ 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);
@@ -185,14 +188,19 @@ void GitApp::raw(std::string name, std::string path) {
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, util::trim_slashes(path), content, is_binary)) {
+ if (!repo->read_file(commit, clean, content, is_binary)) {
show_error(404, "No such file: " + path);
return;
}
- response().content_type(is_binary ? "application/octet-stream"
- : "text/plain; charset=utf-8");
+ 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());
diff --git a/src/content.h b/src/content.h index 2a48b03..4b227ee 100644 --- a/src/content.h +++ b/src/content.h
@@ -48,6 +48,7 @@ struct file_view : public master {
std::string text;
bool is_binary = false;
bool is_markdown = false;
+ bool is_image = false;
uint64_t size = 0;
};
diff --git a/src/git_repo.cpp b/src/git_repo.cpp index 0d0eca5..8163583 100644 --- a/src/git_repo.cpp +++ b/src/git_repo.cpp
@@ -77,6 +77,38 @@ TreeEntry make_entry(const git_tree_entry *entry, const std::string &dir,
return out;
}
+// Upper bound on folded path depth; guards against absurdly deep chains.
+const int MAX_COLLAPSE_DEPTH = 64;
+
+// Fold a chain of single-child directories into one entry, e.g. a "com" folder
+// holding only "domain" holding only "package" becomes "com/domain/package" so
+// navigation jumps straight to the deepest folder. Stops as soon as a directory
+// has more than one child or its only child is not itself a directory. Costs one
+// tree lookup per directory shown in a listing.
+void collapse_single_child_dirs(git_repository *repo, TreeEntry &entry) {
+ git_oid current;
+ if (git_oid_fromstr(&current, entry.oid.c_str()) != 0)
+ return;
+ for (int depth = 0; depth < MAX_COLLAPSE_DEPTH; ++depth) {
+ git_tree *tree = nullptr;
+ if (git_tree_lookup(&tree, repo, &current) != 0)
+ return;
+ const git_tree_entry *child =
+ git_tree_entrycount(tree) == 1 ? git_tree_entry_byindex(tree, 0) : nullptr;
+ if (!child || git_tree_entry_type(child) != GIT_OBJECT_TREE) {
+ git_tree_free(tree);
+ return;
+ }
+ entry.name += "/";
+ entry.name += git_tree_entry_name(child);
+ entry.path += "/";
+ entry.path += git_tree_entry_name(child);
+ git_oid_cpy(&current, git_tree_entry_id(child));
+ entry.oid = oid_to_string(&current);
+ git_tree_free(tree);
+ }
+}
+
std::string first_line(const char *text) {
if (!text)
return "";
@@ -216,8 +248,12 @@ std::vector<TreeEntry> GitRepo::list_tree(const std::string &commit_oid,
std::vector<TreeEntry> entries;
const size_t count = git_tree_entrycount(tree);
entries.reserve(count);
- for (size_t i = 0; i < count; ++i)
- entries.push_back(make_entry(git_tree_entry_byindex(tree, i), clean, repo_));
+ for (size_t i = 0; i < count; ++i) {
+ TreeEntry e = make_entry(git_tree_entry_byindex(tree, i), clean, repo_);
+ if (e.is_dir)
+ collapse_single_child_dirs(repo_, e);
+ entries.push_back(std::move(e));
+ }
if (subtree)
git_tree_free(subtree);
diff --git a/src/util.cpp b/src/util.cpp index 121cdcb..273b6a3 100644 --- a/src/util.cpp +++ b/src/util.cpp
@@ -82,6 +82,31 @@ bool is_markdown_path(const std::string &path) {
return ext == "md" || ext == "markdown";
}
+std::string image_mime_type(const std::string &path) {
+ const size_t dot = path.find_last_of('.');
+ if (dot == std::string::npos)
+ return "";
+ std::string ext = path.substr(dot + 1);
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+ if (ext == "png")
+ return "image/png";
+ if (ext == "jpg" || ext == "jpeg")
+ return "image/jpeg";
+ if (ext == "gif")
+ return "image/gif";
+ if (ext == "webp")
+ return "image/webp";
+ if (ext == "svg")
+ return "image/svg+xml";
+ if (ext == "bmp")
+ return "image/bmp";
+ if (ext == "ico")
+ return "image/x-icon";
+ if (ext == "avif")
+ return "image/avif";
+ return "";
+}
+
bool show_emails() {
static const bool value = [] {
const char *flag = std::getenv("SHOW_EMAILS");
diff --git a/src/util.h b/src/util.h index 8ccd1d3..d395040 100644 --- a/src/util.h +++ b/src/util.h
@@ -33,4 +33,8 @@ bool show_emails();
// insensitive.
bool is_markdown_path(const std::string &path);
+// MIME type for a recognised image path by extension (e.g. "image/png"), or
+// "" when the path is not a recognised image. Case insensitive.
+std::string image_mime_type(const std::string &path);
+
} // namespace util
diff --git a/static/app.js b/static/app.js index ea169a4..5cb7ef3 100644 --- a/static/app.js +++ b/static/app.js
@@ -3,10 +3,39 @@
(function () {
"use strict";
+ // Map a filename to an explicit highlight.js language. Full-name matches
+ // (e.g. CMakeLists.txt) take priority over extensions.
+ var FILENAME_LANGUAGES = { "CMakeLists.txt": "cmake" };
+ var EXTENSION_LANGUAGES = {
+ cpp: "cpp",
+ hpp: "cpp",
+ js: "javascript",
+ json: "json",
+ yml: "yaml",
+ txt: "plaintext",
+ };
+
+ function languageForFilename(filename) {
+ if (!filename) return null;
+ if (FILENAME_LANGUAGES[filename]) return FILENAME_LANGUAGES[filename];
+ var dot = filename.lastIndexOf(".");
+ // Extensionless files (LICENSE, README, etc.) are treated as plain text
+ // rather than letting auto-detection guess a language.
+ if (dot === -1) return "plaintext";
+ return EXTENSION_LANGUAGES[filename.slice(dot + 1).toLowerCase()] || null;
+ }
+
function highlight() {
if (typeof hljs === "undefined") return;
document.querySelectorAll("pre.code > code.hljs").forEach(function (el) {
+ var lang = languageForFilename(el.dataset.filename);
+ if (lang) el.classList.add("language-" + lang);
hljs.highlightElement(el);
+ var detected = el.result && el.result.language ? el.result.language : "plaintext";
+ var label = document.createElement("p");
+ label.className = "lang-detected";
+ label.textContent = "Detected language: " + detected;
+ el.parentElement.insertAdjacentElement("afterend", label);
});
}
diff --git a/static/cmake.min.js b/static/cmake.min.js new file mode 100644 index 0000000..7eae4c3 --- /dev/null +++ b/static/cmake.min.js
@@ -0,0 +1,7 @@
+/*! `cmake` grammar compiled for Highlight.js 11.9.0 */
+(()=>{var e=(()=>{"use strict";return e=>({name:"CMake",aliases:["cmake.in"],
+case_insensitive:!0,keywords:{
+keyword:"break cmake_host_system_information cmake_minimum_required cmake_parse_arguments cmake_policy configure_file continue elseif else endforeach endfunction endif endmacro endwhile execute_process file find_file find_library find_package find_path find_program foreach function get_cmake_property get_directory_property get_filename_component get_property if include include_guard list macro mark_as_advanced math message option return separate_arguments set_directory_properties set_property set site_name string unset variable_watch while add_compile_definitions add_compile_options add_custom_command add_custom_target add_definitions add_dependencies add_executable add_library add_link_options add_subdirectory add_test aux_source_directory build_command create_test_sourcelist define_property enable_language enable_testing export fltk_wrap_ui get_source_file_property get_target_property get_test_property include_directories include_external_msproject include_regular_expression install link_directories link_libraries load_cache project qt_wrap_cpp qt_wrap_ui remove_definitions set_source_files_properties set_target_properties set_tests_properties source_group target_compile_definitions target_compile_features target_compile_options target_include_directories target_link_directories target_link_libraries target_link_options target_sources try_compile try_run ctest_build ctest_configure ctest_coverage ctest_empty_binary_directory ctest_memcheck ctest_read_custom_files ctest_run_script ctest_sleep ctest_start ctest_submit ctest_test ctest_update ctest_upload build_name exec_program export_library_dependencies install_files install_programs install_targets load_command make_directory output_required_files remove subdir_depends subdirs use_mangled_mesa utility_source variable_requires write_file qt5_use_modules qt5_use_package qt5_wrap_cpp on off true false and or not command policy target test exists is_newer_than is_directory is_symlink is_absolute matches less greater equal less_equal greater_equal strless strgreater strequal strless_equal strgreater_equal version_less version_greater version_equal version_less_equal version_greater_equal in_list defined"
+},contains:[{className:"variable",begin:/\$\{/,end:/\}/
+},e.COMMENT(/#\[\[/,/]]/),e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE]
+})})();hljs.registerLanguage("cmake",e)})();
\ No newline at end of file
diff --git a/static/style.css b/static/style.css index fa3ccff..8b59db0 100644 --- a/static/style.css +++ b/static/style.css
@@ -76,6 +76,8 @@ table.blame .line pre { margin: 0; white-space: pre; }
.log .msg { width: 100%; }
.binary { color: var(--muted); }
+.lang-detected { color: var(--muted); font-size: 0.8rem; margin-top: 0.25rem; }
+.fileimg img { max-width: 100%; height: auto; border: 1px solid var(--border); border-radius: 6px; background: var(--code-bg); }
table.tree .status { font-family: ui-monospace, monospace; color: var(--muted); width: 1.5rem; }
table.tree .before { margin-left: 0.5rem; font-size: 0.8rem; color: var(--muted); }
diff --git a/templates/skin.tmpl b/templates/skin.tmpl index ac2b676..e5dfd08 100644 --- a/templates/skin.tmpl +++ b/templates/skin.tmpl
@@ -28,6 +28,7 @@
</header>
<main><% include content_body() %></main>
<script src="/static/highlight.min.js"></script>
+<script src="/static/cmake.min.js"></script>
<script src="/static/marked.min.js"></script>
<script src="/static/app.js"></script>
</body>
@@ -110,6 +111,12 @@
<a href="/repos/<%= repo %>/blame/<%= path %><%= query %>">blame</a> &middot;
<a href="/repos/<%= repo %>/log/<%= query %>">history</a>
</p>
+<% if is_image %>
+<p class="fileimg"><img src="/repos/<%= repo %>/raw/<%= path %><%= query %>" alt="<%= filename %>"></p>
+<% if not empty text %>
+<pre class="code"><code class="hljs" data-filename="<%= filename %>"><%= text %></code></pre>
+<% end %>
+<% else %>
<% if is_binary %>
<p class="binary">Binary file (<% c++ out() << util::human_size(content.size); %>) &mdash;
<a href="/repos/<%= repo %>/raw/<%= path %><%= query %>">view raw</a></p>
@@ -121,6 +128,7 @@
<pre class="code"><code class="hljs" data-filename="<%= filename %>"><%= text %></code></pre>
<% end %>
<% end %>
+<% end %>
<% end template %>
<% end view %>