package main
import (
"bytes"
"fmt"
"net/http"
"os"
"path"
"strings"
"syscall"
"unsafe"
)
var (
repoRoot = "/home/git"
gitBin = "/usr/bin/git"
addr = "127.0.0.1:3000"
host = "git.mleku.dev"
gitBackend = "/usr/lib/git-core/git-http-backend"
)
// ── exec ────────────────────────────────────────────────────────
// run forks, execs bin with args, captures stdout via pipe.
// Uses raw clone syscall to bypass moxie's missing runtime fork hooks.
func run(bin string, args ...string) ([]byte, error) {
var p [2]int
if err := syscall.Pipe(p[:]); err != nil {
return nil, err
}
argv := make([]string, 0, len(args)+1)
argv = append(argv, bin)
argv = append(argv, args...)
binp, _ := syscall.BytePtrFromString(bin)
argvp, _ := syscall.SlicePtrFromStrings(argv)
envp, _ := syscall.SlicePtrFromStrings([]string{"PATH=/usr/bin:/bin"})
r1, _, e := syscall.RawSyscall(syscall.SYS_CLONE, uintptr(syscall.SIGCHLD), 0, 0)
if e != 0 {
syscall.Close(p[0])
syscall.Close(p[1])
return nil, e
}
if r1 == 0 {
// child
syscall.Dup2(p[1], 1)
syscall.Close(p[0])
syscall.Close(p[1])
devnull, _ := syscall.Open("/dev/null", syscall.O_RDWR, 0)
syscall.Dup2(devnull, 0)
syscall.Dup2(devnull, 2)
syscall.Close(devnull)
syscall.RawSyscall(syscall.SYS_EXECVE,
uintptr(unsafe.Pointer(binp)),
uintptr(unsafe.Pointer(&argvp[0])),
uintptr(unsafe.Pointer(&envp[0])))
syscall.Exit(127)
}
pid := int(r1)
syscall.Close(p[1])
var buf bytes.Buffer
tmp := make([]byte, 8192)
for {
n, _ := syscall.Read(p[0], tmp)
if n <= 0 {
break
}
buf.Write(tmp[:n])
}
syscall.Close(p[0])
var ws syscall.WaitStatus
syscall.Wait4(pid, &ws, 0, nil)
if ws.Exited() && ws.ExitStatus() != 0 {
return buf.Bytes(), fmt.Errorf("exit %d", ws.ExitStatus())
}
return buf.Bytes(), nil
}
func gitCmd(repoDir string, args ...string) ([]byte, error) {
full := make([]string, 0, len(args)+2)
full = append(full, "--git-dir="+repoDir)
full = append(full, args...)
return run(gitBin, full...)
}
// runIO forks, execs bin with args using given env, optionally piping stdin.
func runIO(env []string, stdin []byte, bin string, args ...string) ([]byte, error) {
var pout [2]int
if err := syscall.Pipe(pout[:]); err != nil {
return nil, err
}
hasIn := stdin != nil
var pin [2]int
if hasIn {
if err := syscall.Pipe(pin[:]); err != nil {
syscall.Close(pout[0])
syscall.Close(pout[1])
return nil, err
}
}
argv := make([]string, 0, len(args)+1)
argv = append(argv, bin)
argv = append(argv, args...)
binp, _ := syscall.BytePtrFromString(bin)
argvp, _ := syscall.SlicePtrFromStrings(argv)
envp, _ := syscall.SlicePtrFromStrings(env)
r1, _, e := syscall.RawSyscall(syscall.SYS_CLONE, uintptr(syscall.SIGCHLD), 0, 0)
if e != 0 {
syscall.Close(pout[0])
syscall.Close(pout[1])
if hasIn {
syscall.Close(pin[0])
syscall.Close(pin[1])
}
return nil, e
}
if r1 == 0 {
syscall.Dup2(pout[1], 1)
syscall.Close(pout[0])
syscall.Close(pout[1])
if hasIn {
syscall.Dup2(pin[0], 0)
syscall.Close(pin[0])
syscall.Close(pin[1])
}
devnull, _ := syscall.Open("/dev/null", syscall.O_RDWR, 0)
if !hasIn {
syscall.Dup2(devnull, 0)
}
syscall.Dup2(devnull, 2)
syscall.Close(devnull)
syscall.RawSyscall(syscall.SYS_EXECVE,
uintptr(unsafe.Pointer(binp)),
uintptr(unsafe.Pointer(&argvp[0])),
uintptr(unsafe.Pointer(&envp[0])))
syscall.Exit(127)
}
pid := int(r1)
syscall.Close(pout[1])
if hasIn {
syscall.Close(pin[0])
for off := 0; off < len(stdin); {
n, err := syscall.Write(pin[1], stdin[off:])
if n <= 0 || err != nil {
break
}
off += n
}
syscall.Close(pin[1])
}
var buf bytes.Buffer
tmp := make([]byte, 32768)
for {
n, _ := syscall.Read(pout[0], tmp)
if n <= 0 {
break
}
buf.Write(tmp[:n])
}
syscall.Close(pout[0])
var ws syscall.WaitStatus
syscall.Wait4(pid, &ws, 0, nil)
if ws.Exited() && ws.ExitStatus() != 0 {
return buf.Bytes(), fmt.Errorf("exit %d", ws.ExitStatus())
}
return buf.Bytes(), nil
}
// ── html helpers ────────────────────────────────────────────────
func esc(s string) string {
s = strings.ReplaceAll(s, "&", "&")
s = strings.ReplaceAll(s, "<", "<")
s = strings.ReplaceAll(s, ">", ">")
s = strings.ReplaceAll(s, "\"", """)
return s
}
// ── markdown ────────────────────────────────────────────────────
// renderMD converts markdown source to HTML.
func renderMD(src, linkName string) string {
var b strings.Builder
lines := strings.Split(src, "\n")
n := len(lines)
i := 0
var closeTags []string // stack of tags to close
flush := func() {
for j := len(closeTags) - 1; j >= 0; j-- {
b.WriteString(closeTags[j])
}
closeTags = nil
}
inState := func(tag string) bool {
for _, t := range closeTags {
if t == tag {
return true
}
}
return false
}
lastWasHeading := false
for i < n {
line := lines[i]
// fenced code block
if strings.HasPrefix(line, "```") {
flush()
lastWasHeading = false
lang := strings.TrimSpace(line[3:])
b.WriteString(`
`)
if lang != "" {
_ = lang // no syntax highlighting, just consume
}
i++
for i < n && !strings.HasPrefix(lines[i], "```") {
b.WriteString(esc(lines[i]))
b.WriteByte('\n')
i++
}
b.WriteString(` `)
i++ // skip closing ```
continue
}
// blank line — close open blocks
if strings.TrimSpace(line) == "" {
flush()
i++
continue
}
// heading
if line[0] == '#' {
flush()
lvl := 0
for lvl < len(line) && line[lvl] == '#' {
lvl++
}
if lvl <= 6 && lvl < len(line) && line[lvl] == ' ' {
text := strings.TrimSpace(line[lvl:])
text = strings.TrimRight(text, " #")
b.WriteString(fmt.Sprintf("%s \n", lvl, mdInline(text, linkName), lvl))
lastWasHeading = true
i++
continue
}
}
// horizontal rule — skip if immediately after a heading (already has border-bottom)
trimmed := strings.TrimSpace(line)
if len(trimmed) >= 3 && (allChar(trimmed, '-') || allChar(trimmed, '*') || allChar(trimmed, '_')) {
flush()
if !lastWasHeading {
b.WriteString(" \n")
}
lastWasHeading = false
i++
continue
}
// table (line contains | and next line is separator)
if strings.Contains(line, "|") && i+1 < n && isTableSep(lines[i+1]) {
flush()
b.WriteString("\n")
for _, cell := range splitTableRow(line) {
b.WriteString("" + mdInline(strings.TrimSpace(cell), linkName) + " ")
}
b.WriteString(" \n\n")
i += 2 // skip header + separator
for i < n && strings.Contains(lines[i], "|") && strings.TrimSpace(lines[i]) != "" {
b.WriteString("")
for _, cell := range splitTableRow(lines[i]) {
b.WriteString("" + mdInline(strings.TrimSpace(cell), linkName) + " ")
}
b.WriteString(" \n")
i++
}
b.WriteString("
\n")
continue
}
// blockquote
if strings.HasPrefix(line, "> ") || line == ">" {
if !inState("\n") {
flush()
b.WriteString("")
closeTags = append(closeTags, " \n")
}
text := ""
if len(line) > 2 {
text = line[2:]
}
b.WriteString(mdInline(text, linkName) + "\n")
i++
continue
}
// unordered list
if (strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "* ") || strings.HasPrefix(line, "+ ")) && len(line) > 2 {
if !inState("\n") {
flush()
b.WriteString("\n")
closeTags = append(closeTags, " \n")
}
b.WriteString("" + mdInline(line[2:], linkName) + " \n")
i++
continue
}
// ordered list
if isOL(line) {
if !inState("\n") {
flush()
b.WriteString("\n")
closeTags = append(closeTags, " \n")
}
dot := strings.IndexByte(line, '.')
b.WriteString("" + mdInline(strings.TrimSpace(line[dot+1:]), linkName) + " \n")
i++
continue
}
// paragraph
if !inState("\n") {
flush()
b.WriteString("")
closeTags = append(closeTags, "
\n")
} else {
b.WriteByte('\n')
}
b.WriteString(mdInline(line, linkName))
i++
}
flush()
return b.String()
}
// mdInline processes inline markdown: code, bold, italic, links, images.
func mdInline(s, linkName string) string {
s = esc(s) // HTML-escape first; markdown punctuation (*,[,],`,!) is not affected
var b strings.Builder
i := 0
for i < len(s) {
// backtick code span
if s[i] == '`' {
end := strings.IndexByte(s[i+1:], '`')
if end >= 0 {
b.WriteString("" + s[i+1:i+1+end] + "")
i += end + 2
continue
}
}
// linked image [](link-url) — render as text link to link-url
if s[i] == '[' && i+1 < len(s) && s[i+1] == '!' && i+2 < len(s) && s[i+2] == '[' {
if innerText, _, innerAdv := parseLink(s[i+2:]); innerAdv > 0 {
pos := i + 2 + innerAdv
if pos+1 < len(s) && s[pos] == ']' && s[pos+1] == '(' {
end := strings.IndexByte(s[pos+2:], ')')
if end >= 0 {
url := s[pos+2 : pos+2+end]
b.WriteString(`` + innerText + ` `)
i = pos + 2 + end + 1
continue
}
}
}
}
// image  — inline if relative, link if external
if s[i] == '!' && i+1 < len(s) && s[i+1] == '[' {
if text, url, advance := parseLink(s[i+1:]); advance > 0 {
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "//") {
b.WriteString(`` + text + ` `)
} else {
raw := "/" + linkName + "/raw/" + strings.TrimPrefix(url, "./")
b.WriteString(` `)
}
i += 1 + advance
continue
}
}
// link [text](url)
if s[i] == '[' {
if text, url, advance := parseLink(s[i:]); advance > 0 {
b.WriteString(`` + text + ` `)
i += advance
continue
}
}
// bold **text**
if i+1 < len(s) && s[i] == '*' && s[i+1] == '*' {
end := strings.Index(s[i+2:], "**")
if end >= 0 {
b.WriteString("" + s[i+2:i+2+end] + " ")
i += end + 4
continue
}
}
// bold __text__
if i+1 < len(s) && s[i] == '_' && s[i+1] == '_' {
end := strings.Index(s[i+2:], "__")
if end >= 0 {
b.WriteString("" + s[i+2:i+2+end] + " ")
i += end + 4
continue
}
}
// italic *text*
if s[i] == '*' && (i+1 < len(s) && s[i+1] != '*') {
end := strings.IndexByte(s[i+1:], '*')
if end > 0 {
b.WriteString("" + s[i+1:i+1+end] + " ")
i += end + 2
continue
}
}
// italic _text_
if s[i] == '_' && (i+1 < len(s) && s[i+1] != '_') {
end := strings.IndexByte(s[i+1:], '_')
if end > 0 {
b.WriteString("" + s[i+1:i+1+end] + " ")
i += end + 2
continue
}
}
b.WriteByte(s[i])
i++
}
return b.String()
}
// parseLink parses [text](url) starting at s[0]=='['.
// Returns text, url, and total bytes consumed. Returns 0 advance on failure.
func parseLink(s string) (string, string, int) {
if len(s) < 4 || s[0] != '[' {
return "", "", 0
}
closeBracket := strings.IndexByte(s[1:], ']')
if closeBracket < 0 {
return "", "", 0
}
closeBracket++ // adjust for offset
if closeBracket+1 >= len(s) || s[closeBracket+1] != '(' {
return "", "", 0
}
closeParen := strings.IndexByte(s[closeBracket+2:], ')')
if closeParen < 0 {
return "", "", 0
}
text := s[1:closeBracket]
url := s[closeBracket+2 : closeBracket+2+closeParen]
return text, url, closeBracket + 2 + closeParen + 1
}
func allChar(s string, c byte) bool {
for i := 0; i < len(s); i++ {
if s[i] != c && s[i] != ' ' {
return false
}
}
return true
}
func isOL(line string) bool {
i := 0
for i < len(line) && line[i] >= '0' && line[i] <= '9' {
i++
}
return i > 0 && i < len(line)-1 && line[i] == '.' && line[i+1] == ' '
}
func isTableSep(line string) bool {
t := strings.TrimSpace(line)
if !strings.Contains(t, "|") {
return false
}
for _, c := range t {
if c != '|' && c != '-' && c != ':' && c != ' ' {
return false
}
}
return true
}
func splitTableRow(line string) []string {
line = strings.TrimSpace(line)
line = strings.Trim(line, "|")
return strings.Split(line, "|")
}
const css = `
*{box-sizing:border-box}
body{background:#000;color:#bbb;font:14px/1.6 monospace;margin:0;padding:20px 40px;max-width:960px}
a{color:#7ab;text-decoration:none}
a:hover{text-decoration:underline}
h1{color:#ddd;font-size:20px;border-bottom:1px solid #333;padding-bottom:6px}
h2{color:#ccc;font-size:16px;margin-top:24px}
pre{background:#0a0a0a;color:#ccc;padding:14px;border-radius:3px;overflow-x:auto;border:1px solid #222}
.ls{list-style:none;padding:0;margin:0}
.ls li{padding:3px 0;border-bottom:1px solid #1a1a1a}
.ls .d a{color:#8bf}
.ls .d a::before{content:"d ";color:#555}
.ls .f a::before{content:" ";color:#555}
nav{margin-bottom:16px;color:#666}
nav a{margin-right:4px}
.desc{color:#666;margin-left:12px;font-size:12px}
.info{color:#666;font-size:12px;margin-top:4px}
.md{line-height:1.7}
.md h1{font-size:22px;margin-top:28px}
.md h2{font-size:18px;margin-top:24px}
.md h3{font-size:15px;color:#ccc;margin-top:20px}
.md h4,.md h5,.md h6{font-size:14px;color:#aaa;margin-top:16px}
.md p{margin:10px 0}
.md code{background:#1a1a2a;padding:2px 5px;border-radius:3px;font-size:13px}
.md pre{margin:12px 0}
.md pre code{background:none;padding:0}
.md blockquote{border-left:3px solid #444;margin:10px 0;padding:4px 16px;color:#999}
.md ul,.md ol{padding-left:24px;margin:8px 0}
.md li{margin:3px 0}
.md hr{border:none;border-top:1px solid #333;margin:20px 0}
.md img{max-width:100%}
.md table{border-collapse:collapse;margin:12px 0}
.md th,.md td{border:1px solid #333;padding:6px 12px}
.md th{background:#1a1a2a}
.readme-box{position:relative}
.readme-box input{display:none}
.readme-box .readme-body{max-height:50vh;overflow:hidden}
.readme-box input:checked+.readme-body{max-height:none}
.readme-box .readme-body::after{content:"";position:absolute;bottom:28px;left:0;right:0;height:80px;background:linear-gradient(transparent,#000);pointer-events:none}
.readme-box input:checked+.readme-body::after{display:none}
.readme-box label{display:block;text-align:center;padding:6px;color:#7ab;cursor:pointer;border-top:1px solid #222;margin-top:4px;font-size:13px}
.readme-box label:hover{color:#9cd}
.readme-box input:checked~label .show{display:none}
.readme-box input:not(:checked)~label .hide{display:none}
`
func page(title, body string) []byte {
var b bytes.Buffer
b.WriteString(` `)
b.WriteString(` `)
b.WriteString(`` + esc(title) + ` `)
b.WriteString(``)
b.WriteString(`` + body + ``)
return b.Bytes()
}
// ── repo discovery ──────────────────────────────────────────────
func findRepos() []string {
entries, err := os.ReadDir(repoRoot)
if err != nil {
return nil
}
var repos []string
for _, e := range entries {
if !e.IsDir() {
continue
}
if _, err := os.Stat(path.Join(repoRoot, e.Name(), "HEAD")); err == nil {
repos = append(repos, e.Name())
}
}
return repos
}
func cleanName(s string) string {
return strings.TrimSuffix(s, ".git")
}
// resolveRepo maps a URL name to the actual repo directory name.
// Accepts both "smesh" and "smesh.git".
func resolveRepo(name string) (string, string) {
// try exact match first
rp := path.Join(repoRoot, name)
if _, err := os.Stat(path.Join(rp, "HEAD")); err == nil {
return name, rp
}
// try with .git suffix
gitName := name + ".git"
rp = path.Join(repoRoot, gitName)
if _, err := os.Stat(path.Join(rp, "HEAD")); err == nil {
return gitName, rp
}
return "", ""
}
func repoDesc(repoDir string) string {
data, err := os.ReadFile(path.Join(repoDir, "description"))
if err != nil {
return ""
}
s := strings.TrimSpace(string(data))
if strings.HasPrefix(s, "Unnamed repository") {
return ""
}
return s
}
// ── ref resolution ──────────────────────────────────────────────
// defaultRef finds the actual default branch for a bare repo.
// HEAD may point to a ref that doesn't exist (e.g. refs/heads/master
// when the only branch is main).
func defaultRef(repoDir string) string {
// read HEAD to get the symbolic ref
data, err := os.ReadFile(path.Join(repoDir, "HEAD"))
if err != nil {
return "HEAD"
}
s := strings.TrimSpace(string(data))
if strings.HasPrefix(s, "ref: ") {
ref := s[5:]
// check if this ref exists as a loose ref file
if _, err := os.Stat(path.Join(repoDir, ref)); err == nil {
return ref
}
// check packed-refs
packed, err := os.ReadFile(path.Join(repoDir, "packed-refs"))
if err == nil && strings.Contains(string(packed), ref) {
return ref
}
}
// HEAD ref is broken — find first available branch
refsDir := path.Join(repoDir, "refs", "heads")
entries, err := os.ReadDir(refsDir)
if err == nil {
for _, e := range entries {
if !e.IsDir() {
return "refs/heads/" + e.Name()
}
}
}
// try packed-refs as last resort
packed, err := os.ReadFile(path.Join(repoDir, "packed-refs"))
if err == nil {
for _, line := range strings.Split(string(packed), "\n") {
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' || line[0] == '^' {
continue
}
fields := strings.Fields(line)
if len(fields) >= 2 && strings.HasPrefix(fields[1], "refs/heads/") {
return fields[1]
}
}
}
return "HEAD"
}
// ── tree parsing ────────────────────────────────────────────────
type entry struct {
name string
typ string // "tree" or "blob"
}
func lsTree(repoDir, ref, treePath string) ([]entry, error) {
args := []string{"ls-tree", ref}
if treePath != "" {
args = append(args, treePath+"/")
}
out, err := gitCmd(repoDir, args...)
if err != nil {
return nil, err
}
prefix := ""
if treePath != "" {
prefix = treePath + "/"
}
var entries []entry
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// format: \t
tab := strings.IndexByte(line, '\t')
if tab < 0 {
continue
}
name := line[tab+1:]
fields := strings.Fields(line[:tab])
if len(fields) < 2 {
continue
}
// strip directory prefix
if prefix != "" && strings.HasPrefix(name, prefix) {
name = name[len(prefix):]
}
entries = append(entries, entry{name: name, typ: fields[1]})
}
return entries, nil
}
func mimeType(name string) string {
ext := name
if i := strings.LastIndexByte(name, '.'); i >= 0 {
ext = name[i:]
}
switch strings.ToLower(ext) {
case ".html", ".htm":
return "text/html; charset=utf-8"
case ".css":
return "text/css; charset=utf-8"
case ".js":
return "text/javascript; charset=utf-8"
case ".json":
return "application/json"
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".gif":
return "image/gif"
case ".svg":
return "image/svg+xml"
case ".ico":
return "image/x-icon"
case ".woff2":
return "font/woff2"
case ".wasm":
return "application/wasm"
case ".pdf":
return "application/pdf"
case ".xml":
return "application/xml"
case ".txt", ".md", ".go", ".rs", ".py", ".sh", ".yml", ".yaml", ".toml",
".c", ".h", ".cpp", ".java", ".rb", ".pl", ".lua", ".sql", ".diff",
".patch", ".conf", ".cfg", ".ini", ".log", ".csv", ".tsx", ".ts",
".jsx", ".vue", ".svelte", ".mx":
return "text/plain; charset=utf-8"
}
return "application/octet-stream"
}
func isGitProto(sub string) bool {
return sub == "info" || sub == "git-upload-pack" || sub == "git-receive-pack" ||
sub == "HEAD" || sub == "objects"
}
// ── handlers ────────────────────────────────────────────────────
func handler(w http.ResponseWriter, r *http.Request) {
p := strings.Trim(r.URL.Path, "/")
if p == "" {
serveIndex(w)
return
}
parts := strings.SplitN(p, "/", 3)
urlName := parts[0]
// git smart HTTP protocol
if len(parts) > 1 && isGitProto(parts[1]) {
serveGit(w, r)
return
}
repo, rp := resolveRepo(urlName)
if repo == "" {
http.NotFound(w, r)
return
}
// go-get meta tag support
if r.URL.Query().Get("go-get") == "1" {
serveGoGet(w, urlName, repo)
return
}
// use the clean URL name for links
linkName := cleanName(repo)
if len(parts) == 1 {
serveRepo(w, linkName, repo, rp)
return
}
sub := ""
if len(parts) > 2 {
sub = parts[2]
}
switch parts[1] {
case "tree":
serveTree(w, linkName, repo, rp, sub)
case "blob":
serveBlob(w, linkName, repo, rp, sub)
case "raw":
serveRaw(w, linkName, rp, sub)
default:
http.NotFound(w, r)
}
}
func serveGoGet(w http.ResponseWriter, urlName, repo string) {
mod := cleanName(urlName)
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, ` go get`,
esc(host), esc(mod), esc(host), esc(repo))
}
func serveIndex(w http.ResponseWriter) {
repos := findRepos()
var b strings.Builder
b.WriteString(`` + esc(host) + ` `)
for _, r := range repos {
name := cleanName(r)
desc := repoDesc(path.Join(repoRoot, r))
b.WriteString(`` + esc(name) + ` `)
if desc != "" {
b.WriteString(`` + esc(desc) + ` `)
}
b.WriteString(` `)
}
b.WriteString(` `)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(page("repos", b.String()))
}
func serveRepo(w http.ResponseWriter, linkName, repo, repoDir string) {
ref := defaultRef(repoDir)
var b strings.Builder
b.WriteString(`repos `)
b.WriteString(`` + esc(linkName) + ` `)
// description
if desc := repoDesc(repoDir); desc != "" {
b.WriteString(`` + esc(desc) + `
`)
}
// clone urls
b.WriteString(`git clone https://` + esc(host) + `/` + esc(repo) + `
`)
b.WriteString(`git clone ssh://git@` + esc(host) + `:2222/~/` + esc(repo) + `
`)
// readme
for _, readme := range []string{"README.md", "README", "README.txt", "readme.md"} {
data, err := gitCmd(repoDir, "show", ref+":"+readme)
if err == nil && len(data) > 0 {
b.WriteString(``)
b.WriteString(`
`)
b.WriteString(`
`)
if strings.HasSuffix(readme, ".md") {
b.WriteString(`
` + renderMD(string(data), linkName) + `
`)
} else {
b.WriteString(`
` + esc(readme) + ` `)
b.WriteString(`
` + esc(string(data)) + ` `)
}
b.WriteString(`
`)
b.WriteString(`
show more show less `)
b.WriteString(`
`)
break
}
}
// file listing
entries, err := lsTree(repoDir, ref, "")
if err != nil {
b.WriteString(`empty repository — push to get started
`)
} else if len(entries) > 0 {
b.WriteString(`files `)
writeEntriesList(&b, linkName, entries, "")
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(page(linkName, b.String()))
}
func writeEntries(b *strings.Builder, linkName, repoDir, ref, treePath string) {
entries, err := lsTree(repoDir, ref, treePath)
if err != nil {
b.WriteString(`` + esc(err.Error()) + `
`)
return
}
writeEntriesList(b, linkName, entries, treePath)
}
func writeEntriesList(b *strings.Builder, linkName string, entries []entry, treePath string) {
b.WriteString(``)
for _, e := range entries {
if e.typ != "tree" {
continue
}
fp := e.name
if treePath != "" {
fp = treePath + "/" + e.name
}
b.WriteString(`` + esc(e.name) + `/ `)
}
for _, e := range entries {
if e.typ == "tree" {
continue
}
fp := e.name
if treePath != "" {
fp = treePath + "/" + e.name
}
b.WriteString(`` + esc(e.name) + ` `)
}
b.WriteString(` `)
}
func breadcrumb(linkName, treePath string) string {
var b strings.Builder
b.WriteString(`repos / ` + esc(linkName) + ` `)
if treePath != "" {
parts := strings.Split(treePath, "/")
for i, p := range parts {
prefix := strings.Join(parts[:i+1], "/")
if i < len(parts)-1 {
b.WriteString(` / ` + esc(p) + ` `)
} else {
b.WriteString(` / ` + esc(p))
}
}
}
b.WriteString(` `)
return b.String()
}
func serveTree(w http.ResponseWriter, linkName, repo, repoDir, treePath string) {
ref := defaultRef(repoDir)
var b strings.Builder
b.WriteString(breadcrumb(linkName, treePath))
b.WriteString(`` + esc(treePath) + ` `)
writeEntries(&b, linkName, repoDir, ref, treePath)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(page(linkName+" / "+treePath, b.String()))
}
func serveBlob(w http.ResponseWriter, linkName, repo, repoDir, blobPath string) {
ref := defaultRef(repoDir)
data, err := gitCmd(repoDir, "show", ref+":"+blobPath)
if err != nil {
http.NotFound(w, nil)
return
}
var b strings.Builder
b.WriteString(breadcrumb(linkName, blobPath))
fname := blobPath
if idx := strings.LastIndexByte(blobPath, '/'); idx >= 0 {
fname = blobPath[idx+1:]
}
b.WriteString(`` + esc(fname) + ` raw `)
// line numbers
lines := strings.Split(string(data), "\n")
b.WriteString(``)
for i, line := range lines {
ln := fmt.Sprintf("%4d ", i+1)
b.WriteString(`` + ln + ` ` + esc(line) + "\n")
}
b.WriteString(` `)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(page(linkName+" / "+blobPath, b.String()))
}
func serveRaw(w http.ResponseWriter, linkName, repoDir, filePath string) {
if filePath == "" {
http.NotFound(w, nil)
return
}
ref := defaultRef(repoDir)
data, err := gitCmd(repoDir, "show", ref+":"+filePath)
if err != nil {
http.NotFound(w, nil)
return
}
w.Header().Set("Content-Type", mimeType(filePath))
w.Header().Set("Cache-Control", "max-age=300")
w.Write(data)
}
func serveGit(w http.ResponseWriter, r *http.Request) {
env := []string{
"PATH=/usr/bin:/bin",
"GIT_PROJECT_ROOT=" + repoRoot,
"GIT_HTTP_EXPORT_ALL=1",
"REQUEST_METHOD=" + r.Method,
"QUERY_STRING=" + r.URL.RawQuery,
"PATH_INFO=" + r.URL.Path,
"SERVER_PROTOCOL=HTTP/1.1",
}
if ct := r.Header.Get("Content-Type"); ct != "" {
env = append(env, "CONTENT_TYPE="+ct)
}
if cl := r.Header.Get("Content-Length"); cl != "" {
env = append(env, "CONTENT_LENGTH="+cl)
}
if proto := r.Header.Get("Git-Protocol"); proto != "" {
env = append(env, "GIT_PROTOCOL="+proto)
}
ra := r.RemoteAddr
if i := strings.LastIndexByte(ra, ':'); i >= 0 {
ra = ra[:i]
}
env = append(env, "REMOTE_ADDR="+ra)
var stdin []byte
if r.Method == "POST" && r.Body != nil {
var body bytes.Buffer
tmp := make([]byte, 8192)
for {
n, err := r.Body.Read(tmp)
if n > 0 {
body.Write(tmp[:n])
}
if err != nil {
break
}
}
if body.Len() > 0 {
stdin = body.Bytes()
}
}
out, err := runIO(env, stdin, gitBackend)
if err != nil && len(out) == 0 {
http.Error(w, "git backend error", 500)
return
}
// parse CGI response: headers \n\n body
sep := bytes.Index(out, []byte("\r\n\r\n"))
skip := 4
if sep < 0 {
sep = bytes.Index(out, []byte("\n\n"))
skip = 2
}
if sep < 0 {
http.Error(w, "bad cgi response", 500)
return
}
code := 200
for _, line := range strings.Split(string(out[:sep]), "\n") {
line = strings.TrimRight(line, "\r")
idx := strings.IndexByte(line, ':')
if idx < 0 {
continue
}
key := strings.TrimSpace(line[:idx])
val := strings.TrimSpace(line[idx+1:])
if strings.EqualFold(key, "Status") {
if len(val) >= 3 {
code = 0
for j := 0; j < 3; j++ {
if val[j] >= '0' && val[j] <= '9' {
code = code*10 + int(val[j]-'0')
}
}
}
} else {
w.Header().Set(key, val)
}
}
w.WriteHeader(code)
w.Write(out[sep+skip:])
}
// ── main ────────────────────────────────────────────────────────
func main() {
for i := 1; i < len(os.Args); i++ {
switch os.Args[i] {
case "-repos":
i++
if i < len(os.Args) {
repoRoot = os.Args[i]
}
case "-listen":
i++
if i < len(os.Args) {
addr = os.Args[i]
}
case "-git":
i++
if i < len(os.Args) {
gitBin = os.Args[i]
}
case "-host":
i++
if i < len(os.Args) {
host = os.Args[i]
}
case "-git-backend":
i++
if i < len(os.Args) {
gitBackend = os.Args[i]
}
}
}
// auto-detect git-http-backend path
if _, err := os.Stat(gitBackend); err != nil {
for _, p := range []string{"/usr/lib/git-core/git-http-backend", "/usr/libexec/git-core/git-http-backend"} {
if _, err := os.Stat(p); err == nil {
gitBackend = p
break
}
}
}
fmt.Printf("gitweb %s repos=%s\n", addr, repoRoot)
http.HandleFunc("/", handler)
if err := http.ListenAndServe(addr, nil); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}