28 files changed,
1530 insertions(+),
153 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-01-21 01:49:14 +0200
Authored at:
2026-01-18 19:46:07 +0200
Change ID:
wxolklpkwwzqrztquvmtmtsmzuktwqvt
Parent:
7a05bd0
jump to
M
config.yml
路路路 6 6 title: i like git 7 7 description: hey kid, come get your free software 8 8 host: git.olexsmir.xyz 9 - templates_dir: /home/olex/code/mugit/templates 10 9 11 10 repo: 12 11 dir: /home/olex/mugit-test/ 路路路 19 18 masters: 20 19 - master 21 20 - main 22 - private: 23 - # repo also can be marked as private by: 24 - # - putting `muprivate` file in the root of repo 25 - # - adding `[mugit]\n private = true` 26 - - org
M
go.sum
路路路 18 18 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 19 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 20 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 22 +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 21 23 github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= 22 24 github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 23 25 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 路路路 69 71 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 70 72 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 71 73 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 74 +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= 75 +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 72 76 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 73 77 golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 74 78 golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
M
internal/config/config.go
路路路 14 14 Port int `yaml:"port"` 15 15 } `yaml:"server"` 16 16 Meta struct { 17 - Title string `yaml:"title"` 18 - Description string `yaml:"description"` 19 - Host string `yaml:"host"` 20 - ChromaTheme string `yaml:"chroma_theme"` 21 - TemplatesDir string `yaml:"templates_dir"` 17 + Title string `yaml:"title"` 18 + Description string `yaml:"description"` 19 + Host string `yaml:"host"` 22 20 } `yaml:"meta"` 23 21 Repo struct { 24 22 Dir string `yaml:"dir"` 25 23 Readmes []string `yaml:"readmes"` 26 24 Masters []string `yaml:"masters"` 27 - Private []string `yaml:"private"` 28 25 } `yaml:"repo"` 29 - SSH struct { 30 - Keys []string `yaml:"keys"` 31 - } `yaml:"ssh"` 32 26 } 33 27 34 28 func Load(fpath string) (*Config, error) { 路路路 45 39 if config.Repo.Dir, err = filepath.Abs(config.Repo.Dir); err != nil { 46 40 return nil, err 47 41 } 48 - 49 - if config.Meta.TemplatesDir, err = filepath.Abs(config.Meta.TemplatesDir); err != nil { 50 - return nil, err 51 - } 52 - 53 - fmt.Println(config.Meta.TemplatesDir) 54 42 55 43 if verr := config.validate(); verr != nil { 56 44 return nil, verr
M
internal/git/repo.go
路路路 44 44 } 45 45 46 46 func (g *Repo) Commits() ([]*object.Commit, error) { 47 - ci, err := g.r.Log(&git.LogOptions{From: g.h}) 47 + ci, err := g.r.Log(&git.LogOptions{ 48 + From: g.h, 49 + Order: git.LogOrderCommitterTime, 50 + }) 48 51 if err != nil { 49 52 return nil, fmt.Errorf("commits from ref: %w", err) 50 53 }
A
internal/handlers/handlers.go
路路路 1 +package handlers 2 + 3 +import ( 4 + "fmt" 5 + "html/template" 6 + "net/http" 7 + "path/filepath" 8 + "strings" 9 + "time" 10 + 11 + "olexsmir.xyz/mugit/internal/config" 12 + "olexsmir.xyz/mugit/internal/humanize" 13 + "olexsmir.xyz/mugit/web" 14 +) 15 + 16 +type handlers struct { 17 + c *config.Config 18 + t *template.Template 19 +} 20 + 21 +func InitRoutes(cfg *config.Config) *http.ServeMux { 22 + tmpls := template.Must(template.New(""). 23 + Funcs(templateFuncs). 24 + ParseFS(web.TemplatesFS, "*")) 25 + h := handlers{cfg, tmpls} 26 + 27 + mux := http.NewServeMux() 28 + mux.HandleFunc("GET /", h.index) 29 + mux.HandleFunc("GET /static/{file}", h.serveStatic) 30 + mux.HandleFunc("GET /{name}", h.multiplex) 31 + mux.HandleFunc("POST /{name}", h.multiplex) 32 + mux.HandleFunc("GET /{name}/{rest...}", h.multiplex) 33 + mux.HandleFunc("POST /{name}/{rest...}", h.multiplex) 34 + mux.HandleFunc("GET /{name}/tree/{ref}/{rest...}", h.repoTree) 35 + mux.HandleFunc("GET /{name}/blob/{ref}/{rest...}", h.fileContents) 36 + mux.HandleFunc("GET /{name}/log/{ref}", h.log) 37 + mux.HandleFunc("GET /{name}/commit/{ref}", h.commit) 38 + mux.HandleFunc("GET /{name}/refs/{$}", h.refs) 39 + return mux 40 +} 41 + 42 +// multiplex, check if the request smells like gitprotocol-http(5), if so, it 43 +// passes it to git smart http, otherwise renders templates 44 +func (h *handlers) multiplex(w http.ResponseWriter, r *http.Request) { 45 + if r.URL.RawQuery == "service=git-receive-pack" { 46 + w.WriteHeader(http.StatusBadRequest) 47 + w.Write([]byte("http pushing isn't supported")) 48 + return 49 + } 50 + 51 + path := r.PathValue("rest") 52 + if path == "info/refs" && r.Method == "GET" && r.URL.RawQuery == "service=git-upload-pack" { 53 + h.infoRefs(w, r) 54 + } else if path == "git-upload-pack" && r.Method == "POST" { 55 + h.uploadPack(w, r) 56 + } else if r.Method == "GET" { 57 + h.repoIndex(w, r) 58 + } 59 +} 60 + 61 +func (h *handlers) serveStatic(w http.ResponseWriter, r *http.Request) { 62 + f := filepath.Clean(r.PathValue("file")) 63 + // TODO: check if files exists 64 + http.ServeFileFS(w, r, web.StaticFS, f) 65 +} 66 + 67 +var templateFuncs = template.FuncMap{ 68 + "commitSummary": func(v any) string { 69 + s := fmt.Sprint(v) 70 + if i := strings.IndexByte(s, '\n'); i >= 0 { 71 + s = strings.TrimSuffix(s[:i], "\r") 72 + return s + "..." 73 + } 74 + return strings.TrimSuffix(s, "\r") 75 + }, 76 + "humanTime": func(t time.Time) string { 77 + return humanize.Time(t) 78 + }, 79 +}
D
internal/handlers/handles.go
路路路 1 -package handlers 2 - 3 -import ( 4 - "html/template" 5 - "net/http" 6 - "path/filepath" 7 - 8 - "olexsmir.xyz/mugit/internal/config" 9 -) 10 - 11 -type handlers struct { 12 - c *config.Config 13 - t *template.Template 14 -} 15 - 16 -func InitRoutes(cfg *config.Config) *http.ServeMux { 17 - tmpls := template.Must(template.ParseGlob( 18 - filepath.Join(cfg.Meta.TemplatesDir, "*"), 19 - )) 20 - h := handlers{cfg, tmpls} 21 - 22 - mux := http.NewServeMux() 23 - mux.HandleFunc("GET /", h.index) 24 - 25 - return mux 26 -} 27 - 28 -// multiplex if request smells like gitprotocol-http(5) passes it to the git 29 -// http service renders templates. 30 -func (h *handlers) multiplex(w http.ResponseWriter, r *http.Request) { 31 - if r.URL.RawQuery == "service=git-receive-pack" { 32 - w.WriteHeader(http.StatusBadRequest) 33 - w.Write([]byte("http pushing isn't supported")) 34 - return 35 - } 36 - 37 - path := r.PathValue("rest") 38 - if path == "info/refs" && 39 - r.URL.RawQuery == "service=git-upload-pack" && 40 - r.Method == "GET" { 41 - h.infoRefs(w, r) 42 - } else if path == "git-upload-pack" && r.Method == "POST" { 43 - h.uploadPack(w, r) 44 - } else if r.Method == "GET" { 45 - h.repoIndex(w, r) 46 - } 47 -}
A
internal/handlers/repo.go
路路路 1 +package handlers 2 + 3 +import ( 4 + "bytes" 5 + "fmt" 6 + "html/template" 7 + "io" 8 + "log/slog" 9 + "net/http" 10 + "os" 11 + "path/filepath" 12 + "sort" 13 + "strconv" 14 + "strings" 15 + "time" 16 + 17 + "github.com/dustin/go-humanize" 18 + "github.com/yuin/goldmark" 19 + "github.com/yuin/goldmark/extension" 20 + "github.com/yuin/goldmark/renderer/html" 21 + "olexsmir.xyz/mugit/internal/git" 22 +) 23 + 24 +func (h *handlers) index(w http.ResponseWriter, r *http.Request) { 25 + dirs, err := os.ReadDir(h.c.Repo.Dir) 26 + if err != nil { 27 + h.write500(w, err) 28 + return 29 + } 30 + 31 + type repoInfo struct { 32 + Name, Desc, Idle string 33 + t time.Time 34 + } 35 + 36 + repoInfos := []repoInfo{} 37 + for _, dir := range dirs { 38 + name := dir.Name() 39 + repo, err := git.Open(filepath.Join(h.c.Repo.Dir, name), "") 40 + if err != nil { 41 + slog.Error("", "name", name, "err", err) 42 + continue 43 + } 44 + 45 + desc, err := repo.Description() 46 + if err != nil { 47 + slog.Error("", "err", err) 48 + continue 49 + } 50 + 51 + lastComit, err := repo.LastCommit() 52 + if err != nil { 53 + slog.Error("", "err", err) 54 + continue 55 + } 56 + 57 + repoInfos = append(repoInfos, repoInfo{ 58 + Name: name, 59 + Desc: desc, 60 + Idle: humanize.Time(lastComit.Author.When), 61 + t: lastComit.Author.When, 62 + }) 63 + } 64 + 65 + sort.Slice(repoInfos, func(i, j int) bool { 66 + return repoInfos[j].t.Before(repoInfos[i].t) 67 + }) 68 + 69 + data := make(map[string]any) 70 + data["meta"] = h.c.Meta 71 + data["repos"] = repoInfos 72 + h.templ(w, "index", data) 73 +} 74 + 75 +var markdown = goldmark.New( 76 + goldmark.WithRendererOptions(html.WithUnsafe()), 77 + goldmark.WithExtensions( 78 + extension.GFM, 79 + extension.Linkify, 80 + )) 81 + 82 +func (h *handlers) repoIndex(w http.ResponseWriter, r *http.Request) { 83 + name := filepath.Clean(r.PathValue("name")) 84 + repo, err := git.Open(filepath.Join(h.c.Repo.Dir, name), "") 85 + if err != nil { 86 + h.write404(w, err) 87 + return 88 + } 89 + 90 + isPrivate, err := repo.IsPrivate() 91 + if isPrivate || err != nil { // FIX: private = 404, err = 500 92 + h.write404(w, err) 93 + return 94 + } 95 + 96 + var readmeContents template.HTML 97 + for _, readme := range h.c.Repo.Readmes { 98 + ext := filepath.Ext(readme) 99 + content, _ := repo.FileContent(readme) 100 + if len(content) > 0 { 101 + switch ext { 102 + case ".md", ".markdown", ".mkd": 103 + var buf bytes.Buffer 104 + if cerr := markdown.Convert([]byte(content), &buf); cerr != nil { 105 + h.write500(w, cerr) 106 + return 107 + } 108 + readmeContents = template.HTML(buf.String()) 109 + default: 110 + readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content)) 111 + } 112 + break 113 + } 114 + } 115 + 116 + masterBranch, err := repo.FindMasterBranch(h.c.Repo.Masters) 117 + if err != nil { 118 + h.write500(w, err) 119 + return 120 + } 121 + 122 + desc, err := repo.Description() 123 + if err != nil { 124 + h.write500(w, err) 125 + return 126 + } 127 + 128 + commits, err := repo.Commits() 129 + if err != nil { 130 + h.write500(w, err) 131 + return 132 + } 133 + 134 + if len(commits) >= 4 { 135 + commits = commits[:3] 136 + } 137 + 138 + data := make(map[string]any) 139 + data["name"] = name 140 + data["ref"] = masterBranch 141 + data["desc"] = desc 142 + data["readme"] = readmeContents 143 + data["commits"] = commits 144 + data["servername"] = h.c.Meta.Host 145 + data["meta"] = h.c.Meta 146 + data["gomod"] = repo.IsGoMod() 147 + 148 + h.templ(w, "repo_index", data) 149 +} 150 + 151 +func (h *handlers) repoTree(w http.ResponseWriter, r *http.Request) { 152 + name := filepath.Clean(r.PathValue("name")) 153 + ref := r.PathValue("ref") 154 + treePath := r.PathValue("rest") 155 + 156 + repo, err := git.Open(filepath.Join(h.c.Repo.Dir, name), ref) 157 + if err != nil { 158 + h.write404(w, err) 159 + return 160 + } 161 + 162 + isPrivate, err := repo.IsPrivate() 163 + if isPrivate || err != nil { 164 + h.write404(w, err) 165 + return 166 + } 167 + 168 + desc, err := repo.Description() 169 + if err != nil { 170 + h.write500(w, err) 171 + return 172 + } 173 + 174 + files, err := repo.FileTree(treePath) 175 + if err != nil { 176 + h.write500(w, err) 177 + return 178 + } 179 + 180 + data := make(map[string]any) 181 + data["name"] = name 182 + data["ref"] = ref 183 + data["parent"] = treePath 184 + data["dotdot"] = filepath.Dir(treePath) 185 + data["desc"] = desc 186 + data["meta"] = h.c.Meta 187 + data["files"] = files 188 + 189 + h.templ(w, "repo_tree", data) 190 +} 191 + 192 +func (h *handlers) fileContents(w http.ResponseWriter, r *http.Request) { 193 + name := filepath.Clean(r.PathValue("name")) 194 + ref := r.PathValue("ref") 195 + treePath := r.PathValue("rest") 196 + 197 + var raw bool 198 + if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil { 199 + raw = rawParam 200 + } 201 + 202 + repo, err := git.Open(filepath.Join(h.c.Repo.Dir, name), "") 203 + if err != nil { 204 + h.write404(w, err) 205 + return 206 + } 207 + 208 + isPrivate, err := repo.IsPrivate() 209 + if isPrivate || err != nil { 210 + h.write404(w, err) 211 + return 212 + } 213 + 214 + desc, err := repo.Description() 215 + if err != nil { 216 + h.write500(w, err) 217 + return 218 + } 219 + 220 + contents, err := repo.FileContent(treePath) 221 + if err != nil { 222 + h.write500(w, err) 223 + return 224 + } 225 + 226 + data := make(map[string]any) 227 + data["name"] = name 228 + data["ref"] = ref 229 + data["desc"] = desc 230 + data["path"] = treePath 231 + 232 + if raw { 233 + w.WriteHeader(http.StatusOK) 234 + w.Header().Set("Content-Type", "text/plain") 235 + w.Write([]byte(contents)) 236 + return 237 + } 238 + 239 + lc, err := countLines(strings.NewReader(contents)) 240 + if err != nil { 241 + slog.Error("failed to count line numbers", "err", err) 242 + } 243 + 244 + lines := make([]int, lc) 245 + if lc > 0 { 246 + for i := range lines { 247 + lines[i] = i + 1 248 + } 249 + } 250 + 251 + data["linecount"] = lines 252 + data["content"] = contents 253 + data["meta"] = h.c.Meta 254 + 255 + h.templ(w, "file", data) 256 +} 257 + 258 +func (h *handlers) log(w http.ResponseWriter, r *http.Request) { 259 + name := filepath.Clean(r.PathValue("name")) 260 + ref := r.PathValue("ref") 261 + 262 + repo, err := git.Open(filepath.Join(h.c.Repo.Dir, name), ref) 263 + if err != nil { 264 + h.write404(w, err) 265 + return 266 + } 267 + 268 + isPrivate, err := repo.IsPrivate() 269 + if isPrivate || err != nil { 270 + h.write404(w, err) 271 + return 272 + } 273 + 274 + commits, err := repo.Commits() 275 + if err != nil { 276 + h.write500(w, err) 277 + return 278 + } 279 + 280 + desc, err := repo.Description() 281 + if err != nil { 282 + h.write500(w, err) 283 + return 284 + } 285 + 286 + data := make(map[string]any) 287 + data["name"] = name 288 + data["ref"] = ref 289 + data["desc"] = desc 290 + data["meta"] = h.c.Meta 291 + data["log"] = true 292 + data["commits"] = commits 293 + h.templ(w, "repo_log", data) 294 +} 295 + 296 +func (h *handlers) commit(w http.ResponseWriter, r *http.Request) { 297 + name := filepath.Clean(r.PathValue("name")) 298 + ref := r.PathValue("ref") 299 + 300 + repo, err := git.Open(filepath.Join(h.c.Repo.Dir, name), ref) 301 + if err != nil { 302 + h.write404(w, err) 303 + return 304 + } 305 + 306 + isPrivate, err := repo.IsPrivate() 307 + if isPrivate || err != nil { 308 + h.write404(w, err) 309 + return 310 + } 311 + 312 + diff, err := repo.Diff() 313 + if err != nil { 314 + h.write500(w, err) 315 + return 316 + } 317 + 318 + desc, err := repo.Description() 319 + if err != nil { 320 + h.write500(w, err) 321 + return 322 + } 323 + 324 + data := make(map[string]any) 325 + data["stat"] = diff.Stat 326 + data["diff"] = diff.Diff 327 + data["commit"] = diff.Commit 328 + data["name"] = name 329 + data["ref"] = ref 330 + data["desc"] = desc 331 + h.templ(w, "commit", data) 332 +} 333 + 334 +func (h *handlers) refs(w http.ResponseWriter, r *http.Request) { 335 + name := filepath.Clean(r.PathValue("name")) 336 + repo, err := git.Open(filepath.Join(h.c.Repo.Dir, name), "") 337 + if err != nil { 338 + h.write404(w, err) 339 + return 340 + } 341 + 342 + isPrivate, err := repo.IsPrivate() 343 + if isPrivate || err != nil { 344 + h.write404(w, err) 345 + return 346 + } 347 + 348 + desc, err := repo.Description() 349 + if err != nil { 350 + h.write500(w, err) 351 + return 352 + } 353 + 354 + branches, err := repo.Branches() 355 + if err != nil { 356 + h.write500(w, err) 357 + return 358 + } 359 + 360 + tags, err := repo.Tags() 361 + if err != nil { 362 + // repo should have at least one branch, tags are *optional* 363 + slog.Error("couldn't fetch repo tags", "err", err) 364 + } 365 + 366 + data := make(map[string]any) 367 + data["meta"] = h.c.Meta 368 + data["name"] = name 369 + data["desc"] = desc 370 + data["branches"] = branches 371 + data["tags"] = tags 372 + h.templ(w, "repo_refs", data) 373 +} 374 + 375 +func countLines(r io.Reader) (int, error) { 376 + buf := make([]byte, 32*1024) 377 + bufLen := 0 378 + count := 0 379 + nl := []byte{'\n'} 380 + 381 + for { 382 + c, err := r.Read(buf) 383 + if c > 0 { 384 + bufLen += c 385 + } 386 + count += bytes.Count(buf[:c], nl) 387 + 388 + switch { 389 + case err == io.EOF: 390 + // handle last line not having a newline at the end 391 + if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' { 392 + count++ 393 + } 394 + return count, nil 395 + case err != nil: 396 + return 0, err 397 + } 398 + } 399 +}
D
internal/handlers/routes.go
路路路 1 -package handlers 2 - 3 -import ( 4 - "log/slog" 5 - "net/http" 6 -) 7 - 8 -func (h *handlers) index(w http.ResponseWriter, r *http.Request) { 9 - data := make(map[string]any) 10 - data["meta"] = h.c.Meta 11 - 12 - w.WriteHeader(http.StatusOK) 13 - if err := h.t.ExecuteTemplate(w, "index", nil); err != nil { 14 - slog.Error("index template", "err", err) 15 - } 16 -}
M
internal/handlers/util.go
路路路 5 5 "net/http" 6 6 ) 7 7 8 -func (h *handlers) write404(w http.ResponseWriter) { 8 +func (h *handlers) templ(w http.ResponseWriter, name string, data any) { 9 + if err := h.t.ExecuteTemplate(w, name, data); err != nil { 10 + w.WriteHeader(http.StatusInternalServerError) 11 + slog.Error("template", "name", name, "err", err) 12 + } 13 +} 14 + 15 +func (h *handlers) write404(w http.ResponseWriter, err error) { 16 + slog.Info("404", "err", err) 9 17 w.WriteHeader(http.StatusNotFound) 10 - if err := h.t.ExecuteTemplate(w, "404", nil); err != nil { 11 - slog.Error("404 template", "err", err) 12 - } 18 + h.templ(w, "404", nil) 13 19 } 14 20 15 -func (h *handlers) write500(w http.ResponseWriter) { 21 +func (h *handlers) write500(w http.ResponseWriter, err error) { 22 + slog.Info("500", "err", err) 16 23 w.WriteHeader(http.StatusInternalServerError) 17 - if err := h.t.ExecuteTemplate(w, "500", nil); err != nil { 18 - slog.Error("500 template", "err", err) 19 - } 24 + h.templ(w, "500", nil) 20 25 }
M
main.go
路路路 29 29 mux := handlers.InitRoutes(cfg) 30 30 31 31 port := strconv.Itoa(cfg.Server.Port) 32 - err = http.ListenAndServe(net.JoinHostPort(cfg.Server.Host, port), mux) 33 - if err != nil { 32 + slog.Info("starting server", "host", cfg.Server.Host, "port", port) 33 + if err = http.ListenAndServe(net.JoinHostPort(cfg.Server.Host, port), mux); err != nil { 34 34 slog.Error("server error", "err", err) 35 35 } 36 36
M
templates/500.html
→ web/templates/500.html
路路路 1 1 {{ define "500" }} 2 2 <html> 3 - <title>500</title> 3 + <head> 4 + <title>500</title> 4 5 {{ template "head" . }} 6 + </head> 5 7 <body> 6 - {{ template "nav" . }} 8 + <!-- {{ template "nav" . }} --> 7 9 <main> 8 10 <h3>500 — something broke!</h3> 9 11 </main>
D
templates/_head.html
路路路 1 -{{ define "head" }} 2 - <head> 3 - <meta charset="utf-8"> 4 - <meta name="viewport" content="width=device-width, initial-scale=1"> 5 - <link rel="stylesheet" href="/static/style.css" type="text/css"> 6 - <!-- TODO: icon --> 7 - {{ if .parent }} 8 - <title>{{ .meta.Title }} — {{ .name }} ({{ .ref }}): {{ .parent }}/</title> 9 - 10 - {{ else if .path }} 11 - <title>{{ .meta.Title }} — {{ .name }} ({{ .ref }}): {{ .path }}</title> 12 - {{ else if .files }} 13 - <title>{{ .meta.Title }} — {{ .name }} ({{ .ref }})</title> 14 - {{ else if .commit }} 15 - <title>{{ .meta.Title }} — {{ .name }}: {{ .commit.This }}</title> 16 - {{ else if .branches }} 17 - <title>{{ .meta.Title }} — {{ .name }}: refs</title> 18 - {{ else if .commits }} 19 - {{ if .log }} 20 - <title>{{ .meta.Title }} — {{ .name }}: log</title> 21 - {{ else }} 22 - <title>{{ .meta.Title }} — {{ .name }}</title> 23 - {{ end }} 24 - {{ else }} 25 - <title>{{ .meta.Title }}</title> 26 - {{ end }} 27 - <!-- {{ if and .servername .gomod }} --> 28 - <!-- <meta name="go-import" content="{{ .servername}}/{{ .name }} git https://{{ .servername }}/{{ .name }}"> --> 29 - <!-- {{ end }} --> 30 - </head> 31 -{{ end }}
A
web/static/style.css
路路路 1 +:root { 2 + --white: #fff; 3 + --light: #f4f4f4; 4 + --cyan: #509c93; 5 + --light-gray: #eee; 6 + --medium-gray: #ddd; 7 + --gray: #6a6a6a; 8 + --dark: #444; 9 + --darker: #222; 10 + 11 + --sans-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif; 12 + --display-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif; 13 + --mono-font: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', 'Roboto Mono', Menlo, Consolas, monospace; 14 +} 15 + 16 +@media (prefers-color-scheme: dark) { 17 + :root { 18 + color-scheme: dark light; 19 + --light: #181818; 20 + --cyan: #76c7c0; 21 + --light-gray: #333; 22 + --medium-gray: #444; 23 + --gray: #aaa; 24 + --dark: #ddd; 25 + --darker: #f4f4f4; 26 + --white: #000; 27 + } 28 +} 29 + 30 +html { 31 + background: var(--white); 32 + -webkit-text-size-adjust: none; 33 + font-family: var(--sans-font); 34 + font-weight: 380; 35 +} 36 + 37 +pre { 38 + font-family: var(--mono-font); 39 + overflow-x: auto; 40 +} 41 + 42 +::selection { 43 + background: var(--medium-gray); 44 + opacity: 0.3; 45 +} 46 + 47 +* { 48 + box-sizing: border-box; 49 + padding: 0; 50 + margin: 0; 51 +} 52 + 53 +body { 54 + max-width: 1200px; 55 + padding: 0 13px; 56 + margin: 40px auto; 57 +} 58 + 59 +main, footer { 60 + font-size: 1rem; 61 + padding: 0; 62 + line-height: 160%; 63 +} 64 + 65 +header h1, h2, h3 { 66 + font-family: var(--display-font); 67 +} 68 + 69 +h2 { font-weight: 400; } 70 +strong { font-weight: 500; } 71 + 72 +main h1 { padding: 10px 0 10px 0; } 73 +main h2 { font-size: 18px; } 74 +main h3 { font-size: 1.15rem; } 75 +main h2, h3 { padding: 20px 0 0.4rem 0; } 76 + 77 +nav { padding: 0.4rem 0 1.5rem 0; } 78 +nav ul { 79 + padding: 0; 80 + margin: 0; 81 + list-style: none; 82 + padding-bottom: 20px; 83 +} 84 + 85 +nav ul li { 86 + padding-right: 10px; 87 + display: inline-block; 88 +} 89 + 90 +.repo-header { 91 + margin-bottom: 1.25rem; 92 +} 93 + 94 +.repo-breadcrumb { 95 + color: var(--gray); 96 +} 97 + 98 +.repo-breadcrumb a { 99 + color: var(--gray); 100 +} 101 + 102 +.repo-name { 103 + font-size: 1.6rem; 104 + line-height: 1.1; 105 + margin-top: 0.25rem; 106 +} 107 + 108 +.repo-header .desc { 109 + color: var(--gray); 110 + margin-top: 0.35rem; 111 +} 112 + 113 +.repo-header .repo-nav { 114 + padding: 0.5rem 0 0 0; 115 +} 116 + 117 +.repo-header .repo-nav ul { 118 + padding-bottom: 0; 119 +} 120 + 121 +a { 122 + margin: 0; 123 + padding: 0; 124 + box-sizing: border-box; 125 + text-decoration: none; 126 + word-wrap: break-word; 127 + color: var(--darker); 128 + border-bottom: 0; 129 +} 130 + 131 +a:hover { 132 + border-bottom: 1.5px solid var(--gray); 133 +} 134 + 135 +/* index page */ 136 +.index { 137 + width: 100%; 138 + margin-top: 2em; 139 + border-collapse: collapse; 140 + table-layout: auto; 141 +} 142 + 143 +.index th { 144 + text-align: left; 145 + font-weight: 500; 146 + border-bottom: 1.5px solid var(--medium-gray); 147 + padding: 0.25em 0.5em; 148 +} 149 + 150 +.index td { 151 + border-bottom: 1px solid var(--light-gray); 152 + padding: 0.25em 0.5em; 153 + vertical-align: top; 154 +} 155 + 156 +.index .url { white-space: nowrap; } 157 +.index .desc { width: 100%; } 158 +.index .idle { white-space: nowrap; } 159 + 160 +.index tbody tr.nohover:hover { 161 + background: transparent; 162 +} 163 + 164 +.index tbody tr:hover { 165 + background: var(--light); 166 +} 167 + 168 +/* tree page */ 169 + 170 +.tree { 171 + width: 100%; 172 + margin-bottom: 2em; 173 + border-collapse: collapse; 174 + table-layout: auto; 175 +} 176 + 177 +.tree th { 178 + text-align: left; 179 + font-weight: 500; 180 + border-bottom: 1.5px solid var(--medium-gray); 181 + padding: 0.25em 0.5em; 182 +} 183 + 184 +.tree td { 185 + border-bottom: 1px solid var(--light-gray); 186 + padding: 0.25em 0.5em; 187 + vertical-align: top; 188 +} 189 + 190 +.tree .mode { 191 + white-space: nowrap; 192 + font-family: var(--mono-font); 193 +} 194 + 195 +.tree .size { 196 + white-space: nowrap; 197 + text-align: right; 198 + font-family: var(--mono-font); 199 +} 200 + 201 +.tree .name { width: 100%; } 202 + 203 +.tree tbody tr.nohover:hover { 204 + background: transparent; 205 +} 206 + 207 +.tree tbody tr:hover { 208 + background: var(--light); 209 +} 210 + 211 +/* log/repo page */ 212 + 213 +.repo-index { 214 + display: flex; 215 + gap: 1.25rem; 216 + margin-bottom: 2em; 217 + align-items: flex-start; 218 +} 219 + 220 +.repo-index-main { 221 + flex: 0 0 72ch; 222 +} 223 + 224 +.repo-index-side { 225 + flex: 0 0 26rem; 226 + min-width: 0; 227 +} 228 + 229 +.repo-index-main .box { 230 + width: 100%; 231 +} 232 + 233 +.box { 234 + background: var(--light-gray); 235 + padding: 0.6rem; 236 +} 237 + 238 +.box + .box { 239 + margin-top: 0.8rem; 240 +} 241 + 242 +.log { 243 + width: 100%; 244 + margin-bottom: 2em; 245 + border-collapse: collapse; 246 + table-layout: auto; 247 +} 248 + 249 +.log th { 250 + text-align: left; 251 + font-weight: 500; 252 + border-bottom: 1.5px solid var(--medium-gray); 253 + padding: 0.25em 0.5em; 254 +} 255 + 256 +.log td { 257 + border-bottom: 1px solid var(--light-gray); 258 + padding: 0.25em 0.5em; 259 + vertical-align: top; 260 +} 261 + 262 +.log .msg { width: 100%; } 263 +.log .author { white-space: nowrap; position: relative; } 264 +.log .age { white-space: nowrap; } 265 + 266 +.log td.author .author-short { 267 + display: inline-block; 268 + max-width: 25ch; 269 + white-space: nowrap; 270 + overflow: hidden; 271 + text-overflow: ellipsis; 272 + vertical-align: top; 273 +} 274 + 275 +.log td.author .author-tip { 276 + display: none; 277 + position: absolute; 278 + left: 0; 279 + top: 100%; 280 + margin-top: 0.25rem; 281 + padding: 0.35rem 0.5rem; 282 + background: var(--white); 283 + border: 1px solid var(--medium-gray); 284 + max-width: 48ch; 285 + z-index: 2; 286 +} 287 + 288 +.log td.author:hover .author-tip, 289 +.log td.author:focus-within .author-tip { 290 + display: block; 291 +} 292 + 293 +.log tbody tr.nohover:hover { 294 + background: transparent; 295 +} 296 + 297 +.log tbody tr:hover { 298 + background: var(--light); 299 +} 300 + 301 +.clone-url pre { 302 + overflow-x: auto; 303 + white-space: pre; 304 + max-width: 100%; 305 +} 306 + 307 +.mode, .size { 308 + font-family: var(--mono-font); 309 +} 310 +.size { 311 + text-align: right; 312 +} 313 + 314 +/* readme stuff */ 315 + 316 +.readme pre { 317 + white-space: pre-wrap; 318 + overflow-x: auto; 319 +} 320 + 321 +.readme { 322 + background: var(--light-gray); 323 + padding: 0.5rem; 324 +} 325 + 326 +.readme ul { 327 + padding: revert; 328 +} 329 + 330 +.readme img { 331 + max-width: 100%; 332 +} 333 + 334 +.diff { 335 + margin: 1rem 0 1rem 0; 336 + padding: 0.6rem; 337 + border-bottom: 1.5px solid var(--medium-gray); 338 + background: var(--light-gray); 339 +} 340 + 341 +.diff pre { 342 + overflow-x: auto; 343 +} 344 + 345 +.commit-refs { 346 + border-collapse: collapse; 347 + margin: 0.5rem 0 1rem 0; 348 +} 349 + 350 +.commit-refs td { 351 + padding: 0.15rem 0.5rem 0.15rem 0; 352 + vertical-align: top; 353 +} 354 + 355 +.commit-refs td.label { 356 + white-space: nowrap; 357 + padding-right: 1rem; 358 +} 359 + 360 +.diff-stat { 361 + padding: 1rem 0 1rem 0; 362 +} 363 + 364 +.jump { 365 + margin-top: 0.5rem; 366 +} 367 + 368 +.jump-table { 369 + width: 100%; 370 + border-collapse: collapse; 371 + table-layout: auto; 372 + margin-top: 0.25rem; 373 +} 374 + 375 +.jump-table td { 376 + padding: 0.15rem 0.5rem; 377 + border-bottom: 1px solid var(--medium-gray); 378 + vertical-align: top; 379 +} 380 + 381 +.jump-table .diff-type { 382 + font-family: var(--mono-font); 383 + white-space: nowrap; 384 + width: 2ch; 385 +} 386 + 387 +.jump-table .path { 388 + width: 100%; 389 +} 390 + 391 +.commit-hash, .commit-email { 392 + font-family: var(--mono-font); 393 +} 394 + 395 +.commit-email:before { 396 + content: '<'; 397 +} 398 + 399 +.commit-email:after { 400 + content: '>'; 401 +} 402 + 403 +.commit { 404 + margin-bottom: 1rem; 405 +} 406 + 407 + .commit pre { 408 + padding-bottom: 0; 409 + white-space: pre-wrap; 410 + } 411 + 412 + .commit-message { 413 + margin-top: 0.25rem; 414 + font-size: 1rem; 415 + line-height: 1.35; 416 + margin-bottom: 0; 417 + } 418 + 419 + 420 + .commit .box { 421 + margin-bottom: 0.25rem; 422 + } 423 + 424 + 425 + .commit .commit-info { 426 + padding-bottom: 0.25rem; 427 + } 428 + 429 + 430 + 431 + 432 +.diff-add { 433 + color: green; 434 +} 435 + 436 +.diff-del { 437 + color: red; 438 +} 439 + 440 +.diff-noop { 441 + color: var(--gray); 442 +} 443 + 444 +.ref { 445 + font-family: var(--sans-font); 446 + font-size: 14px; 447 + color: var(--gray); 448 + display: inline-block; 449 + padding-top: 0.7em; 450 +} 451 + 452 +.refs pre { 453 + white-space: pre-wrap; 454 + padding-bottom: 0.5rem; 455 +} 456 + 457 +.refs strong { 458 + padding-right: 1em; 459 +} 460 + 461 +.line-numbers { 462 + white-space: pre-line; 463 + -moz-user-select: -moz-none; 464 + -khtml-user-select: none; 465 + -webkit-user-select: none; 466 + -o-user-select: none; 467 + user-select: none; 468 + display: flex; 469 + float: left; 470 + flex-direction: column; 471 + margin-right: 1ch; 472 +} 473 + 474 +.file-wrapper { 475 + display: flex; 476 + flex-direction: row; 477 + grid-template-columns: 1rem minmax(0, 1fr); 478 + gap: 1rem; 479 + padding: 0.5rem; 480 + background: var(--light-gray); 481 + overflow-x: auto; 482 +} 483 + 484 +.chroma-file-wrapper { 485 + display: flex; 486 + flex-direction: row; 487 + grid-template-columns: 1rem minmax(0, 1fr); 488 + overflow-x: auto; 489 +} 490 + 491 +.file-content { 492 + background: var(--light-gray); 493 + overflow-y: hidden; 494 + overflow-x: auto; 495 +} 496 + 497 +.diff-type { 498 + font-family: var(--mono-font); 499 +} 500 + 501 +.diff-type.diff-add { color: green; } 502 +.diff-type.diff-del { color: red; } 503 +.diff-type.diff-mod { color: var(--cyan); } 504 + 505 +.commit-info { 506 + color: var(--gray); 507 + font-size: 0.85rem; 508 +} 509 + 510 +.commit-date { 511 + float: right; 512 +} 513 + 514 +@media (max-width: 600px) { 515 + .index { 516 + grid-row-gap: 0.8em; 517 + } 518 + 519 + .repo-index { 520 + flex-direction: column; 521 + } 522 + 523 + .repo-index-main { 524 + flex: none; 525 + } 526 + 527 + .repo-index-side { 528 + flex: none; 529 + } 530 + 531 + .log { 532 + grid-template-columns: 1fr; 533 + grid-row-gap: 0em; 534 + } 535 + 536 + .index { 537 + grid-template-columns: 1fr; 538 + grid-row-gap: 0em; 539 + } 540 + 541 + .index-name:not(:first-child) { 542 + padding-top: 1.5rem; 543 + } 544 + 545 + .commit-info:not(:last-child) { 546 + padding-bottom: 1.5rem; 547 + } 548 + 549 + pre { 550 + font-size: 0.8rem; 551 + } 552 +}
A
web/templates/_head.html
路路路 1 +{{ define "head" }} 2 + <meta charset="utf-8"> 3 + <meta name="viewport" content="width=device-width, initial-scale=1"> 4 + <link rel="stylesheet" href="/static/style.css" type="text/css"> 5 + <!-- TODO: icon --> 6 + {{ if and .servername .gomod }} 7 + <meta name="go-import" content="{{ .servername}}/{{ .name }} git https://{{ .servername }}/{{ .name }}"> 8 + {{ end }} 9 +{{ end }}
A
web/templates/_repo_header.html
路路路 1 +{{ define "repo_header" }} 2 +<header class="repo-header"> 3 + <div class="repo-breadcrumb"> 4 + <a href="/">all repos</a> 5 + {{- if .ref }} 6 + <span class="ref">@ {{ .ref }}</span> 7 + {{- end }} 8 + </div> 9 + 10 + <h1 class="repo-name">{{ .name }}</h1> 11 + {{- if .desc }} 12 + <div class="desc">{{ .desc }}</div> 13 + {{- end }} 14 + 15 + <nav class="repo-nav"> 16 + <ul> 17 + {{- if .name }} 18 + <li><a href="/{{ .name }}">summary</a></li> 19 + <li><a href="/{{ .name }}/refs">refs</a></li> 20 + {{- if .ref }} 21 + <li><a href="/{{ .name }}/tree/{{ .ref }}/">tree</a></li> 22 + <li><a href="/{{ .name }}/log/{{ .ref }}">log</a></li> 23 + {{- end }} 24 + {{- end }} 25 + </ul> 26 + </nav> 27 +</header> 28 +{{ end }}
A
web/templates/file.html
路路路 1 +{{ define "file" }} 2 +<html> 3 + <head> 4 + {{ template "head" . }} 5 + </head> 6 + {{ template "repo_header" . }} 7 + <body> 8 + <main> 9 + <p>{{ .path }} (<a style="color: gray" href="?raw=true">view raw</a>)</p> 10 + {{if .chroma }} 11 + <div class="chroma-file-wrapper"> 12 + {{ .content }} 13 + </div> 14 + {{else}} 15 + <div class="file-wrapper"> 16 + <table> 17 + <tbody><tr> 18 + <td class="line-numbers"> 19 + <pre> 20 + {{- range .linecount }} 21 + <a id="L{{ . }}" href="#L{{ . }}">{{ . }}</a> 22 + {{- end -}} 23 + </pre> 24 + </td> 25 + <td class="file-content"> 26 + <pre> 27 + {{- .content -}} 28 + </pre> 29 + </td> 30 + </tbody></tr> 31 + </table> 32 + </div> 33 + {{end}} 34 + </main> 35 + </body> 36 +</html> 37 +{{ end }}
A
web/templates/index.html
路路路 1 +{{ define "index" }} 2 +<!DOCTYPE html> 3 +<html> 4 + <head> 5 + {{ template "head" . }} 6 + <title>{{ .meta.Title }}</title> 7 + </head> 8 + <header> 9 + <h1>{{ .meta.Title }}</h1> 10 + <h2>{{ .meta.Description }}</h2> 11 + </header> 12 + <body> 13 + <main> 14 + <table class="index"> 15 + <thead> 16 + <tr class="nohover"> 17 + <th class="url">Name</th> 18 + <th class="desc">Description</th> 19 + <th class="idle">Idle</th> 20 + </tr> 21 + </thead> 22 + <tbody> 23 + {{ range .repos }} 24 + <tr> 25 + <td class="url"><a href="/{{ .Name }}">{{ .Name }}</a></td> 26 + <td class="desc">{{ .Desc }}</td> 27 + <td class="idle">{{ .Idle }}</td> 28 + </tr> 29 + {{ end}} 30 + </tbody> 31 + </table> 32 + </main> 33 + </body> 34 +</html> 35 +{{ end }}
A
web/templates/repo_commit.html
路路路 1 +{{ define "commit" }} 2 +<html> 3 + <head> 4 + {{ template "head" . }} 5 + <title>{{ .name }}: {{ .commit.This }}</title> 6 + </head> 7 + {{ template "repo_header" . }} 8 + <body> 9 + <main> 10 + <section class="commit"> 11 + <div class="box"> 12 + <div class="commit-info"> 13 + <span class="commit-date">{{ .commit.Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</span> 14 + {{ .commit.Author.Name }} <a href="mailto:{{ .commit.Author.Email }}" class="commit-email">{{ .commit.Author.Email }}</a> 15 + </div> 16 + <pre class="commit-message">{{- .commit.Message -}}</pre> 17 + </div> 18 + 19 + <table class="commit-refs"> 20 + <tbody> 21 + <tr> 22 + <td class="label"><strong>commit</strong></td> 23 + <td> 24 + <span class="commit-hash">{{ .commit.This }}</span> 25 + </td> 26 + </tr> 27 + {{ if .commit.Parent }} 28 + <tr> 29 + <td class="label"><strong>parent</strong></td> 30 + <td> 31 + <span class="commit-hash">{{ .commit.Parent }}</span> 32 + </td> 33 + </tr> 34 + {{ end }} 35 + </tbody> 36 + </table> 37 + 38 + <div class="diff-stat"> 39 + <div> 40 + {{ .stat.FilesChanged }} files changed, 41 + {{ .stat.Insertions }} insertions(+), 42 + {{ .stat.Deletions }} deletions(-) 43 + </div> 44 + 45 + <div class="jump"> 46 + <strong>jump to</strong> 47 + <table class="jump-table"> 48 + <tbody> 49 + {{ range .diff }} 50 + {{ $path := .Name.New }} 51 + {{ if not $path }}{{ $path = .Name.Old }}{{ end }} 52 + <tr> 53 + <td class="diff-type"> 54 + {{ if .IsNew }}<span class="diff-type diff-add">A</span>{{ end }} 55 + {{ if .IsDelete }}<span class="diff-type diff-del">D</span>{{ end }} 56 + {{ if not (or .IsNew .IsDelete) }}<span class="diff-type diff-mod">M</span>{{ end }} 57 + </td> 58 + <td class="path"><a href="#{{ $path }}">{{ $path }}</a></td> 59 + </tr> 60 + {{ end }} 61 + </tbody> 62 + </table> 63 + </div> 64 + </div> 65 + </section> 66 + <section> 67 + {{ $repo := .name }} 68 + {{ $this := .commit.This }} 69 + {{ $parent := .commit.Parent }} 70 + {{ range .diff }} 71 + {{ $path := .Name.New }} 72 + {{ if not $path }}{{ $path = .Name.Old }}{{ end }} 73 + <div id="{{ $path }}"> 74 + <div class="diff"> 75 + {{ if .IsNew }} 76 + <span class="diff-type diff-add">A</span> 77 + {{ end }} 78 + {{ if .IsDelete }} 79 + <span class="diff-type diff-del">D</span> 80 + {{ end }} 81 + {{ if not (or .IsNew .IsDelete) }} 82 + <span class="diff-type diff-mod">M</span> 83 + {{ end }} 84 + {{ if .Name.Old }} 85 + <a href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}">{{ .Name.Old }}</a> 86 + {{ if .Name.New }} 87 + → 88 + <a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a> 89 + {{ end }} 90 + {{ else }} 91 + <a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a> 92 + {{- end -}} 93 + {{ if .IsBinary }} 94 + <p>Not showing binary file.</p> 95 + {{ else }} 96 + <pre> 97 + {{- range .TextFragments -}} 98 + <p>{{- .Header -}}</p> 99 + {{- range .Lines -}} 100 + {{- if eq .Op.String "+" -}} 101 + <span class="diff-add">{{ .String }}</span> 102 + {{- end -}} 103 + {{- if eq .Op.String "-" -}} 104 + <span class="diff-del">{{ .String }}</span> 105 + {{- end -}} 106 + {{- if eq .Op.String " " -}} 107 + <span class="diff-noop">{{ .String }}</span> 108 + {{- end -}} 109 + {{- end -}} 110 + {{- end -}} 111 + {{- end -}} 112 + </pre> 113 + </div> 114 + </div> 115 + {{ end }} 116 + </section> 117 + </main> 118 + </body> 119 +</html> 120 +{{ end }}
A
web/templates/repo_index.html
路路路 1 +{{ define "repo_index" }} 2 +<!DOCTYPE html> 3 +<html> 4 + <head> 5 + {{ template "head" . }} 6 + <title>{{ .name }} — {{ .meta.Title }}</title> 7 + </head> 8 + <body> 9 + {{ template "repo_header" . }} 10 + 11 + <main> 12 + {{ $repo := .name }} 13 + 14 + <section class="repo-index"> 15 + <div class="repo-index-main"> 16 + {{ range .commits }} 17 + <div class="box"> 18 + <div> 19 + <a href="/{{ $repo }}/commit/{{ .Hash.String }}" class="commit-hash">{{ slice .Hash.String 0 8 }}</a> 20 + — {{ .Author.Name }} 21 + <span class="commit-date commit-info">{{ .Author.When.Format "Mon, 02 Jan 2006" }}</span> 22 + </div> 23 + <div>{{ commitSummary .Message }}</div> 24 + </div> 25 + {{ end }} 26 + </div> 27 + 28 + <aside class="repo-index-side"> 29 + <div class="box"> 30 + <strong>clone</strong> 31 + <pre>{{- /**/ -}} 32 +https://{{ .servername }}/{{ .name }} 33 +git@{{ .servername }}:{{ .name }} 34 +{{- /**/ -}}</pre> 35 + </div> 36 + </aside> 37 + </section> 38 + 39 + {{- if .readme }} 40 + <article class="readme"> 41 + {{- .readme -}} 42 + </article> 43 + {{- end -}} 44 + </main> 45 + </body> 46 +</html> 47 +{{ end }}
A
web/templates/repo_log.html
路路路 1 +{{ define "repo_log" }} 2 +<html> 3 + <head> 4 + {{ template "head" . }} 5 + <title>{{ .name }}: log</title> 6 + </head> 7 + {{ template "repo_header" . }} 8 + <body> 9 + <main> 10 + {{ $repo := .name }} 11 + 12 + <table class="log"> 13 + <thead> 14 + <tr class="nohover"> 15 + <th class="msg">commit</th> 16 + <th class="author">author</th> 17 + <th class="age">age</th> 18 + </tr> 19 + </thead> 20 + <tbody> 21 + {{ range .commits }} 22 + <tr> 23 + <td class="msg"> 24 + <a href="/{{ $repo }}/commit/{{ .Hash.String }}">{{ commitSummary .Message }}</a> 25 + </td> 26 + <td class="author"> 27 + <span class="author-short"> 28 + {{ .Author.Name }} <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a> 29 + </span> 30 + <span class="author-tip" role="tooltip"> 31 + <strong>{{ .Author.Name }}</strong><br> 32 + <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a> 33 + </span> 34 + </td> 35 + <td class="age">{{ humanTime .Committer.When }}</td> 36 + </tr> 37 + {{ end }} 38 + </tbody> 39 + </table> 40 + </main> 41 + </body> 42 +</html> 43 +{{ end }}
A
web/templates/repo_refs.html
路路路 1 +{{ define "repo_refs" }} 2 +<html> 3 + <head> 4 + {{ template "head" . }} 5 + <title>{{ .name }}: refs</title> 6 + </head> 7 + {{ template "repo_header" . }} 8 + <body> 9 + <main> 10 + {{ $name := .name }} 11 + <h3>branches</h3> 12 + <div class="refs"> 13 + {{ range .branches }} 14 + <div> 15 + <strong>{{ .Name.Short }}</strong> 16 + <a href="/{{ $name }}/tree/{{ .Name.Short }}/">browse</a> 17 + <a href="/{{ $name }}/log/{{ .Name.Short }}">log</a> 18 + <a href="/{{ $name }}/archive/{{ .Name.Short }}.tar.gz">tar.gz</a> 19 + </div> 20 + {{ end }} 21 + </div> 22 + {{ if .tags }} 23 + <h3>tags</h3> 24 + <div class="refs"> 25 + {{ range .tags }} 26 + <div> 27 + <strong>{{ .Name }}</strong> 28 + <a href="/{{ $name }}/tree/{{ .Name }}/">browse</a> 29 + <a href="/{{ $name }}/log/{{ .Name }}">log</a> 30 + {{ if .Message }} 31 + <pre>{{ .Message }}</pre> 32 + </div> 33 + {{ end }} 34 + {{ end }} 35 + </div> 36 + {{ end }} 37 + </main> 38 + </body> 39 +</html> 40 +{{ end }}
A
web/templates/repo_tree.html
路路路 1 +{{ define "repo_tree" }} 2 +<html> 3 + <head> 4 + {{ template "head" . }} 5 + <title>{{ .name }}: tree ({{ .ref }})</title> 6 + </head> 7 + {{ template "repo_header" . }} 8 + <body> 9 + <main> 10 + {{ $repo := .name }} 11 + {{ $ref := .ref }} 12 + {{ $parent := .parent }} 13 + 14 + <table class="tree"> 15 + <thead> 16 + <tr class="nohover"> 17 + <th class="mode">mode</th> 18 + <th class="size">size</th> 19 + <th class="name">name</th> 20 + </tr> 21 + </thead> 22 + <tbody> 23 + {{ if $parent }} 24 + <tr> 25 + <td class="mode"></td> 26 + <td class="size"></td> 27 + <td class="name"><a href="/{{ $repo }}/tree/{{ $ref }}/{{ .dotdot }}">..</a></td> 28 + </tr> 29 + {{ end }} 30 + 31 + {{ range .files }} 32 + {{ if not .IsFile }} 33 + <tr> 34 + <td class="mode">{{ .Mode }}</td> 35 + <td class="size">{{ .Size }}</td> 36 + <td class="name"> 37 + {{ if $parent }} 38 + <a href="/{{ $repo }}/tree/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}/</a> 39 + {{ else }} 40 + <a href="/{{ $repo }}/tree/{{ $ref }}/{{ .Name }}">{{ .Name }}/</a> 41 + {{ end }} 42 + </td> 43 + </tr> 44 + {{ end }} 45 + {{ end }} 46 + 47 + {{ range .files }} 48 + {{ if .IsFile }} 49 + <tr> 50 + <td class="mode">{{ .Mode }}</td> 51 + <td class="size">{{ .Size }}</td> 52 + <td class="name"> 53 + {{ if $parent }} 54 + <a href="/{{ $repo }}/blob/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}</a> 55 + {{ else }} 56 + <a href="/{{ $repo }}/blob/{{ $ref }}/{{ .Name }}">{{ .Name }}</a> 57 + {{ end }} 58 + </td> 59 + </tr> 60 + {{ end }} 61 + {{ end }} 62 + </tbody> 63 + </table> 64 + 65 + <article> 66 + <pre> 67 + {{- if .readme }}{{ .readme }}{{- end -}} 68 + </pre> 69 + </article> 70 + </main> 71 + </body> 72 +</html> 73 +{{ end }}
A
web/web.go
路路路 1 +package web 2 + 3 +import ( 4 + "embed" 5 + "io/fs" 6 +) 7 + 8 +var ( 9 + //go:embed templates/* static/* 10 + allFS embed.FS 11 + TemplatesFS = fsSub(allFS, "templates") 12 + StaticFS = fsSub(allFS, "static") 13 +) 14 + 15 +func fsSub(fsys fs.FS, dir string) fs.FS { 16 + f, err := fs.Sub(fsys, dir) 17 + if err != nil { 18 + panic(err) 19 + } 20 + return f 21 +}