From 86ad88fb093a2392cc2e32d09ab273bbdc04ce9c Mon Sep 17 00:00:00 2001 From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com> Date: Tue, 30 Jan 2024 01:02:28 +0100 Subject: [PATCH] Set gist URL and title via push options (#216) --- docs/usage/git-push-options.md | 13 ++++++ internal/hooks/post_receive.go | 38 +++++++++++++++-- internal/utils/validator.go | 74 ++++++++++++++++++++++++++++++++++ internal/web/auth.go | 3 +- internal/web/gist.go | 3 +- internal/web/server.go | 3 +- internal/web/settings.go | 7 ++-- internal/web/util.go | 68 ------------------------------- 8 files changed, 131 insertions(+), 78 deletions(-) create mode 100644 internal/utils/validator.go diff --git a/docs/usage/git-push-options.md b/docs/usage/git-push-options.md index 3bc17fc..218d6d7 100644 --- a/docs/usage/git-push-options.md +++ b/docs/usage/git-push-options.md @@ -4,6 +4,19 @@ Opengist has support for a few [Git push options](https://git-scm.com/docs/git-p These options are passed to `git push` command and can be used to change the metadata of a gist. +## Set URL + +```shell +git push -o url=mygist # Will set the URL to https://opengist.example.com/user/mygist +``` + +## Change title + +```shell +git push -o title=Gist123 +git push -o title="My Gist 123" +``` + ## Change visibility ```shell diff --git a/internal/hooks/post_receive.go b/internal/hooks/post_receive.go index 5e7948c..f9ee908 100644 --- a/internal/hooks/post_receive.go +++ b/internal/hooks/post_receive.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/git" + "github.com/thomiceli/opengist/internal/utils" "io" "os" "os/exec" @@ -13,7 +14,12 @@ import ( ) func PostReceive(in io.Reader, out, er io.Writer) error { + var outputSb strings.Builder + newGist := false opts := pushOptions() + gistUrl := os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL") + validator := utils.NewValidator() + scanner := bufio.NewScanner(in) for scanner.Scan() { line := scanner.Text() @@ -29,9 +35,7 @@ func PostReceive(in io.Reader, out, er io.Writer) error { } if oldrev == BaseHash { - _, _ = fmt.Fprintf(out, "\nYour new repository has been created here: %s\n\n", os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL")) - _, _ = fmt.Fprintln(out, "If you want to keep working with your gist, you could set the remote URL via:") - _, _ = fmt.Fprintf(out, "git remote set-url origin %s\n\n", os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL")) + newGist = true } } @@ -43,7 +47,22 @@ 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"]) - _, _ = fmt.Fprintf(out, "\nGist visibility set to %s\n\n", opts["visibility"]) + outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"])) + } + + if opts["url"] != "" && validator.Var(opts["url"], "max=32,alphanumdashorempty") == nil { + gist.URL = opts["url"] + lastIndex := strings.LastIndex(gistUrl, "/") + gistUrl = gistUrl[:lastIndex+1] + gist.URL + if !newGist { + outputSb.WriteString(fmt.Sprintf("Gist URL set to %s. Set the Git remote URL via:\n", gistUrl)) + outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl)) + } + } + + if opts["title"] != "" && validator.Var(opts["title"], "max=250") == nil { + gist.Title = opts["title"] + outputSb.WriteString(fmt.Sprintf("Gist title set to \"%s\"\n\n", opts["title"])) } if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil { @@ -65,6 +84,17 @@ func PostReceive(in io.Reader, out, er io.Writer) error { gist.AddInIndex() + if newGist { + outputSb.WriteString(fmt.Sprintf("Your new gist has been created here: %s\n", gistUrl)) + outputSb.WriteString("If you want to keep working with your gist, you could set the Git remote URL via:\n") + outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl)) + } + + outputStr := outputSb.String() + if outputStr != "" { + _, _ = fmt.Fprint(out, "\n"+outputStr) + } + return nil } diff --git a/internal/utils/validator.go b/internal/utils/validator.go new file mode 100644 index 0000000..d9a72c7 --- /dev/null +++ b/internal/utils/validator.go @@ -0,0 +1,74 @@ +package utils + +import ( + "github.com/go-playground/validator/v10" + "regexp" + "strings" +) + +type OpengistValidator struct { + v *validator.Validate +} + +func NewValidator() *OpengistValidator { + v := validator.New() + _ = v.RegisterValidation("notreserved", validateReservedKeywords) + _ = v.RegisterValidation("alphanumdash", validateAlphaNumDash) + _ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty) + return &OpengistValidator{v} +} + +func (cv *OpengistValidator) Validate(i interface{}) error { + return cv.v.Struct(i) +} + +func (cv *OpengistValidator) Var(field interface{}, tag string) error { + return cv.v.Var(field, tag) +} + +func ValidationMessages(err *error) string { + errs := (*err).(validator.ValidationErrors) + messages := make([]string, len(errs)) + for i, e := range errs { + switch e.Tag() { + case "max": + messages[i] = e.Field() + " is too long" + case "required": + messages[i] = e.Field() + " should not be empty" + case "excludes": + messages[i] = e.Field() + " should not include a sub directory" + case "alphanum": + messages[i] = e.Field() + " should only contain alphanumeric characters" + case "alphanumdash": + case "alphanumdashorempty": + messages[i] = e.Field() + " should only contain alphanumeric characters and dashes" + case "min": + messages[i] = "Not enough " + e.Field() + case "notreserved": + messages[i] = "Invalid " + e.Field() + } + } + + return strings.Join(messages, " ; ") +} + +func validateReservedKeywords(fl validator.FieldLevel) bool { + name := fl.Field().String() + + restrictedNames := map[string]struct{}{} + for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck"} { + restrictedNames[restrictedName] = struct{}{} + } + + // if the name is not in the restricted names, it is valid + _, ok := restrictedNames[name] + return !ok +} + +func validateAlphaNumDash(fl validator.FieldLevel) bool { + return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String()) +} + +func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool { + return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String()) +} diff --git a/internal/web/auth.go b/internal/web/auth.go index ece9067..c43b688 100644 --- a/internal/web/auth.go +++ b/internal/web/auth.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/thomiceli/opengist/internal/utils" "io" "net/http" "net/url" @@ -63,7 +64,7 @@ func processRegister(ctx echo.Context) error { } if err := ctx.Validate(dto); err != nil { - addFlash(ctx, validationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err), "error") return html(ctx, "auth_form.html") } diff --git a/internal/web/gist.go b/internal/web/gist.go index 47c36e4..0d943cf 100644 --- a/internal/web/gist.go +++ b/internal/web/gist.go @@ -10,6 +10,7 @@ import ( "github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/index" "github.com/thomiceli/opengist/internal/render" + "github.com/thomiceli/opengist/internal/utils" "html/template" "net/url" "path/filepath" @@ -539,7 +540,7 @@ func processCreate(ctx echo.Context) error { err = ctx.Validate(dto) if err != nil { - addFlash(ctx, validationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err), "error") if isCreate { return html(ctx, "create.html") } else { diff --git a/internal/web/server.go b/internal/web/server.go index 2aaea44..8b41640 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/thomiceli/opengist/internal/index" + "github.com/thomiceli/opengist/internal/utils" htmlpkg "html" "html/template" "io" @@ -205,7 +206,7 @@ func NewServer(isDev bool) *Server { e.Use(sessionInit) - e.Validator = NewValidator() + e.Validator = utils.NewValidator() if !dev { parseManifestEntries() diff --git a/internal/web/settings.go b/internal/web/settings.go index 8d1f583..33c0a1e 100644 --- a/internal/web/settings.go +++ b/internal/web/settings.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/git" + "github.com/thomiceli/opengist/internal/utils" "os" "path/filepath" "strconv" @@ -73,7 +74,7 @@ func sshKeysProcess(ctx echo.Context) error { } if err := ctx.Validate(dto); err != nil { - addFlash(ctx, validationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err), "error") return redirect(ctx, "/settings") } key := dto.ToSSHKey() @@ -126,7 +127,7 @@ func passwordProcess(ctx echo.Context) error { dto.Username = user.Username if err := ctx.Validate(dto); err != nil { - addFlash(ctx, validationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err), "error") return html(ctx, "settings.html") } @@ -154,7 +155,7 @@ func usernameProcess(ctx echo.Context) error { dto.Password = user.Password if err := ctx.Validate(dto); err != nil { - addFlash(ctx, validationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err), "error") return redirect(ctx, "/settings") } diff --git a/internal/web/util.go b/internal/web/util.go index 5547581..8adb65b 100644 --- a/internal/web/util.go +++ b/internal/web/util.go @@ -7,7 +7,6 @@ import ( "encoding/base64" "errors" "fmt" - "github.com/go-playground/validator/v10" "github.com/gorilla/sessions" "github.com/labstack/echo/v4" "github.com/thomiceli/opengist/internal/config" @@ -16,7 +15,6 @@ import ( "golang.org/x/crypto/argon2" "html/template" "net/http" - "regexp" "strconv" "strings" ) @@ -129,72 +127,6 @@ func loadSettings(ctx echo.Context) error { return nil } -type OpengistValidator struct { - v *validator.Validate -} - -func NewValidator() *OpengistValidator { - v := validator.New() - _ = v.RegisterValidation("notreserved", validateReservedKeywords) - _ = v.RegisterValidation("alphanumdash", validateAlphaNumDash) - _ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty) - return &OpengistValidator{v} -} - -func (cv *OpengistValidator) Validate(i interface{}) error { - if err := cv.v.Struct(i); err != nil { - return err - } - return nil -} - -func validationMessages(err *error) string { - errs := (*err).(validator.ValidationErrors) - messages := make([]string, len(errs)) - for i, e := range errs { - switch e.Tag() { - case "max": - messages[i] = e.Field() + " is too long" - case "required": - messages[i] = e.Field() + " should not be empty" - case "excludes": - messages[i] = e.Field() + " should not include a sub directory" - case "alphanum": - messages[i] = e.Field() + " should only contain alphanumeric characters" - case "alphanumdash": - case "alphanumdashorempty": - messages[i] = e.Field() + " should only contain alphanumeric characters and dashes" - case "min": - messages[i] = "Not enough " + e.Field() - case "notreserved": - messages[i] = "Invalid " + e.Field() - } - } - - return strings.Join(messages, " ; ") -} - -func validateReservedKeywords(fl validator.FieldLevel) bool { - name := fl.Field().String() - - restrictedNames := map[string]struct{}{} - for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck"} { - restrictedNames[restrictedName] = struct{}{} - } - - // if the name is not in the restricted names, it is valid - _, ok := restrictedNames[name] - return !ok -} - -func validateAlphaNumDash(fl validator.FieldLevel) bool { - return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String()) -} - -func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool { - return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String()) -} - func getPage(ctx echo.Context) int { page := ctx.QueryParam("page") if page == "" {