Compare commits

..

4 Commits

Author SHA1 Message Date
jolheiser 86aa09929f
a few more unit tests 2025-06-04 16:13:46 -05:00
jolheiser b8ca3fc4b8
fix tags 2025-06-04 16:06:14 -05:00
jolheiser ebe2dc4603
add last commit to index 2025-06-04 15:09:24 -05:00
jolheiser ea40aa746e
permalink 2025-06-04 13:47:09 -05:00
13 changed files with 506 additions and 40 deletions

View File

@ -3,8 +3,10 @@ package git_test
import ( import (
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/alecthomas/assert/v2" "github.com/alecthomas/assert/v2"
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
"go.jolheiser.com/ugit/internal/git" "go.jolheiser.com/ugit/internal/git"
) )
@ -43,3 +45,232 @@ func TestRepo(t *testing.T) {
assert.NoError(t, err, "should not error when getting existing repo") assert.NoError(t, err, "should not error when getting existing repo")
assert.False(t, repo.Meta.Private, "repo should be public after saving meta") assert.False(t, repo.Meta.Private, "repo should be public after saving meta")
} }
func TestPathExists(t *testing.T) {
tmp := t.TempDir()
exists, err := git.PathExists(tmp)
assert.NoError(t, err)
assert.True(t, exists)
doesNotExist := filepath.Join(tmp, "does-not-exist")
exists, err = git.PathExists(doesNotExist)
assert.NoError(t, err)
assert.False(t, exists)
}
func TestRepoMetaUpdate(t *testing.T) {
original := git.RepoMeta{
Description: "Original description",
Private: true,
Tags: git.TagSet{"tag1": struct{}{}, "tag2": struct{}{}},
}
update := git.RepoMeta{
Description: "Updated description",
Private: false,
Tags: git.TagSet{"tag3": struct{}{}},
}
err := original.Update(update)
assert.NoError(t, err)
assert.Equal(t, "Updated description", original.Description)
assert.False(t, original.Private)
assert.Equal(t, []string{"tag1", "tag2", "tag3"}, original.Tags.Slice())
}
func TestFileInfoName(t *testing.T) {
testCases := []struct {
path string
expected string
}{
{path: "file.txt", expected: "file.txt"},
{path: "dir/file.txt", expected: "file.txt"},
{path: "nested/path/to/file.go", expected: "file.go"},
{path: "README.md", expected: "README.md"},
}
for _, tc := range testCases {
t.Run(tc.path, func(t *testing.T) {
fi := git.FileInfo{Path: tc.path}
assert.Equal(t, tc.expected, fi.Name())
})
}
}
func TestCommitSummaryAndDetails(t *testing.T) {
testCases := []struct {
message string
expectedSummary string
expectedDetails string
}{
{
message: "Simple commit message",
expectedSummary: "Simple commit message",
expectedDetails: "",
},
{
message: "Add feature X\n\nThis commit adds feature X\nWith multiple details\nAcross multiple lines",
expectedSummary: "Add feature X",
expectedDetails: "\nThis commit adds feature X\nWith multiple details\nAcross multiple lines",
},
{
message: "Fix bug\n\nDetailed explanation",
expectedSummary: "Fix bug",
expectedDetails: "\nDetailed explanation",
},
}
for _, tc := range testCases {
t.Run(tc.message, func(t *testing.T) {
commit := git.Commit{
SHA: "abcdef1234567890",
Message: tc.message,
Signature: "",
Author: "Test User",
Email: "test@example.com",
When: time.Now(),
}
assert.Equal(t, tc.expectedSummary, commit.Summary())
assert.Equal(t, tc.expectedDetails, commit.Details())
})
}
}
func TestCommitShort(t *testing.T) {
commit := git.Commit{
SHA: "abcdef1234567890abcdef1234567890",
}
assert.Equal(t, "abcdef12", commit.Short())
}
func TestCommitFilePath(t *testing.T) {
testCases := []struct {
name string
fromPath string
toPath string
expected string
}{
{
name: "to path preferred",
fromPath: "old/path.txt",
toPath: "new/path.txt",
expected: "new/path.txt",
},
{
name: "fallback to from path",
fromPath: "deleted/file.txt",
toPath: "",
expected: "deleted/file.txt",
},
{
name: "both paths empty",
fromPath: "",
toPath: "",
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cf := git.CommitFile{
From: git.CommitFileEntry{Path: tc.fromPath},
To: git.CommitFileEntry{Path: tc.toPath},
}
assert.Equal(t, tc.expected, cf.Path())
})
}
}
func TestRepoName(t *testing.T) {
tmp := t.TempDir()
repoName := "testrepo"
err := git.EnsureRepo(tmp, repoName+".git")
assert.NoError(t, err)
repo, err := git.NewRepo(tmp, repoName)
assert.NoError(t, err)
assert.Equal(t, repoName, repo.Name())
repoName2 := "test-repo-with-hyphens"
err = git.EnsureRepo(tmp, repoName2+".git")
assert.NoError(t, err)
repo2, err := git.NewRepo(tmp, repoName2)
assert.NoError(t, err)
assert.Equal(t, repoName2, repo2.Name())
}
func TestHandlePushOptions(t *testing.T) {
tmp := t.TempDir()
err := git.EnsureRepo(tmp, "test.git")
assert.NoError(t, err)
repo, err := git.NewRepo(tmp, "test")
assert.NoError(t, err)
opts := []*packp.Option{
{Key: "description", Value: "New description"},
}
err = git.HandlePushOptions(repo, opts)
assert.NoError(t, err)
assert.Equal(t, "New description", repo.Meta.Description)
opts = []*packp.Option{
{Key: "private", Value: "false"},
}
err = git.HandlePushOptions(repo, opts)
assert.NoError(t, err)
assert.False(t, repo.Meta.Private)
repo.Meta.Private = true
opts = []*packp.Option{
{Key: "private", Value: "invalid"},
}
err = git.HandlePushOptions(repo, opts)
assert.NoError(t, err)
assert.True(t, repo.Meta.Private)
opts = []*packp.Option{
{Key: "tags", Value: "tag1,tag2"},
}
err = git.HandlePushOptions(repo, opts)
assert.NoError(t, err)
opts = []*packp.Option{
{Key: "description", Value: "Combined update"},
{Key: "private", Value: "true"},
}
err = git.HandlePushOptions(repo, opts)
assert.NoError(t, err)
assert.Equal(t, "Combined update", repo.Meta.Description)
assert.True(t, repo.Meta.Private)
}
func TestRepoPath(t *testing.T) {
tmp := t.TempDir()
err := git.EnsureRepo(tmp, "test.git")
assert.NoError(t, err)
repo, err := git.NewRepo(tmp, "test")
assert.NoError(t, err)
expected := filepath.Join(tmp, "test.git")
assert.Equal(t, expected, repo.Path())
}
func TestEnsureJSONFile(t *testing.T) {
tmp := t.TempDir()
err := git.EnsureRepo(tmp, "test.git")
assert.NoError(t, err)
repo, err := git.NewRepo(tmp, "test")
assert.NoError(t, err)
assert.True(t, repo.Meta.Private, "default repo should be private")
assert.Equal(t, "", repo.Meta.Description, "default description should be empty")
assert.Equal(t, 0, len(repo.Meta.Tags), "default tags should be empty")
}

View File

@ -7,13 +7,60 @@ import (
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"slices"
) )
// RepoMeta is the meta information a Repo can have // RepoMeta is the meta information a Repo can have
type RepoMeta struct { type RepoMeta struct {
Description string `json:"description"` Description string `json:"description"`
Private bool `json:"private"` Private bool `json:"private"`
Tags []string `json:"tags"` Tags TagSet `json:"tags"`
}
// TagSet is a Set of tags
type TagSet map[string]struct{}
// Add adds a tag to the set
func (t TagSet) Add(tag string) {
t[tag] = struct{}{}
}
// Remove removes a tag from the set
func (t TagSet) Remove(tag string) {
delete(t, tag)
}
// Contains checks if a tag is in the set
func (t TagSet) Contains(tag string) bool {
_, ok := t[tag]
return ok
}
// Slice returns the set as a (sorted) slice
func (t TagSet) Slice() []string {
s := make([]string, 0, len(t))
for k := range t {
s = append(s, k)
}
slices.Sort(s)
return s
}
// MarshalJSON implements [json.Marshaler]
func (t TagSet) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Slice())
}
// UnmarshalJSON implements [json.Unmarshaler]
func (t *TagSet) UnmarshalJSON(b []byte) error {
var s []string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
for _, ss := range s {
t.Add(ss)
}
return nil
} }
// Update updates meta given another RepoMeta // Update updates meta given another RepoMeta

View File

@ -0,0 +1,53 @@
package git
import (
"encoding/json"
"testing"
"github.com/alecthomas/assert/v2"
)
func TestTagSet(t *testing.T) {
set := make(TagSet)
assert.Equal(t, 0, len(set))
assert.Equal(t, 0, len(set.Slice()))
set.Add("foo")
assert.Equal(t, 1, len(set))
assert.Equal(t, 1, len(set.Slice()))
assert.True(t, set.Contains("foo"))
set.Add("bar")
assert.Equal(t, 2, len(set))
assert.Equal(t, 2, len(set.Slice()))
assert.True(t, set.Contains("foo"))
assert.True(t, set.Contains("bar"))
set.Add("bar")
assert.Equal(t, 2, len(set))
assert.Equal(t, 2, len(set.Slice()))
assert.True(t, set.Contains("foo"))
assert.True(t, set.Contains("bar"))
set.Remove("foo")
assert.Equal(t, 1, len(set))
assert.Equal(t, 1, len(set.Slice()))
assert.False(t, set.Contains("foo"))
assert.True(t, set.Contains("bar"))
set.Add("foo")
set.Add("baz")
j, err := json.Marshal(set)
assert.NoError(t, err)
assert.Equal(t, `["bar","baz","foo"]`, string(j))
set = make(TagSet)
b := []byte(`["foo","bar","baz"]`)
err = json.Unmarshal(b, &set)
assert.NoError(t, err)
assert.Equal(t, 3, len(set))
assert.Equal(t, 3, len(set.Slice()))
assert.True(t, set.Contains("foo"))
assert.True(t, set.Contains("bar"))
assert.True(t, set.Contains("baz"))
}

View File

@ -58,15 +58,11 @@ func HandlePushOptions(repo *Repo, opts []*packp.Option) error {
remove = true remove = true
tagValue = strings.TrimPrefix(tagValue, "-") tagValue = strings.TrimPrefix(tagValue, "-")
} }
for idx, tag := range repo.Meta.Tags { tagValue = strings.ToLower(tagValue)
if strings.EqualFold(tag, tagValue) { if remove {
if remove { repo.Meta.Tags.Remove(tagValue)
repo.Meta.Tags = append(repo.Meta.Tags[:idx], repo.Meta.Tags[idx+1:]...) } else {
} else { repo.Meta.Tags.Add(tagValue)
repo.Meta.Tags = append(repo.Meta.Tags, strings.ToLower(tagValue))
}
break
}
} }
} }
} }

View File

@ -57,6 +57,9 @@ func NewRepo(dir, name string) (*Repo, error) {
if err := json.NewDecoder(fi).Decode(&r.Meta); err != nil { if err := json.NewDecoder(fi).Decode(&r.Meta); err != nil {
return nil, err return nil, err
} }
if r.Meta.Tags == nil {
r.Meta.Tags = make(TagSet)
}
return r, nil return r, nil
} }

View File

@ -1,6 +1,8 @@
package html package html
import ( import (
"fmt"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"go.jolheiser.com/ugit/assets" "go.jolheiser.com/ugit/assets"
"go.jolheiser.com/ugit/internal/git" "go.jolheiser.com/ugit/internal/git"
@ -25,7 +27,7 @@ type IndexLink struct {
URL string URL string
} }
func lastCommit(repo *git.Repo, human bool) string { func lastCommitTime(repo *git.Repo, human bool) string {
c, err := repo.LastCommit() c, err := repo.LastCommit()
if err != nil { if err != nil {
return "" return ""
@ -36,6 +38,14 @@ func lastCommit(repo *git.Repo, human bool) string {
return c.When.Format("01/02/2006 03:04:05 PM") return c.When.Format("01/02/2006 03:04:05 PM")
} }
func lastCommit(repo *git.Repo) *git.Commit {
c, err := repo.LastCommit()
if err != nil {
return nil
}
return &c
}
func IndexTemplate(ic IndexContext) Node { func IndexTemplate(ic IndexContext) Node {
return base(ic.BaseContext, []Node{ return base(ic.BaseContext, []Node{
Header( Header(
@ -65,19 +75,28 @@ func IndexTemplate(ic IndexContext) Node {
) )
}), }),
), ),
Div(Class("grid sm:grid-cols-8 gap-2 mt-5"), Div(Class("grid sm:grid-cols-10 gap-2 mt-5"),
Map(ic.Repos, func(repo *git.Repo) Node { Map(ic.Repos, func(repo *git.Repo) Node {
commit := lastCommit(repo)
return Group([]Node{ return Group([]Node{
Div(Class("sm:col-span-2 text-blue dark:text-lavender"), Div(Class("sm:col-span-2 text-blue dark:text-lavender"),
A(Class("underline decoration-blue/50 dark:decoration-lavender/50 decoration-dashed hover:decoration-solid"), Href("/"+repo.Name()), Text(repo.Name())), A(Class("underline decoration-blue/50 dark:decoration-lavender/50 decoration-dashed hover:decoration-solid"), Href("/"+repo.Name()), Text(repo.Name())),
), ),
Div(Class("sm:col-span-4 text-subtext0"), Text(repo.Meta.Description)), Div(Class("sm:col-span-3 text-subtext0"), Text(repo.Meta.Description)),
Div(Class("sm:col-span-3 text-subtext0"),
If(commit != nil,
Div(Title(commit.Message),
A(Class("underline text-blue dark:text-lavender decoration-blue/50 dark:decoration-lavender/50 decoration-dashed hover:decoration-solid"), Href(fmt.Sprintf("/%s/commit/%s", repo.Name(), commit.SHA)), Text(commit.Short())),
Text(": "+commit.Summary()),
),
),
),
Div(Class("sm:col-span-1 text-subtext0"), Div(Class("sm:col-span-1 text-subtext0"),
Map(repo.Meta.Tags, func(tag string) Node { Map(repo.Meta.Tags.Slice(), func(tag string) Node {
return A(Class("rounded border-rosewater border-solid border pb-0.5 px-1 mr-1 mb-1 inline-block"), Href("?tag="+tag), Text(tag)) return A(Class("rounded border-rosewater border-solid border pb-0.5 px-1 mr-1 mb-1 inline-block"), Href("?tag="+tag), Text(tag))
}), }),
), ),
Div(Class("sm:col-span-1 text-text/80 mb-4 sm:mb-0"), Title(lastCommit(repo, false)), Text(lastCommit(repo, true))), Div(Class("sm:col-span-1 text-text/80 mb-4 sm:mb-0"), Title(lastCommitTime(repo, false)), Text(lastCommitTime(repo, true))),
}) })
}), }),
), ),

View File

@ -21,6 +21,7 @@ type RepoFileContext struct {
var repoFileJS string var repoFileJS string
func RepoFileTemplate(rfc RepoFileContext) Node { func RepoFileTemplate(rfc RepoFileContext) Node {
permalink := fmt.Sprintf("/%s/tree/%s/%s", rfc.RepoBreadcrumbComponentContext.Repo, rfc.Commit, rfc.Path)
return base(rfc.BaseContext, []Node{ return base(rfc.BaseContext, []Node{
repoHeaderComponent(rfc.RepoHeaderComponentContext), repoHeaderComponent(rfc.RepoHeaderComponentContext),
Div(Class("mt-2 text-text"), Div(Class("mt-2 text-text"),
@ -28,7 +29,7 @@ func RepoFileTemplate(rfc RepoFileContext) Node {
Text(" - "), Text(" - "),
A(Class("text-text underline decoration-text/50 decoration-dashed hover:decoration-solid"), Href("?raw"), Text("raw")), A(Class("text-text underline decoration-text/50 decoration-dashed hover:decoration-solid"), Href("?raw"), Text("raw")),
Text(" - "), Text(" - "),
A(Class("text-text underline decoration-text/50 decoration-dashed hover:decoration-solid"), ID("permalink"), Href(fmt.Sprintf("/%s/tree/%s/%s", rfc.RepoBreadcrumbComponentContext.Repo, rfc.Commit, rfc.Path)), Text("permalink")), A(Class("text-text underline decoration-text/50 decoration-dashed hover:decoration-solid"), ID("permalink"), Data("permalink", permalink), Href(permalink), Text("permalink")),
Div(Class("code relative"), Div(Class("code relative"),
Raw(rfc.Code), Raw(rfc.Code),
Button(ID("copy"), Class("absolute top-0 right-0 rounded bg-base hover:bg-surface0")), Button(ID("copy"), Class("absolute top-0 right-0 rounded bg-base hover:bg-surface0")),

View File

@ -2,7 +2,7 @@ const lineRe = /#L(\d+)(?:-L(\d+))?/g
const $lineLines = document.querySelectorAll(".chroma .lntable .lnt"); const $lineLines = document.querySelectorAll(".chroma .lntable .lnt");
const $codeLines = document.querySelectorAll(".chroma .lntable .line"); const $codeLines = document.querySelectorAll(".chroma .lntable .line");
const $copyButton = document.getElementById('copy'); const $copyButton = document.getElementById('copy');
const $permalinkButton = document.getElementById('permalink'); const $permalink = document.getElementById('permalink');
const $copyIcon = "📋"; const $copyIcon = "📋";
const $copiedIcon = "✅"; const $copiedIcon = "✅";
let $code = "" let $code = ""
@ -14,9 +14,12 @@ if (0 in results) {
start = results[0][1] !== undefined ? parseInt(results[0][1]) : 0; start = results[0][1] !== undefined ? parseInt(results[0][1]) : 0;
end = results[0][2] !== undefined ? parseInt(results[0][2]) : 0; end = results[0][2] !== undefined ? parseInt(results[0][2]) : 0;
} }
if (start != 0) { if (start !== 0) {
deactivateLines(); deactivateLines();
activateLines(start, end); activateLines(start, end);
let anchor = `#${start}`;
if (end !== 0) anchor += `-${end}`;
if (anchor !== "") $permalink.href = $permalink.dataset.permalink + anchor;
$lineLines[start - 1].scrollIntoView(true); $lineLines[start - 1].scrollIntoView(true);
} }
for (let line of $lineLines) { for (let line of $lineLines) {
@ -28,13 +31,17 @@ for (let line of $lineLines) {
if (event.shiftKey) { if (event.shiftKey) {
end = n; end = n;
anchor = `#L${start}-L${end}`; anchor = `#L${start}-L${end}`;
} else if (start === n) {
start = 0;
end = 0;
} else { } else {
start = n; start = n;
end = 0; end = 0;
anchor = `#L${start}`; anchor = `#L${start}`;
} }
history.replaceState(null, null, anchor); history.replaceState(null, null, window.location.pathname + anchor);
activateLines(start, end); $permalink.href = $permalink.dataset.permalink + anchor;
if (start !== 0) activateLines(start, end);
}); });
} }
if (navigator.clipboard && navigator.clipboard.writeText) { if (navigator.clipboard && navigator.clipboard.writeText) {
@ -49,18 +56,6 @@ $copyButton.addEventListener("click", () => {
}, 1000); }, 1000);
}); });
$permalinkButton.addEventListener("click", (event) => {
event.preventDefault();
const url = $permalinkButton.getAttribute("href");
navigator.clipboard.writeText(window.location.origin + url + location.hash);
const originalText = $permalinkButton.innerText;
$permalinkButton.innerText = "copied!";
setTimeout(() => {
$permalinkButton.innerText = originalText;
}, 1000);
});
function activateLines(start, end) { function activateLines(start, end) {
if (end < start) end = start; if (end < start) end = start;
for (let idx = start - 1; idx < end; idx++) { for (let idx = start - 1; idx < end; idx++) {

File diff suppressed because one or more lines are too long

View File

@ -126,7 +126,7 @@ func (rh repoHandler) repoHeaderContext(repo *git.Repo, r *http.Request) html.Re
Name: chi.URLParam(r, "repo"), Name: chi.URLParam(r, "repo"),
Ref: ref, Ref: ref,
CloneURL: rh.s.CloneURL, CloneURL: rh.s.CloneURL,
Tags: repo.Meta.Tags, Tags: repo.Meta.Tags.Slice(),
} }
} }

View File

@ -0,0 +1,122 @@
package httperr_test
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/alecthomas/assert/v2"
"go.jolheiser.com/ugit/internal/http/httperr"
)
func successHandler(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(http.StatusOK)
return nil
}
func errorHandler(w http.ResponseWriter, r *http.Request) error {
return errors.New("test error")
}
func statusErrorHandler(status int) func(w http.ResponseWriter, r *http.Request) error {
return func(w http.ResponseWriter, r *http.Request) error {
return httperr.Status(errors.New("test error"), status)
}
}
func TestHandler_Success(t *testing.T) {
handler := httperr.Handler(successHandler)
req := httptest.NewRequest("GET", "/", nil)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
}
func TestHandler_Error(t *testing.T) {
handler := httperr.Handler(errorHandler)
req := httptest.NewRequest("GET", "/", nil)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusInternalServerError, recorder.Code)
}
func TestHandler_StatusError(t *testing.T) {
testCases := []struct {
name string
status int
expectedStatus int
}{
{
name: "not found",
status: http.StatusNotFound,
expectedStatus: http.StatusNotFound,
},
{
name: "bad request",
status: http.StatusBadRequest,
expectedStatus: http.StatusBadRequest,
},
{
name: "unauthorized",
status: http.StatusUnauthorized,
expectedStatus: http.StatusUnauthorized,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
handler := httperr.Handler(statusErrorHandler(tc.status))
req := httptest.NewRequest("GET", "/", nil)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
assert.Equal(t, tc.expectedStatus, recorder.Code)
})
}
}
type unwrapper interface {
Unwrap() error
}
func TestError(t *testing.T) {
originalErr := errors.New("original error")
httpErr := httperr.Error(originalErr)
assert.Equal(t, originalErr.Error(), httpErr.Error())
unwrapper, ok := any(httpErr).(unwrapper)
assert.True(t, ok)
assert.Equal(t, originalErr, unwrapper.Unwrap())
}
func TestStatus(t *testing.T) {
originalErr := errors.New("original error")
httpErr := httperr.Status(originalErr, http.StatusNotFound)
assert.Equal(t, originalErr.Error(), httpErr.Error())
unwrapper, ok := any(httpErr).(unwrapper)
assert.True(t, ok)
assert.Equal(t, originalErr, unwrapper.Unwrap())
handler := httperr.Handler(func(w http.ResponseWriter, r *http.Request) error {
return httpErr
})
req := httptest.NewRequest("GET", "/", nil)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusNotFound, recorder.Code)
}

View File

@ -3,7 +3,6 @@ package http
import ( import (
"net/http" "net/http"
"os" "os"
"slices"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -34,10 +33,10 @@ func (rh repoHandler) index(w http.ResponseWriter, r *http.Request) error {
if !rh.s.ShowPrivate { if !rh.s.ShowPrivate {
continue continue
} }
repo.Meta.Tags = append(repo.Meta.Tags, "private") repo.Meta.Tags.Add("private")
} }
if tagFilter != "" && !slices.Contains(repo.Meta.Tags, strings.ToLower(tagFilter)) { if tagFilter != "" && !repo.Meta.Tags.Contains(strings.ToLower(tagFilter)) {
continue continue
} }
repos = append(repos, repo) repos = append(repos, repo)

View File

@ -30,7 +30,7 @@ func (rh repoHandler) repoMiddleware(next http.Handler) http.Handler {
if !rh.s.ShowPrivate { if !rh.s.ShowPrivate {
return httperr.Status(errors.New("could not get git repo"), http.StatusNotFound) return httperr.Status(errors.New("could not get git repo"), http.StatusNotFound)
} }
repo.Meta.Tags = append(repo.Meta.Tags, "private") repo.Meta.Tags.Add("private")
} }
r = r.WithContext(context.WithValue(r.Context(), repoCtxKey, repo)) r = r.WithContext(context.WithValue(r.Context(), repoCtxKey, repo))
next.ServeHTTP(w, r) next.ServeHTTP(w, r)