wayver's git archive


a simple self-hosted git server
git clone https://git.wayver.dev/bile

Commit: f4279bc2ca927f6f651a20c5f0e7b5ebf76cc3a4 (tree)
Parent: a5fbdd41e56dc312103006ed0c7058b275594a83 (tree)
Author: wayverd
Date: 2026 M02 26, Thu 12:41:36 -0500
5 files changed; 56 insertions 40 deletions
simple gitweb project listing-like implementation

it doesnt properly support nested sections yet

diff --git a/TODO.md b/TODO.md
index 06a75bf..dbdedce 100644
--- a/TODO.md
+++ b/TODO.md
@@ -23,27 +23,6 @@ maybe add a way to switch between rendered and 'raw' views
 
 `?raw` query maybe?
 
-## gitweb style project listing
-
-support something like gitwebs project listing config to allow for a custom repo listing layout
-
-might use the current config for it
-
-maybe something like this?
-
-```toml
-[[repo]]
-name = "bile"
-section = "web services"
-```
-
-could even have it be part of the repo config
-
-```ini
-[bile]
-    section = "web services"
-```
-
 ## html sanitization for code view and readme
 
 looking into using https://crates.io/crates/ammonia for this as there might be an injection possiblity with code and readme views
diff --git a/src/git/core.rs b/src/git/core.rs
index a774f68..435d0b7 100644
--- a/src/git/core.rs
+++ b/src/git/core.rs
@@ -51,11 +51,19 @@ impl Repository {
     }
 
     #[must_use]
-    pub(crate) fn owner(&self) -> String {
+    pub(crate) fn owner(&self) -> Option<String> {
         self.inner
             .config()
             .and_then(|config| config.get_string("gitweb.owner"))
-            .unwrap_or_default()
+            .ok()
+    }
+
+    #[must_use]
+    pub(crate) fn section(&self) -> Option<String> {
+        self.inner
+            .config()
+            .and_then(|config| config.get_string("bile.section"))
+            .ok()
     }
 
     #[must_use]
diff --git a/src/handlers/index.rs b/src/handlers/index.rs
index 5aa93b0..3526b23 100644
--- a/src/handlers/index.rs
+++ b/src/handlers/index.rs
@@ -18,6 +18,11 @@ use crate::{
 #[template(path = "index.html")]
 struct IndexTemplate<'a> {
     config: &'a Config,
+    sections: Vec<Section>,
+}
+
+struct Section {
+    name: Option<String>,
     repos: Vec<Repository>,
 }
 
@@ -30,12 +35,12 @@ fn inner(state: &BileState) -> Result<Response> {
     let Ok(read) = fs::read_dir(&state.config.project_root) else {
         return Ok(Html(IndexTemplate {
             config: &state.config,
-            repos: Vec::new(),
+            sections: Vec::new(),
         })
         .into_response());
     };
 
-    let mut repos = Vec::new();
+    let mut sections = Vec::new();
 
     for entry in read {
         let entry = entry.context("failed to open directory entry")?;
@@ -65,12 +70,27 @@ fn inner(state: &BileState) -> Result<Response> {
             continue;
         }
 
-        repos.push(repo);
+        let repo_section = repo.section();
+        let section = sections
+            .iter_mut()
+            .find(|s: &&mut Section| s.name == repo_section);
+        match section {
+            Some(section) => section.repos.push(repo),
+            None => sections.push(Section {
+                name: repo_section,
+                repos: vec![repo],
+            }),
+        }
+    }
+
+    sections.sort_by(|a, b| a.name.cmp(&b.name));
+    for section in &mut sections {
+        section.repos.sort_by(|a, b| a.name().cmp(&b.name()));
     }
 
     Ok(Html(IndexTemplate {
         config: &state.config,
-        repos,
+        sections,
     })
     .into_response())
 }
diff --git a/src/utils/filters.rs b/src/utils/filters.rs
index 3643db8..c762854 100644
--- a/src/utils/filters.rs
+++ b/src/utils/filters.rs
@@ -78,7 +78,7 @@ pub(crate) fn last_modified(repo: &Repository, _: &dyn askama::Values) -> askama
 
 #[askama::filter_fn]
 pub(crate) fn repo_owner(repo: &Repository, _: &dyn askama::Values) -> askama::Result<String> {
-    Ok(repo.owner())
+    Ok(repo.owner().unwrap_or_default())
 }
 
 #[askama::filter_fn]
diff --git a/templates/index.html b/templates/index.html
index d1ac34b..c897e53 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,19 +1,28 @@
 {% extends "base.html" %}
 
+{% macro render_section(section) %}
+  {% if section.name.is_some() %}
+    <h2>{{ section.name.as_deref().unwrap_or("") }}</h2>
+  {% endif %}
+  {% for repo in section.repos %}
+    <table class="repo">
+      <tbody>
+        <tr>
+          <td class="repo-link"><a href="{{repo|repo_name|urlencode_strict}}">{{repo|repo_name}}</a></td>
+          <td class="repo-last-updated">last updated {{repo|last_modified|format_datetime("%Y-%m-%d")}}</td>
+        </tr>
+        <tr>
+          <td class="repo-description" colspan="2">{{repo|description}}</td>
+        </tr>
+      </tbody>
+    </table>
+  {% endfor %}
+{% endmacro %}
+
 {% block content %}
   <div>
-    {% for repo in repos %}
-      <table class="repo">
-        <tbody>
-          <tr>
-            <td class="repo-link"><a href="{{repo|repo_name|urlencode_strict}}">{{repo|repo_name}}</a></td>
-            <td class="repo-last-updated">last updated {{repo|last_modified|format_datetime("%Y-%m-%d")}}</td>
-          </tr>
-          <tr>
-            <td class="repo-description" colspan="2">{{repo|description}}</td>
-          </tr>
-        </tbody>
-      </table>
+    {% for section in sections %}
+      {{ render_section(section=section) }}
     {% endfor %}
   </div>
 {% endblock %}=
\ No newline at end of file