all repos

mugit @ a0a9d4ac6fd9080169581577fc9ba0d2da6a5807

馃惍 git server that your cow will love
7 files changed, 108 insertions(+), 126 deletions(-)
some refactoring
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.htmlweb/templates/repo_file.html
路路路
        1
        
        -{{ define "file" }}

      
        
        1
        +{{ define "repo_file" }}

      
        2
        2
         <html>

      
        3
        3
           <head>

      
        4
        4
           {{ template "head" . }}

      
M web/templates/index.html
路路路
        24
        24
                   <tr>

      
        25
        25
                     <td class="url"><a href="/{{ .Name }}">{{ .Name }}</a></td>

      
        26
        26
                     <td class="desc">{{ .Desc }}</td>

      
        27
        
        -            <td class="idle">{{ .Idle }}</td>

      
        
        27
        +            <td class="idle">{{ humanTime .LastCommit }}</td>

      
        28
        28
                   </tr>

      
        29
        29
                   {{ end}}

      
        30
        30
                 </tbody>