package main import ( "bytes" "fmt" "net" "net/http" "os" "path" "syscall" "unsafe" ) var ( repoRoot = "/home/git" gitBin = "/usr/bin/git" addr = "127.0.0.1:3000" host = "git.smesh.lol" 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 := []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 := []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 := []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 := []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 := []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 = bytes.ReplaceAll(s, "&", "&") s = bytes.ReplaceAll(s, "<", "<") s = bytes.ReplaceAll(s, ">", ">") s = bytes.ReplaceAll(s, "\"", """) return s } // ── markdown ──────────────────────────────────────────────────── // renderMD converts markdown source to HTML. func renderMD(src, linkName string) string { var b bytes.Buffer lines := bytes.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 — count opening backticks for GFM nesting if bytes.HasPrefix(line, "```") { flush() lastWasHeading = false fenceLen := 0 for fenceLen < len(line) && line[fenceLen] == '`' { fenceLen++ } lang := bytes.TrimSpace(line[fenceLen:]) b.WriteString(`
`)
			if lang != "" {
				_ = lang // no syntax highlighting, just consume
			}
			i++
			for i < n {
				cl := bytes.TrimSpace(lines[i])
				// closing fence: >= same backtick count, nothing else
				ticks := 0
				for ticks < len(cl) && cl[ticks] == '`' {
					ticks++
				}
				if ticks >= fenceLen && allChar(cl, '`') {
					break
				}
				b.WriteString(esc(lines[i]))
				b.WriteByte('\n')
				i++
			}
			b.WriteString(`
`) i++ // skip closing ``` continue } // blank line — close open blocks if bytes.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 := bytes.TrimSpace(line[lvl:]) text = bytes.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 := bytes.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 unescaped | and next line is separator) if hasUnescapedPipe(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 && hasUnescapedPipe(lines[i]) && bytes.TrimSpace(lines[i]) != "" { b.WriteString("") for _, cell := range splitTableRow(lines[i]) { b.WriteString("") } b.WriteString("\n") i++ } b.WriteString("
" + mdInline(bytes.TrimSpace(cell), linkName) + "
" + mdInline(bytes.TrimSpace(cell), linkName) + "
\n") continue } // blockquote if bytes.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 (bytes.HasPrefix(line, "- ") || bytes.HasPrefix(line, "* ") || bytes.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 := bytes.IndexByte(line, '.') b.WriteString("
  • " + mdInline(bytes.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 bytes.Buffer i := 0 for i < len(s) { // backtick code span if s[i] == '`' { end := bytes.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 := bytes.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 bytes.HasPrefix(url, "http://") || bytes.HasPrefix(url, "https://") || bytes.HasPrefix(url, "//") { b.WriteString(`` + text + ``) } else { raw := "/" + linkName + "/raw/" + bytes.TrimPrefix(url, "./") b.WriteString(`` + text + ``) } i += 1 + advance continue } } // link [text](url) — rewrite relative paths to gitweb blob URLs if s[i] == '[' { if text, url, advance := parseLink(s[i:]); advance > 0 { href := url if linkName != "" && !bytes.HasPrefix(url, "http://") && !bytes.HasPrefix(url, "https://") && !bytes.HasPrefix(url, "//") && !bytes.HasPrefix(url, "#") && !bytes.HasPrefix(url, "/") { href = "/" + linkName + "/blob/" + bytes.TrimPrefix(url, "./") } b.WriteString(`` + text + ``) i += advance continue } } // bold **text** if i+1 < len(s) && s[i] == '*' && s[i+1] == '*' { end := bytes.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 := bytes.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 := bytes.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 := bytes.IndexByte(s[i+1:], '_') if end > 0 { b.WriteString("" + s[i+1:i+1+end] + "") i += end + 2 continue } } // backslash escape — \| \* \_ \` \[ \] \\ render as literal if s[i] == '\\' && i+1 < len(s) { next := s[i+1] if next == '|' || next == '*' || next == '_' || next == '`' || next == '[' || next == ']' || next == '\\' { b.WriteByte(next) i += 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 := bytes.IndexByte(s[1:], ']') if closeBracket < 0 { return "", "", 0 } closeBracket++ // adjust for offset if closeBracket+1 >= len(s) || s[closeBracket+1] != '(' { return "", "", 0 } closeParen := bytes.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] == ' ' } // hasUnescapedPipe returns true if the line contains | that isn't // preceded by \ and isn't inside a backtick code span. func hasUnescapedPipe(line string) bool { inCode := false for i := 0; i < len(line); i++ { if line[i] == '`' { inCode = !inCode } else if line[i] == '\\' && i+1 < len(line) && line[i+1] == '|' { i++ // skip escaped pipe } else if line[i] == '|' && !inCode { return true } } return false } func isTableSep(line string) bool { t := bytes.TrimSpace(line) if !bytes.Contains(t, "|") { return false } for _, c := range t { if c != '|' && c != '-' && c != ':' && c != ' ' { return false } } return true } func splitTableRow(line string) []string { line = bytes.TrimSpace(line) line = bytes.Trim(line, "|") // Split on | but not \| (escaped pipe) or | inside backtick spans. // Uses index slicing into the original line — avoids bytes.Buffer // aliasing where string=[]byte causes Reset() to overwrite prior cells. var cells []string start := 0 inCode := false for i := 0; i < len(line); i++ { if line[i] == '`' { inCode = !inCode } else if line[i] == '\\' && i+1 < len(line) && line[i+1] == '|' { i++ // skip escaped pipe — mdInline handles \| → | } else if line[i] == '|' && !inCode { cells = append(cells, line[start:i]) start = i + 1 } } cells = append(cells, line[start:]) return cells } const css = ` :root{--bg:#fff;--bg2:#eaeaea;--fg:#111;--accent:#7b3fe4;--muted:#666;--border:#bbb} .dark{--bg:#000;--bg2:#111;--fg:#e0e0e0;--accent:#f59e0b;--muted:#666;--border:#333} *{box-sizing:border-box} body{background:var(--bg);color:var(--fg);font:14px/1.6 'Fira Code',monospace;margin:0;padding:20px 40px;max-width:960px} a{color:var(--accent);text-decoration:none} a:hover{text-decoration:underline} h1{color:var(--fg);font-size:20px;border-bottom:1px solid var(--border);padding-bottom:6px} h2{color:var(--fg);font-size:16px;margin-top:24px} pre{background:var(--bg2);color:var(--fg);padding:14px;border-radius:3px;overflow-x:auto;border:1px solid var(--border)} .ls{list-style:none;padding:0;margin:0} .ls li{padding:3px 0;border-bottom:1px solid var(--border)} .ls .d a{color:var(--accent)} .ls .d a::before{content:"d ";color:var(--muted)} .ls .f a::before{content:" ";color:var(--muted)} nav{margin-bottom:16px;color:var(--muted)} nav a{margin-right:4px} .desc{color:var(--muted);margin-left:12px;font-size:12px} .info{color:var(--muted);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;margin-top:20px} .md h4,.md h5,.md h6{font-size:14px;color:var(--muted);margin-top:16px} .md p{margin:10px 0} .md code{background:var(--bg2);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 var(--border);margin:10px 0;padding:4px 16px;color:var(--muted)} .md ul,.md ol{padding-left:24px;margin:8px 0} .md li{margin:3px 0} .md hr{border:none;border-top:1px solid var(--border);margin:20px 0} .md img{max-width:100%} .md table{border-collapse:collapse;margin:12px 0} .md th,.md td{border:1px solid var(--border);padding:6px 12px} .md th{background:var(--bg2)} .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,var(--bg));pointer-events:none} .readme-box input:checked+.readme-body::after{display:none} .readme-box label{display:block;text-align:center;padding:6px;color:var(--accent);cursor:pointer;border-top:1px solid var(--border);margin-top:4px;font-size:13px} .readme-box label:hover{opacity:0.8} .readme-box input:checked~label .show{display:none} .readme-box input:not(:checked)~label .hide{display:none} .hdr{display:flex;align-items:center;gap:12px;margin-bottom:16px} .hdr svg{width:56px;height:56px;flex-shrink:0} .hdr h1{border:none;padding:0;margin:0} .theme-btn{position:fixed;top:12px;right:12px;background:none;border:none;color:var(--fg);cursor:pointer;padding:8px;opacity:0.6} .theme-btn:hover{opacity:1} .theme-btn svg{width:22px;height:22px} .when-dark{display:none} .dark .when-light{display:none} .dark .when-dark{display:inline} .ln{color:var(--muted)} ::-webkit-scrollbar{width:14px} ::-webkit-scrollbar-track{background:var(--bg)} ::-webkit-scrollbar-thumb{background:var(--border);border-radius:7px;border:3px solid var(--bg)} ::-webkit-scrollbar-thumb:hover{background:var(--muted)} *{scrollbar-width:auto;scrollbar-color:var(--border) var(--bg)} ` const logo = `` const themeJS = `` const sunIcon = `` const moonIcon = `` const toggleJS = `` func page(title, body string) []byte { var b bytes.Buffer b.WriteString(``) b.WriteString(``) b.WriteString(`` + esc(title) + ``) b.WriteString(``) b.WriteString(themeJS) b.WriteString(``) b.WriteString(``) b.WriteString(body) b.WriteString(toggleJS) b.WriteString(``) 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 bytes.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 := bytes.TrimSpace(string(data)) if bytes.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 := bytes.TrimSpace(string(data)) if bytes.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 && bytes.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 bytes.Split(string(packed), "\n") { line = bytes.TrimSpace(line) if line == "" || line[0] == '#' || line[0] == '^' { continue } fields := bytes.Fields(line) if len(fields) >= 2 && bytes.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 bytes.Split(string(out), "\n") { line = bytes.TrimSpace(line) if line == "" { continue } // format: \t tab := bytes.IndexByte(line, '\t') if tab < 0 { continue } name := line[tab+1:] fields := bytes.Fields(line[:tab]) if len(fields) < 2 { continue } // strip directory prefix if prefix != "" && bytes.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 := bytes.LastIndexByte(name, '.'); i >= 0 { ext = name[i:] } switch bytes.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 := bytes.Trim(r.URL.Path, "/") if p == "" { serveIndex(w) return } parts := bytes.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 bytes.Buffer b.WriteString(`
    ` + logo + `

    ` + 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 bytes.Buffer 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 bytes.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 *bytes.Buffer, 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 *bytes.Buffer, 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 bytes.Buffer b.WriteString(``) return b.String() } func serveTree(w http.ResponseWriter, linkName, repo, repoDir, treePath string) { ref := defaultRef(repoDir) var b bytes.Buffer 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 bytes.Buffer b.WriteString(breadcrumb(linkName, blobPath)) fname := blobPath if idx := bytes.LastIndexByte(blobPath, '/'); idx >= 0 { fname = blobPath[idx+1:] } b.WriteString(`

    ` + esc(fname) + ` raw

    `) if bytes.HasSuffix(blobPath, ".md") { b.WriteString(`
    ` + renderMD(string(data), linkName) + `
    `) } else { lines := bytes.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 := bytes.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 := []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 bytes.Split(string(out[:sep]), "\n") { line = bytes.TrimRight(line, "\r") idx := bytes.IndexByte(line, ':') if idx < 0 { continue } key := bytes.TrimSpace(line[:idx]) val := bytes.TrimSpace(line[idx+1:]) if bytes.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) // Parse addr ourselves to bypass moxie SplitHostPort codegen bug. tcpAddr := &net.TCPAddr{Port: 3000} if i := bytes.LastIndexByte(addr, ':'); i >= 0 { h := addr[:i] p := addr[i+1:] if h != "" { tcpAddr.IP = net.ParseIP(h) } port := 0 for _, c := range p { port = port*10 + int(c-'0') } tcpAddr.Port = port } ln, err := net.ListenTCP("tcp", tcpAddr) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } http.HandleFunc("/", handler) if err := http.Serve(ln, nil); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } }