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("") } 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("") } b.WriteString("\n") i++ } b.WriteString("
" + mdInline(strings.TrimSpace(cell), linkName) + "
" + mdInline(strings.TrimSpace(cell), linkName) + "
\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") } 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 [![text](img-url)](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 ![text](url) — 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(`` + text + ``) } 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(``) 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(``) 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(``) 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) } }