all repos

mugit @ 435b61e3b9036a837f226829338ba651722c8cc4

馃惍 git server that your cow will love
28 files changed, 1530 insertions(+), 153 deletions(-)
ui: add ui
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
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.mod
路路路
        4
        4
         

      
        5
        5
         require (

      
        6
        6
         	github.com/bluekeyes/go-gitdiff v0.8.1

      
        
        7
        +	github.com/dustin/go-humanize v1.0.1

      
        7
        8
         	github.com/go-git/go-git/v5 v5.16.4

      
        
        9
        +	github.com/yuin/goldmark v1.7.16

      
        8
        10
         	gopkg.in/yaml.v2 v2.4.0

      
        9
        11
         )

      
        10
        12
         

      
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
         

      
D templates/404.html
路路路
        1
        
        -{{ define "404 "}}

      
        2
        
        -<html>

      
        3
        
        -  <title>404</title>

      
        4
        
        -{{ template "head" . }}

      
        5
        
        -  <body>

      
        6
        
        -    {{ template "nav" . }}

      
        7
        
        -    <main>

      
        8
        
        -      <h3>404 &mdash; nothing like that here.</h3>

      
        9
        
        -    </main>

      
        10
        
        -  </body>

      
        11
        
        -</html>

      
        12
        
        -{{ end }}

      
M templates/500.htmlweb/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 &mdash; 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 }} &mdash; {{ .name }} ({{ .ref }}): {{ .parent }}/</title>

      
        9
        
        -

      
        10
        
        -    {{ else if .path }}

      
        11
        
        -    <title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .path }}</title>

      
        12
        
        -    {{ else if .files }}

      
        13
        
        -    <title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }})</title>

      
        14
        
        -    {{ else if .commit }}

      
        15
        
        -    <title>{{ .meta.Title }} &mdash; {{ .name }}: {{ .commit.This }}</title>

      
        16
        
        -    {{ else if .branches }}

      
        17
        
        -    <title>{{ .meta.Title }} &mdash; {{ .name }}: refs</title>

      
        18
        
        -    {{ else if .commits }}

      
        19
        
        -    {{ if .log }}

      
        20
        
        -    <title>{{ .meta.Title }} &mdash; {{ .name }}: log</title>

      
        21
        
        -    {{ else }}

      
        22
        
        -    <title>{{ .meta.Title }} &mdash; {{ .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 }}

      
M templates/_nav.htmlweb/templates/_repo_nav.html
路路路
        1
        
        -{{ define "nav" }}

      
        
        1
        +{{ define "repo_nav" }}

      
        2
        2
           <nav>

      
        3
        3
             <ul>

      
        4
        4
             {{ if .name }}

      
D templates/index.html
路路路
        1
        
        -{{ define "index" }}

      
        2
        
        -<html>

      
        3
        
        -{{ template "head" . }}

      
        4
        
        -  <header>

      
        5
        
        -    <h1>{{ .meta.Title }}</h1>

      
        6
        
        -    <h2>{{ .meta.Description }}</h2>

      
        7
        
        -  </header>

      
        8
        
        -  <body>

      
        9
        
        -        hi

      
        10
        
        -  </body>

      
        11
        
        -</html>

      
        12
        
        -{{ 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/404.html
路路路
        
        1
        +{{ define "404" }}

      
        
        2
        +<html>

      
        
        3
        +  <head>

      
        
        4
        +    <title>404</title>

      
        
        5
        +{{ template "head" . }}

      
        
        6
        +  </head>

      
        
        7
        +  <body>

      
        
        8
        +    <!-- {{ template "nav" . }} -->

      
        
        9
        +    <main>

      
        
        10
        +      <h3>404 &mdash; nothing like that here.</h3>

      
        
        11
        +    </main>

      
        
        12
        +  </body>

      
        
        13
        +</html>

      
        
        14
        +{{ end }}

      
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
        +            &#8594; 

      
        
        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 }} &mdash; {{ .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
        +              &mdash; {{ .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
        +}