7 files changed,
108 insertions(+),
126 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-01-23 00:22:58 +0200
Authored at:
2026-01-22 23:26:38 +0200
Change ID:
ttwlklypxyvnwqwxssuqqnzstuyspspk
Parent:
10fc8ef
M
internal/git/gitservice/gitservice.go
路路路 64 64 }) 65 65 } 66 66 67 -func ReceivePack(dir string, in io.Reader, out, stderr io.Writer) error { 67 +func ReceivePack(dir string, in io.Reader, out, errout io.Writer) error { 68 68 return gitCmd("receive-pack", config{ 69 69 Dir: dir, 70 70 Stdin: in, 71 71 Stdout: out, 72 - Stderr: stderr, 72 + Stderr: errout, 73 73 }) 74 74 } 75 75
M
internal/git/repo.go
路路路 151 151 152 152 func (g *Repo) Description() (string, error) { 153 153 // TODO: ??? Support both mugit.description and /description file 154 - 155 154 path := filepath.Join(g.path, "description") 156 155 if _, err := os.Stat(path); err != nil { 157 156 return "", fmt.Errorf("no description file found")
M
internal/handlers/git.go
路路路 2 2 3 3 import ( 4 4 "compress/gzip" 5 - "fmt" 6 5 "io" 7 6 "log/slog" 8 7 "net/http" 9 8 "path/filepath" 10 9 11 - "olexsmir.xyz/mugit/internal/git" 12 10 "olexsmir.xyz/mugit/internal/git/gitservice" 13 11 ) 14 12 路路路 17 15 func (h *handlers) multiplex(w http.ResponseWriter, r *http.Request) { 18 16 if r.URL.RawQuery == "service=git-receive-pack" { 19 17 w.WriteHeader(http.StatusBadRequest) 20 - w.Write([]byte("http pushing isn't supported")) 18 + w.Write([]byte("http pushing is not supported")) 21 19 return 22 20 } 23 21 路路路 42 40 w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") 43 41 w.WriteHeader(http.StatusOK) 44 42 45 - if err := gitservice.InfoRefs( 46 - filepath.Join(h.c.Repo.Dir, name), // FIXME: use securejoin 47 - w, 48 - ); err != nil { 43 + path := filepath.Join(h.c.Repo.Dir, filepath.Clean(name)) 44 + if err := gitservice.InfoRefs(path, w); err != nil { 49 45 slog.Error("git: info/refs", "err", err) 50 46 return 51 47 } 路路路 68 64 if r.Header.Get("Content-Encoding") == "gzip" { 69 65 gr, err := gzip.NewReader(r.Body) 70 66 if err != nil { 67 + w.WriteHeader(http.StatusInternalServerError) 71 68 slog.Error("git: gzip reader", "err", err) 72 69 return 73 70 } 路路路 75 72 reader = gr 76 73 } 77 74 78 - if err := gitservice.UploadPack( 79 - filepath.Join(h.c.Repo.Dir, name), 80 - true, 81 - reader, 82 - newFlushWriter(w), 83 - ); err != nil { 75 + path := filepath.Join(h.c.Repo.Dir, filepath.Clean(name)) 76 + if err := gitservice.UploadPack(path, true, reader, newFlushWriter(w)); err != nil { 84 77 slog.Error("git: upload-pack", "err", err) 85 78 return 86 79 } 87 -} 88 - 89 -func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) { 90 - n := filepath.Clean(name) 91 - repo, err := git.Open(filepath.Join(h.c.Repo.Dir, n), ref) 92 - if err != nil { 93 - return nil, err 94 - } 95 - 96 - isPrivate, err := repo.IsPrivate() 97 - if err != nil { 98 - return nil, err 99 - } 100 - 101 - if isPrivate { 102 - return nil, fmt.Errorf("repo is private") 103 - } 104 - 105 - return repo, nil 106 80 } 107 81 108 82 type flushWriter struct {
M
internal/handlers/handlers.go
路路路 25 25 h := handlers{cfg, tmpls} 26 26 27 27 mux := http.NewServeMux() 28 - mux.HandleFunc("GET /", h.index) 28 + mux.HandleFunc("GET /", h.indexHandler) 29 29 mux.HandleFunc("GET /static/{file}", h.serveStatic) 30 30 mux.HandleFunc("GET /{name}", h.multiplex) 31 31 mux.HandleFunc("POST /{name}", h.multiplex) 32 32 mux.HandleFunc("GET /{name}/{rest...}", h.multiplex) 33 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) 34 + mux.HandleFunc("GET /{name}/tree/{ref}/{rest...}", h.repoTreeHandler) 35 + mux.HandleFunc("GET /{name}/blob/{ref}/{rest...}", h.fileContentsHandler) 36 + mux.HandleFunc("GET /{name}/log/{ref}", h.logHandler) 37 + mux.HandleFunc("GET /{name}/commit/{ref}", h.commitHandler) 38 + mux.HandleFunc("GET /{name}/refs/{$}", h.refsHandler) 39 39 return mux 40 40 } 41 - 42 41 43 42 func (h *handlers) serveStatic(w http.ResponseWriter, r *http.Request) { 44 43 f := filepath.Clean(r.PathValue("file"))
M
internal/handlers/repo.go
路路路 2 2 3 3 import ( 4 4 "bytes" 5 + "errors" 5 6 "fmt" 6 7 "html/template" 7 8 "io" 路路路 17 18 "github.com/yuin/goldmark" 18 19 "github.com/yuin/goldmark/extension" 19 20 "github.com/yuin/goldmark/renderer/html" 20 - "olexsmir.xyz/mugit/internal/humanize" 21 + "olexsmir.xyz/mugit/internal/git" 21 22 ) 22 23 23 -func (h *handlers) index(w http.ResponseWriter, r *http.Request) { 24 - dirs, err := os.ReadDir(h.c.Repo.Dir) 24 +func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) { 25 + repos, err := h.listPublicRepos() 25 26 if err != nil { 26 27 h.write500(w, err) 27 28 return 28 29 } 29 30 30 - type repoInfo struct { 31 - Name, Desc, Idle string 32 - t time.Time 33 - } 34 - 35 - repoInfos := []repoInfo{} 36 - for _, dir := range dirs { 37 - if !dir.IsDir() { 38 - continue 39 - } 40 - 41 - name := dir.Name() 42 - repo, err := h.openPublicRepo(name, "") 43 - if err != nil { 44 - slog.Error("", "name", name, "err", err) 45 - continue 46 - } 47 - 48 - desc, err := repo.Description() 49 - if err != nil { 50 - slog.Error("", "err", err) 51 - continue 52 - } 53 - 54 - lastComit, err := repo.LastCommit() 55 - if err != nil { 56 - slog.Error("", "err", err) 57 - continue 58 - } 59 - 60 - repoInfos = append(repoInfos, repoInfo{ 61 - Name: name, 62 - Desc: desc, 63 - Idle: humanize.Time(lastComit.Author.When), 64 - t: lastComit.Author.When, 65 - }) 66 - } 67 - 68 - sort.Slice(repoInfos, func(i, j int) bool { 69 - return repoInfos[j].t.Before(repoInfos[i].t) 70 - }) 71 - 72 31 data := make(map[string]any) 73 32 data["meta"] = h.c.Meta 74 - data["repos"] = repoInfos 33 + data["repos"] = repos 75 34 h.templ(w, "index", data) 76 35 } 77 36 路路路 145 104 h.templ(w, "repo_index", data) 146 105 } 147 106 148 -func (h *handlers) repoTree(w http.ResponseWriter, r *http.Request) { 107 +func (h *handlers) repoTreeHandler(w http.ResponseWriter, r *http.Request) { 149 108 name := r.PathValue("name") 150 109 ref := r.PathValue("ref") 151 110 treePath := r.PathValue("rest") 路路路 156 115 return 157 116 } 158 117 159 - isPrivate, err := repo.IsPrivate() 160 - if isPrivate || err != nil { 161 - h.write404(w, err) 162 - return 163 - } 164 - 165 118 desc, err := repo.Description() 166 119 if err != nil { 167 120 h.write500(w, err) 路路路 186 139 h.templ(w, "repo_tree", data) 187 140 } 188 141 189 -func (h *handlers) fileContents(w http.ResponseWriter, r *http.Request) { 142 +func (h *handlers) fileContentsHandler(w http.ResponseWriter, r *http.Request) { 190 143 name := r.PathValue("name") 191 144 ref := r.PathValue("ref") 192 145 treePath := r.PathValue("rest") 路路路 214 167 return 215 168 } 216 169 217 - data := make(map[string]any) 218 - data["name"] = name 219 - data["ref"] = ref 220 - data["desc"] = desc 221 - data["path"] = treePath 222 - 223 170 if raw { 224 171 w.WriteHeader(http.StatusOK) 225 172 w.Header().Set("Content-Type", "text/plain") 226 173 w.Write([]byte(contents)) 227 174 return 228 175 } 176 + 177 + data := make(map[string]any) 178 + data["name"] = name 179 + data["ref"] = ref 180 + data["desc"] = desc 181 + data["path"] = treePath 229 182 230 183 lc, err := countLines(strings.NewReader(contents)) 231 184 if err != nil { 路路路 243 196 data["content"] = contents 244 197 data["meta"] = h.c.Meta 245 198 246 - h.templ(w, "file", data) 199 + h.templ(w, "repo_file", data) 247 200 } 248 201 249 -func (h *handlers) log(w http.ResponseWriter, r *http.Request) { 202 +func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) { 250 203 name := r.PathValue("name") 251 204 ref := r.PathValue("ref") 252 205 路路路 256 209 return 257 210 } 258 211 259 - isPrivate, err := repo.IsPrivate() 260 - if isPrivate || err != nil { 261 - h.write404(w, err) 262 - return 263 - } 264 - 265 212 commits, err := repo.Commits() 266 213 if err != nil { 267 214 h.write500(w, err) 路路路 284 231 h.templ(w, "repo_log", data) 285 232 } 286 233 287 -func (h *handlers) commit(w http.ResponseWriter, r *http.Request) { 234 +func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) { 288 235 name := r.PathValue("name") 289 236 ref := r.PathValue("ref") 290 237 repo, err := h.openPublicRepo(name, ref) 路路路 293 240 return 294 241 } 295 242 296 - isPrivate, err := repo.IsPrivate() 297 - if isPrivate || err != nil { 298 - h.write404(w, err) 299 - return 300 - } 301 - 302 243 diff, err := repo.Diff() 303 244 if err != nil { 304 245 h.write500(w, err) 路路路 321 262 h.templ(w, "commit", data) 322 263 } 323 264 324 -func (h *handlers) refs(w http.ResponseWriter, r *http.Request) { 265 +func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) { 325 266 name := r.PathValue("name") 326 267 repo, err := h.openPublicRepo(name, "") 327 268 if err != nil { 328 - h.write404(w, err) 329 - return 330 - } 331 - 332 - isPrivate, err := repo.IsPrivate() 333 - if isPrivate || err != nil { 334 269 h.write404(w, err) 335 270 return 336 271 } 路路路 387 322 } 388 323 } 389 324 } 325 + 326 +var errPrivateRepo = errors.New("privat err") 327 + 328 +func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) { 329 + n := filepath.Clean(name) 330 + repo, err := git.Open(filepath.Join(h.c.Repo.Dir, n), ref) 331 + if err != nil { 332 + return nil, err 333 + } 334 + 335 + isPrivate, err := repo.IsPrivate() 336 + if err != nil { 337 + return nil, err 338 + } 339 + if isPrivate { 340 + return nil, errPrivateRepo 341 + } 342 + 343 + return repo, nil 344 +} 345 + 346 +type repoList struct { 347 + Name string 348 + Desc string 349 + LastCommit time.Time 350 +} 351 + 352 +func (h *handlers) listPublicRepos() ([]repoList, error) { 353 + dirs, err := os.ReadDir(h.c.Repo.Dir) 354 + if err != nil { 355 + return nil, err 356 + } 357 + 358 + var repos []repoList 359 + var errs []error 360 + for _, dir := range dirs { 361 + if !dir.IsDir() { 362 + continue 363 + } 364 + 365 + name := dir.Name() 366 + repo, err := h.openPublicRepo(name, "") 367 + if err != nil { 368 + if errors.Is(err, errPrivateRepo) { 369 + continue 370 + } 371 + errs = append(errs, err) 372 + continue 373 + } 374 + 375 + desc, err := repo.Description() 376 + if err != nil { 377 + errs = append(errs, err) 378 + continue 379 + } 380 + 381 + lastComit, err := repo.LastCommit() 382 + if err != nil { 383 + errs = append(errs, err) 384 + continue 385 + } 386 + 387 + repos = append(repos, repoList{ 388 + Name: name, 389 + Desc: desc, 390 + LastCommit: lastComit.Author.When, 391 + }) 392 + } 393 + 394 + sort.Slice(repos, func(i, j int) bool { 395 + return repos[j].LastCommit.Before(repos[i].LastCommit) 396 + }) 397 + 398 + return repos, errors.Join(errs...) 399 +}
M
web/templates/file.html
→ web/templates/repo_file.html
路路路 1 -{{ define "file" }} 1 +{{ define "repo_file" }} 2 2 <html> 3 3 <head> 4 4 {{ template "head" . }}