From b8ca3fc4b80b621b88470a4ae62937238773a3ef Mon Sep 17 00:00:00 2001 From: jolheiser Date: Wed, 4 Jun 2025 15:42:48 -0500 Subject: [PATCH] fix tags --- internal/git/meta.go | 53 ++++++++++++++++++++++++++++++++++--- internal/git/meta_test.go | 53 +++++++++++++++++++++++++++++++++++++ internal/git/protocol.go | 14 ++++------ internal/html/index.go | 2 +- internal/http/http.go | 2 +- internal/http/index.go | 5 ++-- internal/http/middleware.go | 2 +- 7 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 internal/git/meta_test.go diff --git a/internal/git/meta.go b/internal/git/meta.go index 52bb262..0a13833 100644 --- a/internal/git/meta.go +++ b/internal/git/meta.go @@ -7,13 +7,60 @@ import ( "io/fs" "os" "path/filepath" + "slices" ) // RepoMeta is the meta information a Repo can have type RepoMeta struct { - Description string `json:"description"` - Private bool `json:"private"` - Tags []string `json:"tags"` + Description string `json:"description"` + Private bool `json:"private"` + 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 diff --git a/internal/git/meta_test.go b/internal/git/meta_test.go new file mode 100644 index 0000000..e51c029 --- /dev/null +++ b/internal/git/meta_test.go @@ -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")) +} diff --git a/internal/git/protocol.go b/internal/git/protocol.go index 02921bf..287347d 100644 --- a/internal/git/protocol.go +++ b/internal/git/protocol.go @@ -58,15 +58,11 @@ func HandlePushOptions(repo *Repo, opts []*packp.Option) error { remove = true tagValue = strings.TrimPrefix(tagValue, "-") } - for idx, tag := range repo.Meta.Tags { - if strings.EqualFold(tag, tagValue) { - if remove { - repo.Meta.Tags = append(repo.Meta.Tags[:idx], repo.Meta.Tags[idx+1:]...) - } else { - repo.Meta.Tags = append(repo.Meta.Tags, strings.ToLower(tagValue)) - } - break - } + tagValue = strings.ToLower(tagValue) + if remove { + repo.Meta.Tags.Remove(tagValue) + } else { + repo.Meta.Tags.Add(tagValue) } } } diff --git a/internal/html/index.go b/internal/html/index.go index 621eacc..478cae2 100644 --- a/internal/html/index.go +++ b/internal/html/index.go @@ -92,7 +92,7 @@ func IndexTemplate(ic IndexContext) Node { ), ), 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)) }), ), diff --git a/internal/http/http.go b/internal/http/http.go index 61831c7..b89b824 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -126,7 +126,7 @@ func (rh repoHandler) repoHeaderContext(repo *git.Repo, r *http.Request) html.Re Name: chi.URLParam(r, "repo"), Ref: ref, CloneURL: rh.s.CloneURL, - Tags: repo.Meta.Tags, + Tags: repo.Meta.Tags.Slice(), } } diff --git a/internal/http/index.go b/internal/http/index.go index aeac9f8..ed752ab 100644 --- a/internal/http/index.go +++ b/internal/http/index.go @@ -3,7 +3,6 @@ package http import ( "net/http" "os" - "slices" "sort" "strings" "time" @@ -34,10 +33,10 @@ func (rh repoHandler) index(w http.ResponseWriter, r *http.Request) error { if !rh.s.ShowPrivate { 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 } repos = append(repos, repo) diff --git a/internal/http/middleware.go b/internal/http/middleware.go index b5c4ae0..2b40746 100644 --- a/internal/http/middleware.go +++ b/internal/http/middleware.go @@ -30,7 +30,7 @@ func (rh repoHandler) repoMiddleware(next http.Handler) http.Handler { if !rh.s.ShowPrivate { 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)) next.ServeHTTP(w, r)