all repos

mugit @ deb6f005148c166e8219a4219b01f6fb57cab220

🐮 git server that your cow will love
5 files changed, 126 insertions(+), 39 deletions(-)
show binary files
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-02-14 17:02:46 +0200
Authored at: 2026-02-14 16:02:09 +0200
Change ID: sltryzxnkwwzuvxsqnnmposuynqytlrr
Parent: 2487773
M internal/git/repo.go
···
        160
        160
         	return newCommit(c), nil

      
        161
        161
         }

      
        162
        162
         

      
        163
        
        -func (g *Repo) FileContent(path string) (string, error) {

      
        164
        
        -	c, err := g.r.CommitObject(g.h)

      
        165
        
        -	if err != nil {

      
        166
        
        -		return "", fmt.Errorf("commit object: %w", err)

      
        167
        
        -	}

      
        168
        
        -

      
        169
        
        -	tree, err := c.Tree()

      
        170
        
        -	if err != nil {

      
        171
        
        -		return "", fmt.Errorf("file tree: %w", err)

      
        172
        
        -	}

      
        173
        
        -

      
        174
        
        -	file, err := tree.File(path)

      
        175
        
        -	if err != nil {

      
        176
        
        -		if errors.Is(err, object.ErrFileNotFound) {

      
        177
        
        -			return "", ErrFileNotFound

      
        178
        
        -		}

      
        179
        
        -		return "", err

      
        180
        
        -	}

      
        181
        
        -

      
        182
        
        -	isbin, _ := file.IsBinary()

      
        183
        
        -	if !isbin {

      
        184
        
        -		return file.Contents()

      
        185
        
        -	} else {

      
        186
        
        -		return "Not displaying binary file", nil

      
        187
        
        -	}

      
        188
        
        -}

      
        189
        
        -

      
        190
        163
         type Branch struct {

      
        191
        164
         	Name       string

      
        192
        165
         	LastUpdate time.Time

      
M internal/git/tree.go
···
        1
        1
         package git

      
        2
        2
         

      
        3
        3
         import (

      
        
        4
        +	"errors"

      
        4
        5
         	"fmt"

      
        
        6
        +	"io"

      
        
        7
        +	"mime"

      
        
        8
        +	"path/filepath"

      
        
        9
        +	"strings"

      
        5
        10
         

      
        6
        11
         	"github.com/go-git/go-git/v5/plumbing/object"

      
        7
        12
         )

      ···
        60
        65
         

      
        61
        66
         	return files, nil

      
        62
        67
         }

      
        
        68
        +

      
        
        69
        +type FileContent struct {

      
        
        70
        +	IsBinary bool

      
        
        71
        +	Content  []byte

      
        
        72
        +	Mime     string

      
        
        73
        +	Size     int64

      
        
        74
        +}

      
        
        75
        +

      
        
        76
        +func (fc FileContent) IsImage() bool {

      
        
        77
        +	return strings.HasPrefix(fc.Mime, "image/")

      
        
        78
        +}

      
        
        79
        +

      
        
        80
        +func (fc *FileContent) String() string {

      
        
        81
        +	if fc.IsBinary {

      
        
        82
        +		return ""

      
        
        83
        +	}

      
        
        84
        +	return string(fc.Content)

      
        
        85
        +}

      
        
        86
        +

      
        
        87
        +func (g *Repo) FileContent(path string) (*FileContent, error) {

      
        
        88
        +	c, err := g.r.CommitObject(g.h)

      
        
        89
        +	if err != nil {

      
        
        90
        +		return &FileContent{}, fmt.Errorf("commit object: %w", err)

      
        
        91
        +	}

      
        
        92
        +

      
        
        93
        +	tree, err := c.Tree()

      
        
        94
        +	if err != nil {

      
        
        95
        +		return &FileContent{}, fmt.Errorf("file tree: %w", err)

      
        
        96
        +	}

      
        
        97
        +

      
        
        98
        +	file, err := tree.File(path)

      
        
        99
        +	if err != nil {

      
        
        100
        +		if errors.Is(err, object.ErrFileNotFound) {

      
        
        101
        +			return &FileContent{}, ErrFileNotFound

      
        
        102
        +		}

      
        
        103
        +		return &FileContent{}, err

      
        
        104
        +	}

      
        
        105
        +

      
        
        106
        +	reader, err := file.Reader()

      
        
        107
        +	if err != nil {

      
        
        108
        +		return nil, fmt.Errorf("file reader: %w", err)

      
        
        109
        +	}

      
        
        110
        +	defer reader.Close()

      
        
        111
        +

      
        
        112
        +	content, err := io.ReadAll(reader)

      
        
        113
        +	if err != nil {

      
        
        114
        +		return nil, fmt.Errorf("read file: %w", err)

      
        
        115
        +	}

      
        
        116
        +

      
        
        117
        +	isBin, _ := file.IsBinary()

      
        
        118
        +	mimeType := mime.TypeByExtension(filepath.Ext(path))

      
        
        119
        +	if mimeType == "" {

      
        
        120
        +		if isBin {

      
        
        121
        +			mimeType = "application/octet-stream"

      
        
        122
        +		} else {

      
        
        123
        +			mimeType = "text/plain"

      
        
        124
        +		}

      
        
        125
        +	}

      
        
        126
        +

      
        
        127
        +	return &FileContent{

      
        
        128
        +		IsBinary: isBin,

      
        
        129
        +		Content:  content,

      
        
        130
        +		Mime:     mimeType,

      
        
        131
        +		Size:     file.Size,

      
        
        132
        +	}, nil

      
        
        133
        +}

      
M internal/handlers/repo.go
···
        69
        69
         

      
        70
        70
         	var readmeContents template.HTML

      
        71
        71
         	for _, readme := range h.c.Repo.Readmes {

      
        
        72
        +		fc, ferr := repo.FileContent(readme)

      
        
        73
        +		if ferr != nil {

      
        
        74
        +			continue

      
        
        75
        +		}

      
        
        76
        +

      
        
        77
        +		if fc.IsBinary {

      
        
        78
        +			continue

      
        
        79
        +		}

      
        
        80
        +

      
        72
        81
         		ext := filepath.Ext(readme)

      
        73
        
        -		content, _ := repo.FileContent(readme)

      
        
        82
        +		content := fc.String()

      
        74
        83
         		if len(content) > 0 {

      
        75
        84
         			switch ext {

      
        76
        85
         			case ".md", ".markdown", ".mkd":

      ···
        178
        187
         		return

      
        179
        188
         	}

      
        180
        189
         

      
        181
        
        -	contents, err := repo.FileContent(treePath)

      
        
        190
        +	fc, err := repo.FileContent(treePath)

      
        182
        191
         	if err != nil {

      
        183
        192
         		if errors.Is(err, git.ErrFileNotFound) {

      
        184
        193
         			h.write404(w, err)

      ···
        190
        199
         

      
        191
        200
         	if raw {

      
        192
        201
         		w.WriteHeader(http.StatusOK)

      
        193
        
        -		w.Header().Set("Content-Type", "text/plain")

      
        194
        
        -		w.Write([]byte(contents))

      
        
        202
        +		w.Header().Set("Content-Type", fc.Mime)

      
        
        203
        +		w.Write(fc.Content)

      
        
        204
        +		return

      
        
        205
        +	}

      
        
        206
        +

      
        
        207
        +	if fc.IsImage() || fc.IsBinary {

      
        
        208
        +		data := make(map[string]any)

      
        
        209
        +		data["name"] = name

      
        
        210
        +		data["ref"] = ref

      
        
        211
        +		data["desc"] = desc

      
        
        212
        +		data["path"] = treePath

      
        
        213
        +		data["is_image"] = fc.IsImage()

      
        
        214
        +		data["is_binary"] = fc.IsBinary

      
        
        215
        +		data["mime_type"] = fc.Mime

      
        
        216
        +		data["size"] = fc.Size

      
        
        217
        +		data["meta"] = h.c.Meta

      
        
        218
        +		h.templ(w, "repo_file", data)

      
        195
        219
         		return

      
        196
        220
         	}

      
        197
        221
         

      ···
        201
        225
         	data["desc"] = desc

      
        202
        226
         	data["path"] = treePath

      
        203
        227
         

      
        204
        
        -	lc, err := countLines(strings.NewReader(contents))

      
        
        228
        +	contentStr := fc.String()

      
        
        229
        +	lc, err := countLines(strings.NewReader(contentStr))

      
        205
        230
         	if err != nil {

      
        206
        231
         		slog.Error("failed to count line numbers", "err", err)

      
        207
        232
         	}

      ···
        214
        239
         	}

      
        215
        240
         

      
        216
        241
         	data["linecount"] = lines

      
        217
        
        -	data["content"] = contents

      
        
        242
        +	data["content"] = contentStr

      
        218
        243
         	data["meta"] = h.c.Meta

      
        219
        244
         

      
        220
        245
         	h.templ(w, "repo_file", data)

      
M web/static/style.css
···
        265
        265
           overflow-y: hidden;

      
        266
        266
           overflow-x: auto;

      
        267
        267
         }

      
        
        268
        +.image-viewer, .binary-viewer { padding: 1rem 0; }

      
        
        269
        +.image-viewer img {

      
        
        270
        +  max-width: 100%;

      
        
        271
        +  height: auto;

      
        
        272
        +}

      
        268
        273
         

      
        269
        274
         .line-numbers pre,

      
        270
        275
         .file-content pre {

      
M web/templates/repo_file.html
···
        1
        1
         {{ define "repo_file" }}

      
        2
        2
         <html>

      
        3
        3
           <head>

      
        4
        
        -  {{ template "head" . }}

      
        
        4
        +    {{ template "head" . }}

      
        5
        5
           </head>

      
        6
        6
           {{ template "repo_header" . }}

      
        7
        7
           <body>

      
        8
        8
             <main>

      
        9
        
        -      <p>{{ .path }} (<a style="color: gray" href="?raw=true">view raw</a>)</p>

      
        
        9
        +      <p>{{ .path }} (<a class="muted" href="?raw=true">view raw</a>)</p>

      
        10
        10
               <div class="file-wrapper">

      
        11
        
        -      <table>

      
        12
        
        -        <tbody><tr>

      
        
        11
        +        {{ if .is_image }}

      
        
        12
        +        <div class="image-viewer">

      
        
        13
        +          <p>{{.mime_type}} • {{.size}} bytes</p>

      
        
        14
        +          <img src="?raw=true" alt="{{- .path -}}">

      
        
        15
        +        </div>

      
        
        16
        +        {{ else if .is_binary }}

      
        
        17
        +        <div class="binary-viewer">

      
        
        18
        +          <p>Binary file ({{.mime_type}})</p>

      
        
        19
        +          <p>Size: {{.size}} bytes</p>

      
        
        20
        +          <a class="link" href="?raw=true" download>[ Download ]</a>

      
        
        21
        +        </div>

      
        
        22
        +        {{ else }}

      
        
        23
        +        <table>

      
        
        24
        +          <tbody><tr>

      
        13
        25
                     <td class="line-numbers">

      
        14
        26
                       <pre>{{- range .linecount }}

      
        15
        27
         <a id="L{{ . }}" href="#L{{ . }}">{{ . }}</a>

      ···
        18
        30
                     <td class="file-content">

      
        19
        31
                       <pre>{{- .content -}}</pre>

      
        20
        32
                     </td>

      
        21
        
        -        </tbody></tr>

      
        22
        
        -      </table>

      
        
        33
        +          </tbody></tr>

      
        
        34
        +        </table>

      
        
        35
        +        {{ end }}

      
        23
        36
               </div>

      
        24
        37
             </main>

      
        25
        38
           </body>