Add Gist code search (#194)

This commit is contained in:
Thomas Miceli
2024-01-04 03:38:15 +01:00
parent 4cb7dc2d30
commit 87a6113cc7
25 changed files with 734 additions and 87 deletions

View File

@ -46,6 +46,7 @@ func adminIndex(ctx echo.Context) error {
setData(ctx, "gitGcRepos", actions.IsRunning(actions.GitGcRepos))
setData(ctx, "syncGistPreviews", actions.IsRunning(actions.SyncGistPreviews))
setData(ctx, "resetHooks", actions.IsRunning(actions.ResetHooks))
setData(ctx, "indexGists", actions.IsRunning(actions.IndexGists))
return html(ctx, "admin_index.html")
}
@ -116,6 +117,8 @@ func adminGistDelete(ctx echo.Context) error {
return errorRes(500, "Cannot delete this gist", err)
}
gist.RemoveFromIndex()
addFlash(ctx, "Gist has been deleted", "success")
return redirect(ctx, "/admin-panel/gists")
}
@ -150,6 +153,12 @@ func adminResetHooks(ctx echo.Context) error {
return redirect(ctx, "/admin-panel")
}
func adminIndexGists(ctx echo.Context) error {
addFlash(ctx, "Indexing all gists...", "success")
go actions.Run(actions.IndexGists)
return redirect(ctx, "/admin-panel")
}
func adminConfig(ctx echo.Context) error {
setData(ctx, "title", "Configuration")
setData(ctx, "htmlTitle", "Configuration - Admin panel")

View File

@ -8,6 +8,7 @@ import (
"fmt"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/render"
"html/template"
"net/url"
@ -252,20 +253,20 @@ func allGists(ctx echo.Context) error {
}
}
renderedFiles := make([]*render.RenderedGist, 0, len(gists))
renderedGists := make([]*render.RenderedGist, 0, len(gists))
for _, gist := range gists {
rendered, err := render.HighlightGistPreview(gist)
if err != nil {
log.Warn().Err(err).Msg("Error rendering gist preview for " + gist.Identifier() + " - " + gist.PreviewFilename)
log.Error().Err(err).Msg("Error rendering gist preview for " + gist.Identifier() + " - " + gist.PreviewFilename)
}
renderedFiles = append(renderedFiles, &rendered)
renderedGists = append(renderedGists, &rendered)
}
if err != nil {
return errorRes(500, "Error fetching gists", err)
}
if err = paginate(ctx, renderedFiles, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
if err = paginate(ctx, renderedGists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
return errorRes(404, "Page not found", nil)
}
@ -273,6 +274,69 @@ func allGists(ctx echo.Context) error {
return html(ctx, "all.html")
}
func search(ctx echo.Context) error {
var err error
content, meta := parseSearchQueryStr(ctx.QueryParam("q"))
pageInt := getPage(ctx)
var currentUserId uint
userLogged := getUserLogged(ctx)
if userLogged != nil {
currentUserId = userLogged.ID
} else {
currentUserId = 0
}
var visibleGistsIds []uint
visibleGistsIds, err = db.GetAllGistsVisibleByUser(currentUserId)
if err != nil {
return errorRes(500, "Error fetching gists", err)
}
gistsIds, nbHits, langs, err := index.SearchGists(content, index.SearchGistMetadata{
Username: meta["user"],
Title: meta["title"],
Filename: meta["filename"],
Extension: meta["extension"],
Language: meta["language"],
}, visibleGistsIds, pageInt)
if err != nil {
return errorRes(500, "Error searching gists", err)
}
gists, err := db.GetAllGistsByIds(gistsIds)
if err != nil {
return errorRes(500, "Error fetching gists", err)
}
renderedGists := make([]*render.RenderedGist, 0, len(gists))
for _, gist := range gists {
rendered, err := render.HighlightGistPreview(gist)
if err != nil {
log.Error().Err(err).Msg("Error rendering gist preview for " + gist.Identifier() + " - " + gist.PreviewFilename)
}
renderedGists = append(renderedGists, &rendered)
}
if pageInt > 1 && len(renderedGists) != 0 {
setData(ctx, "prevPage", pageInt-1)
}
if 10*pageInt < int(nbHits) {
setData(ctx, "nextPage", pageInt+1)
}
setData(ctx, "prevLabel", tr(ctx, "pagination.previous"))
setData(ctx, "nextLabel", tr(ctx, "pagination.next"))
setData(ctx, "urlPage", "search")
setData(ctx, "urlParams", template.URL("&q="+ctx.QueryParam("q")))
setData(ctx, "htmlTitle", "Search results")
setData(ctx, "nbHits", nbHits)
setData(ctx, "gists", renderedGists)
setData(ctx, "langs", langs)
setData(ctx, "searchQuery", ctx.QueryParam("q"))
return html(ctx, "search.html")
}
func gistIndex(ctx echo.Context) error {
if getData(ctx, "gistpage") == "js" {
return gistJs(ctx)
@ -545,6 +609,8 @@ func processCreate(ctx echo.Context) error {
}
}
gist.AddInIndex()
return redirect(ctx, "/"+user.Username+"/"+gist.Identifier())
}
@ -566,6 +632,7 @@ func deleteGist(ctx echo.Context) error {
if err := gist.Delete(); err != nil {
return errorRes(500, "Error deleting this gist", err)
}
gist.RemoveFromIndex()
addFlash(ctx, "Gist has been deleted", "success")
return redirect(ctx, "/")

View File

@ -218,6 +218,7 @@ func pack(ctx echo.Context, serviceType string) error {
_ = gist.SetLastActiveNow()
_ = gist.UpdatePreviewAndCount(false)
gist.AddInIndex()
}
return nil
}

View File

@ -3,7 +3,9 @@ package web
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/thomiceli/opengist/internal/index"
htmlpkg "html"
"html/template"
"io"
@ -115,6 +117,22 @@ var (
"safe": func(s string) template.HTML {
return template.HTML(s)
},
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{})
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
"addMetadataToSearchQuery": addMetadataToSearchQuery,
"indexEnabled": index.Enabled,
}
)
@ -223,7 +241,6 @@ func NewServer(isDev bool) *Server {
g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged)
g1.PUT("/settings/password", passwordProcess, logged)
g1.PUT("/settings/username", usernameProcess, logged)
g2 := g1.Group("/admin-panel")
{
g2.Use(adminPermission)
@ -237,6 +254,7 @@ func NewServer(isDev bool) *Server {
g2.POST("/gc-repos", adminGcRepos)
g2.POST("/sync-previews", adminSyncGistPreviews)
g2.POST("/reset-hooks", adminResetHooks)
g2.POST("/index-gists", adminIndexGists)
g2.GET("/configuration", adminConfig)
g2.PUT("/set-config", adminSetConfig)
}
@ -246,7 +264,13 @@ func NewServer(isDev bool) *Server {
}
g1.GET("/all", allGists, checkRequireLogin)
g1.GET("/search", allGists, checkRequireLogin)
if index.Enabled() {
g1.GET("/search", search, checkRequireLogin)
} else {
g1.GET("/search", allGists, checkRequireLogin)
}
g1.GET("/:user", allGists, checkRequireLogin)
g1.GET("/:user/liked", allGists, checkRequireLogin)
g1.GET("/:user/forked", allGists, checkRequireLogin)

View File

@ -1,11 +1,10 @@
package test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"testing"
)
func TestGists(t *testing.T) {

View File

@ -133,14 +133,13 @@ func setup(t *testing.T) {
git.ReposDirectory = path.Join("tests")
config.C.IndexEnabled = false
config.C.LogLevel = "debug"
config.InitLog()
homePath := config.GetHomeDir()
log.Info().Msg("Data directory: " + homePath)
err = os.MkdirAll(filepath.Join(homePath, "repos"), 0755)
require.NoError(t, err, "Could not create repos directory")
err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
require.NoError(t, err, "Could not create tmp repos directory")
@ -149,6 +148,9 @@ func setup(t *testing.T) {
err = memdb.Setup()
require.NoError(t, err, "Could not initialize in memory database")
// err = index.Open(filepath.Join(homePath, "testsindex", "opengist.index"))
// require.NoError(t, err, "Could not open index")
}
func teardown(t *testing.T, s *testServer) {
@ -159,4 +161,10 @@ func teardown(t *testing.T, s *testServer) {
err = os.RemoveAll(path.Join(config.C.OpengistHome, "tests"))
require.NoError(t, err, "Could not remove repos directory")
// err = os.RemoveAll(path.Join(config.C.OpengistHome, "testsindex"))
// require.NoError(t, err, "Could not remove repos directory")
// err = index.Close()
// require.NoError(t, err, "Could not close index")
}

View File

@ -248,6 +248,46 @@ func tr(ctx echo.Context, key string) template.HTML {
return l.Tr(key)
}
func parseSearchQueryStr(query string) (string, map[string]string) {
words := strings.Fields(query)
metadata := make(map[string]string)
var contentBuilder strings.Builder
for _, word := range words {
if strings.Contains(word, ":") {
keyValue := strings.SplitN(word, ":", 2)
if len(keyValue) == 2 {
key := keyValue[0]
value := keyValue[1]
metadata[key] = value
}
} else {
contentBuilder.WriteString(word + " ")
}
}
content := strings.TrimSpace(contentBuilder.String())
return content, metadata
}
func addMetadataToSearchQuery(input, key, value string) string {
content, metadata := parseSearchQueryStr(input)
metadata[key] = value
var resultBuilder strings.Builder
resultBuilder.WriteString(content)
for k, v := range metadata {
resultBuilder.WriteString(" ")
resultBuilder.WriteString(k)
resultBuilder.WriteString(":")
resultBuilder.WriteString(v)
}
return strings.TrimSpace(resultBuilder.String())
}
type Argon2ID struct {
format string
version int