wayver's git archive


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

src/git/core.rs@f4279bc2ca927f6f651a20c5f0e7b5ebf76cc3a4

raw
Date Commit Message Author Files + -
2026-02-26 12:41 simple gitweb project listing-like implementation wayverd 5 56 40
...

1use std::path::Path;
2
3use git2::{Reference, Time};
4use syntect::parsing::SyntaxSet;
5
6use crate::{error::Context as _, error::Result, git::Repository, http::extractor::Ref};
7
8impl Repository {
9    #[must_use]
10    pub(crate) fn description(&self) -> String {
11        let content = std::fs::read_to_string(self.path().join("description")).unwrap_or_default();
12
13        let first = content.lines().next().unwrap_or_default();
14
15        first.to_string()
16    }
17
18    #[tracing::instrument(skip_all)]
19    pub(crate) fn head(&self) -> Result<Reference<'_>> {
20        let head = self.inner.head()?;
21
22        Ok(head)
23    }
24
25    #[tracing::instrument(skip_all)]
26    pub(crate) fn is_empty(&self) -> Result<bool> {
27        Ok(self.inner.is_empty()?)
28    }
29
30    #[must_use]
31    pub(crate) fn is_shallow(&self) -> bool {
32        self.inner.is_shallow()
33    }
34
35    #[tracing::instrument(skip_all)]
36    pub(crate) fn last_modified(&self) -> Result<Time> {
37        let head = self.head()?;
38        let commit = head.peel_to_commit()?;
39        let time = commit.committer().when();
40
41        Ok(time)
42    }
43
44    pub(crate) fn name(&self) -> Option<&str> {
45        self.inner
46            .workdir()
47            // use the path for bare repositories
48            .unwrap_or_else(|| self.inner.path())
49            .file_name()
50            .and_then(std::ffi::OsStr::to_str)
51    }
52
53    #[must_use]
54    pub(crate) fn owner(&self) -> Option<String> {
55        self.inner
56            .config()
57            .and_then(|config| config.get_string("gitweb.owner"))
58            .ok()
59    }
60
61    #[must_use]
62    pub(crate) fn section(&self) -> Option<String> {
63        self.inner
64            .config()
65            .and_then(|config| config.get_string("bile.section"))
66            .ok()
67    }
68
69    #[must_use]
70    pub(crate) fn path(&self) -> &Path {
71        self.inner.path()
72    }
73
74    #[must_use]
75    pub(crate) fn readme(&self, syntaxes: &SyntaxSet) -> String {
76        use askama::filters::Escaper as _;
77
78        enum ReadmeFormat {
79            Plaintext,
80            Html,
81            Markdown,
82        }
83
84        let mut format = ReadmeFormat::Plaintext;
85
86        self.inner
87            .revparse_single("HEAD:readme")
88            .or_else(|_| self.inner.revparse_single("HEAD:README.txt"))
89            .or_else(|_| self.inner.revparse_single("HEAD:readme.txt"))
90            .or_else(|_| self.inner.revparse_single("HEAD:README.txt"))
91            .or_else(|_| {
92                format = ReadmeFormat::Markdown;
93                self.inner.revparse_single("HEAD:readme.md")
94            })
95            .or_else(|_| self.inner.revparse_single("HEAD:README.md"))
96            .or_else(|_| self.inner.revparse_single("HEAD:readme.mdown"))
97            .or_else(|_| self.inner.revparse_single("HEAD:README.mdown"))
98            .or_else(|_| self.inner.revparse_single("HEAD:readme.markdown"))
99            .or_else(|_| self.inner.revparse_single("HEAD:README.markdown"))
100            .or_else(|_| {
101                format = ReadmeFormat::Html;
102                self.inner.revparse_single("HEAD:readme.html")
103            })
104            .or_else(|_| self.inner.revparse_single("HEAD:README.html"))
105            .or_else(|_| self.inner.revparse_single("HEAD:readme.htm"))
106            .or_else(|_| self.inner.revparse_single("HEAD:README.htm"))
107            .ok()
108            .and_then(|readme| readme.into_blob().ok())
109            .map(|blob| {
110                let text = str::from_utf8(blob.content()).unwrap_or_default();
111
112                // render the file contents to HTML
113                match format {
114                    // render plaintext as preformatted text
115                    ReadmeFormat::Plaintext => {
116                        let mut output = "<pre>".to_string();
117                        if let Err(err) = askama::filters::Html.write_escaped_str(&mut output, text)
118                        {
119                            tracing::error!(err=?err, "failed to write escaped plaintext readme");
120                        }
121                        output.push_str("</pre>");
122                        output
123                    }
124                    // already is HTML
125                    ReadmeFormat::Html => text.to_string(),
126                    // render Markdown to HTML
127                    ReadmeFormat::Markdown => crate::utils::markdown::render(syntaxes, text),
128                }
129            })
130            .unwrap_or_default()
131    }
132
133    #[tracing::instrument(skip_all)]
134    pub(crate) fn ref_or_head_shorthand(&self, r#ref: Option<&Ref>) -> Result<String> {
135        let head = self.head()?;
136
137        let spec = r#ref
138            .map(|r| r.0.clone())
139            .or_else(move || head.shorthand().map(str::to_string))
140            .context("failed to get repo ref spec")?;
141
142        Ok(spec)
143    }
144}
145