Skip to content

Commit

Permalink
Search gists on user profile
Browse files Browse the repository at this point in the history
  • Loading branch information
thomiceli committed Feb 2, 2025
1 parent 76fc129 commit 7dc2551
Show file tree
Hide file tree
Showing 23 changed files with 428 additions and 53 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-tpm v0.9.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/schema v1.4.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
Expand Down
17 changes: 17 additions & 0 deletions internal/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
SyncGistPreviews
ResetHooks
IndexGists
SyncGistLanguages
)

var (
Expand Down Expand Up @@ -73,6 +74,8 @@ func Run(actionType int) {
functionToRun = resetHooks
case IndexGists:
functionToRun = indexGists
case SyncGistLanguages:
functionToRun = syncGistLanguages
default:
log.Error().Msg("Unknown action type")
}
Expand Down Expand Up @@ -166,3 +169,17 @@ func indexGists() {
}
}
}

func syncGistLanguages() {
log.Info().Msg("Syncing all Gist languages...")
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}

for _, gist := range gists {
log.Info().Msgf("Syncing languages for gist %d", gist.ID)
gist.UpdateLanguages()
}
}
2 changes: 1 addition & 1 deletion internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func Setup(dbUri string) error {
return err
}

if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}); err != nil {
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}); err != nil {
return err
}

Expand Down
113 changes: 102 additions & 11 deletions internal/db/gist.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os/exec"
"path/filepath"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -50,16 +51,16 @@ func (v Visibility) Next() Visibility {
}
}

func ParseVisibility[T string | int](v T) (Visibility, error) {
func ParseVisibility[T string | int](v T) Visibility {
switch s := fmt.Sprint(v); s {
case "0", "public":
return PublicVisibility, nil
return PublicVisibility
case "1", "unlisted":
return UnlistedVisibility, nil
return UnlistedVisibility
case "2", "private":
return PrivateVisibility, nil
return PrivateVisibility
default:
return -1, fmt.Errorf("unknown visibility %q", s)
return PublicVisibility
}
}

Expand All @@ -84,7 +85,8 @@ type Gist struct {
Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
ForkedID uint

Topics []GistTopic `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Topics []GistTopic `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Languages []GistLanguage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}

type Like struct {
Expand Down Expand Up @@ -166,25 +168,59 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st
}

func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
}

func gistsFromUserStatementWithPreloads(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
}

func GetAllGistsFromUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
func GetAllGistsFromUser(fromUserId uint, currentUserId uint, title string, language string, visibility string, topics []string, offset int, sort string, order string) ([]*Gist, int64, error) {
var gists []*Gist
err := gistsFromUserStatement(fromUserId, currentUserId).Limit(11).
var count int64

baseQuery := gistsFromUserStatementWithPreloads(fromUserId, currentUserId).Model(&Gist{})

if title != "" {
baseQuery = baseQuery.Where("gists.title like ?", "%"+title+"%")
}

if language != "" {
baseQuery = baseQuery.Joins("join gist_languages on gists.id = gist_languages.gist_id").
Where("gist_languages.language = ?", language)
}

if visibility != "" {
baseQuery = baseQuery.Where("gists.private = ?", ParseVisibility(visibility))
}

if len(topics) > 0 {
baseQuery = baseQuery.Joins("join gist_topics on gists.id = gist_topics.gist_id").
Where("gist_topics.topic in ?", topics)
}

err := baseQuery.Count(&count).Error
if err != nil {
return nil, 0, err
}

err = baseQuery.Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error

return gists, err
return gists, count, err
}

func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
var count int64
err := gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
err := gistsFromUserStatementWithPreloads(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
return count, err
}

Expand Down Expand Up @@ -258,7 +294,18 @@ func GetAllGistsByIds(ids []uint) ([]*Gist, error) {
Where("id in ?", ids).
Find(&gists).Error

return gists, err
// keep order
ordered := make([]*Gist, 0, len(ids))
for _, wantedId := range ids {
for _, gist := range gists {
if gist.ID == wantedId {
ordered = append(ordered, gist)
break
}
}
}

return ordered, err
}

func (gist *Gist) Create() error {
Expand Down Expand Up @@ -593,6 +640,47 @@ func DeserialiseInitRepository(user string) (*Gist, error) {
return &gist, nil
}

func (gist *Gist) UpdateLanguages() {
languages, err := gist.GetLanguagesFromFiles()
if err != nil {
log.Error().Err(err).Msgf("Cannot get languages for gist %d", gist.ID)
return
}

slices.Sort(languages)
languages = slices.Compact(languages)

tx := db.Begin()
if tx.Error != nil {
log.Error().Err(tx.Error).Msgf("Cannot start transaction for gist %d", gist.ID)
return
}

if err := tx.Where("gist_id = ?", gist.ID).Delete(&GistLanguage{}).Error; err != nil {
tx.Rollback()
log.Error().Err(err).Msgf("Cannot delete languages for gist %d", gist.ID)
return
}

for _, language := range languages {
gistLanguage := &GistLanguage{
GistID: gist.ID,
Language: language,
}
if err := tx.Create(gistLanguage).Error; err != nil {
tx.Rollback()
log.Error().Err(err).Msgf("Cannot create gist language %s for gist %d", language, gist.ID)
return
}
}

if err := tx.Commit().Error; err != nil {
tx.Rollback()
log.Error().Err(err).Msgf("Cannot commit transaction for gist %d", gist.ID)
return
}
}

func (gist *Gist) ToDTO() (*GistDTO, error) {
files, err := gist.Files("HEAD", false)
if err != nil {
Expand Down Expand Up @@ -684,6 +772,9 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
wholeContent := ""
for _, file := range files {
wholeContent += file.Content
if !strings.HasSuffix(wholeContent, "\n") {
wholeContent += "\n"
}
exts = append(exts, filepath.Ext(file.Filename))
}

Expand Down
27 changes: 27 additions & 0 deletions internal/db/gist_language.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package db

type GistLanguage struct {
GistID uint `gorm:"primaryKey"`
Language string `gorm:"primaryKey;size:100"`
}

func GetGistLanguagesForUser(fromUserId, currentUserId uint) ([]struct {
Language string
Count int64
}, error) {
var results []struct {
Language string
Count int64
}

err := gistsFromUserStatement(fromUserId, currentUserId).Model(&GistLanguage{}).
Select("language, count(*) as count").
Joins("JOIN gists ON gists.id = gist_languages.gist_id").
Where("gists.user_id = ?", fromUserId).
Group("language").
Order("count DESC").
Limit(15). // Added limit of 15
Find(&results).Error

return results, err
}
File renamed without changes.
2 changes: 1 addition & 1 deletion internal/hooks/post_receive.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
}

if slices.Contains([]string{"public", "unlisted", "private"}, opts["visibility"]) {
gist.Private, _ = db.ParseVisibility(opts["visibility"])
gist.Private = db.ParseVisibility(opts["visibility"])
outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"]))
}

Expand Down
12 changes: 11 additions & 1 deletion internal/i18n/locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,15 @@ gist.search.help.filename: gists having files with given name
gist.search.help.extension: gists having files with given extension
gist.search.help.language: gists having files with given language
gist.search.help.topic: gists with given topic

gist.search.placeholder.title: Title
gist.search.placeholder.visibility: Visibility
gist.search.placeholder.public: Public
gist.search.placeholder.unlisted: Unlisted
gist.search.placeholder.private: Private
gist.search.placeholder.language: Language
gist.search.placeholder.all: All
gist.search.placeholder.topics: Topics
gist.search.placeholder.search: Search

gist.forks: Forks
gist.forks.view: View fork
Expand Down Expand Up @@ -234,6 +242,7 @@ admin.actions.git-gc: Garbage collect all git repositories
admin.actions.sync-previews: Synchronize all gists previews
admin.actions.reset-hooks: Reset Git server hooks for all repositories
admin.actions.index-gists: Index all gists
admin.actions.sync-gist-languages: Synchronize all gists languages
admin.id: ID
admin.user: User
admin.delete: Delete
Expand Down Expand Up @@ -279,6 +288,7 @@ flash.admin.git-gc: Garbage collecting repositories...
flash.admin.sync-previews: Syncing Gist previews...
flash.admin.reset-hooks: Resetting Git server hooks for all repositories...
flash.admin.index-gists: Indexing all gists...
flash.admin.sync-gist-languages: Syncing Gist languages...

flash.auth.username-exists: Username already exists
flash.auth.invalid-credentials: Invalid credentials
Expand Down
2 changes: 1 addition & 1 deletion internal/index/bleve.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func SearchGists(queryStr string, queryMetadata SearchGistMetadata, gistsIds []u
perPage := 10
offset := (page - 1) * perPage

s := bleve.NewSearchRequestOptions(indexerQuery, perPage, offset, false)
s := bleve.NewSearchRequestOptions(indexerQuery, perPage+1, offset, false)
s.AddFacet("languageFacet", languageFacet)
s.Fields = []string{"GistID"}
s.IncludeLocations = false
Expand Down
6 changes: 6 additions & 0 deletions internal/web/handlers/admin/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ func AdminIndexGists(ctx *context.Context) error {
go actions.Run(actions.IndexGists)
return ctx.RedirectTo("/admin-panel")
}

func AdminSyncGistLanguages(ctx *context.Context) error {
ctx.AddFlash(ctx.Tr("flash.admin.sync-gist-languages"), "success")
go actions.Run(actions.SyncGistLanguages)
return ctx.RedirectTo("/admin-panel")
}
5 changes: 3 additions & 2 deletions internal/web/handlers/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func AdminIndex(ctx *context.Context) error {
ctx.SetData("syncGistPreviews", actions.IsRunning(actions.SyncGistPreviews))
ctx.SetData("resetHooks", actions.IsRunning(actions.ResetHooks))
ctx.SetData("indexGists", actions.IsRunning(actions.IndexGists))
ctx.SetData("syncGistLanguages", actions.IsRunning(actions.SyncGistLanguages))
return ctx.Html("admin_index.html")
}

Expand All @@ -64,7 +65,7 @@ func AdminUsers(ctx *context.Context) error {
return ctx.ErrorRes(500, "Cannot get users", err)
}

if err = handlers.Paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil {
if err = handlers.Paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1, nil); err != nil {
return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil)
}

Expand All @@ -82,7 +83,7 @@ func AdminGists(ctx *context.Context) error {
return ctx.ErrorRes(500, "Cannot get gists", err)
}

if err = handlers.Paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil {
if err = handlers.Paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1, nil); err != nil {
return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil)
}

Expand Down
Loading

0 comments on commit 7dc2551

Please sign in to comment.