Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support system/user default clone editors #22378

Closed
wants to merge 16 commits into from
6 changes: 3 additions & 3 deletions models/user/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,18 +150,18 @@ func DeleteUserSetting(userID int64, key string) error {
}

// SetUserSetting updates a users' setting for a specific key
func SetUserSetting(userID int64, key, value string) error {
func SetUserSetting(uid int64, key, value string) error {
if err := validateUserSettingKey(key); err != nil {
return err
}

if err := upsertUserSettingValue(userID, key, value); err != nil {
if err := upsertUserSettingValue(uid, strings.ToLower(key), value); err != nil {
return err
}

cc := cache.GetCache()
if cc != nil {
return cc.Put(genSettingCacheKey(userID, key), value, setting_module.CacheService.TTLSeconds())
return cc.Put(genSettingCacheKey(uid, key), value, setting_module.CacheService.TTLSeconds())
}

return nil
Expand Down
7 changes: 7 additions & 0 deletions modules/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/dev"

"github.com/editorconfig/editorconfig-core-go/v2"
)
Expand Down Expand Up @@ -588,6 +589,12 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
}
ctx.Data["CloneButtonShowHTTPS"] = cloneButtonShowHTTPS
ctx.Data["CloneButtonShowSSH"] = cloneButtonShowSSH
editors, err := dev.GetUserDefaultEditorsWithFallback(ctx.Doer)
if err != nil {
ctx.ServerError("dev.GetDefaultEditor", err)
return
}
ctx.Data["CloneEditors"] = editors
ctx.Data["CloneButtonOriginLink"] = ctx.Data["RepoCloneLink"] // it may be rewritten to the WikiCloneLink by the router middleware

ctx.Data["RepoSearchEnabled"] = setting.Indexer.RepoIndexerEnabled
Expand Down
6 changes: 6 additions & 0 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func NewFuncMap() []template.FuncMap {
},
"Safe": Safe,
"SafeJS": SafeJS,
"SafeURL": SafeURL,
"JSEscape": JSEscape,
"Str2html": Str2html,
"TimeSince": timeutil.TimeSince,
Expand Down Expand Up @@ -671,6 +672,11 @@ func Safe(raw string) template.HTML {
return template.HTML(raw)
}

// SafeURL render raw as URL
func SafeURL(raw string) template.URL {
return template.URL(raw)
}

// SafeJS renders raw as JS
func SafeJS(raw string) template.JS {
return template.JS(raw)
Expand Down
14 changes: 14 additions & 0 deletions modules/util/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,17 @@ func SliceRemoveAllFunc[T comparable](slice []T, targetFunc func(T) bool) []T {
}
return slice[:idx]
}

func SplitStringWithTrim(s, sep string) []string {
if len(s) == 0 {
return nil
}
var result []string
for _, word := range strings.Split(s, sep) {
word = strings.TrimSpace(word)
if len(word) > 0 {
result = append(result, word)
}
}
return result
}
6 changes: 5 additions & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,7 @@ already_forked = You've already forked %s
fork_to_different_account = Fork to a different account
fork_visibility_helper = The visibility of a forked repository cannot be changed.
use_template = Use this template
clone_in_vsc = Clone in VS Code
clone_in_editor = Clone in %s
download_zip = Download ZIP
download_tar = Download TAR.GZ
download_bundle = Download BUNDLE
Expand Down Expand Up @@ -2943,6 +2943,10 @@ config.xorm_log_sql = Log SQL
config.get_setting_failed = Get setting %s failed
config.set_setting_failed = Set setting %s failed

config.dev_config = Development
config.dev_default_editors = Default Editors
config.dev_default_editors_desc = Choose default editors

monitor.cron = Cron Tasks
monitor.name = Name
monitor.schedule = Schedule
Expand Down
1 change: 1 addition & 0 deletions public/img/svg/gitea-idea.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/img/svg/gitea-vscodium.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions routers/web/admin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/dev"
"code.gitea.io/gitea/services/mailer"

"gitea.com/go-chi/session"
Expand Down Expand Up @@ -189,6 +190,20 @@ func Config(ctx *context.Context) {
ctx.Data["EnableXORMLog"] = setting.EnableXORMLog
ctx.Data["LogSQL"] = setting.Database.LogSQL

editors := dev.GetEditors()

defaultEditorS := systemSettings.Get(dev.KeyDevDefaultEditors)
if defaultEditorS.SettingValue == "" {
defaultEditorS = system_model.Setting{
SettingKey: dev.KeyDevDefaultEditors,
SettingValue: dev.DefaultEditorsNames(),
}
}

ctx.Data["DevEditors"] = editors
ctx.Data["DevDefaultEditorNames"] = defaultEditorS.SettingValue
ctx.Data["DevDefaultEditorVersion"] = defaultEditorS.Version

ctx.HTML(http.StatusOK, tplConfig)
}

Expand Down
43 changes: 43 additions & 0 deletions routers/web/user/setting/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/agit"
"code.gitea.io/gitea/services/dev"
"code.gitea.io/gitea/services/forms"
container_service "code.gitea.io/gitea/services/packages/container"
user_service "code.gitea.io/gitea/services/user"
Expand Down Expand Up @@ -372,9 +373,51 @@ func Appearance(ctx *context.Context) {
return forms.IsUserHiddenCommentTypeGroupChecked(commentTypeGroup, hiddenCommentTypes)
}

editors := dev.GetEditors()

myDefaultEditors, err := dev.GetUserDefaultEditorsWithFallback(ctx.Doer)
if err != nil {
ctx.ServerError("dev.GetEditors", err)
return
}

var myEditorNames string
for i, editor := range myDefaultEditors {
if i > 0 {
myEditorNames += ","
}
myEditorNames += editor.Name
}

ctx.Data["DevEditors"] = editors
ctx.Data["DevDefaultEditorNames"] = myEditorNames

ctx.HTML(http.StatusOK, tplSettingsAppearance)
}

func ChangeConfig(ctx *context.Context) {
key := strings.TrimSpace(ctx.FormString("key"))
if key == "" {
ctx.JSON(http.StatusOK, map[string]string{
"redirect": ctx.Req.URL.String(),
})
return
}
value := ctx.FormString("value")

if err := user_model.SetUserSetting(ctx.Doer.ID, key, value); err != nil {
log.Error("set setting failed: %v", err)
ctx.JSON(http.StatusOK, map[string]string{
"err": ctx.Tr("admin.config.set_setting_failed", key),
})
return
}

ctx.JSON(http.StatusOK, map[string]interface{}{
"version": 1,
})
}

// UpdateUIThemePost is used to update users' specific theme
func UpdateUIThemePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateThemeForm)
Expand Down
8 changes: 8 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"code.gitea.io/gitea/routers/web/user/setting/security"
auth_service "code.gitea.io/gitea/services/auth"
context_service "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/dev"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/lfs"

Expand Down Expand Up @@ -405,6 +406,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/user/settings", func() {
m.Get("", user_setting.Profile)
m.Post("", web.Bind(forms.UpdateProfileForm{}), user_setting.ProfilePost)
m.Post("/config", user_setting.ChangeConfig)
m.Get("/change_password", auth.MustChangePassword)
m.Post("/change_password", web.Bind(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost)
m.Post("/avatar", web.Bind(forms.AvatarForm{}), user_setting.AvatarPost)
Expand Down Expand Up @@ -1308,6 +1310,12 @@ func RegisterRoutes(m *web.Route) {
m.Get("/commit/{sha:[a-f0-9]{7,40}}.{ext:patch|diff}", repo.RawDiff)
}, repo.MustEnableWiki, func(ctx *context.Context) {
ctx.Data["PageIsWiki"] = true
editors, err := dev.GetUserDefaultEditorsWithFallback(ctx.Doer)
if err != nil {
ctx.ServerError("dev.GetDefaultEditor", err)
return
}
ctx.Data["CloneEditors"] = editors
ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink()
})

Expand Down
136 changes: 136 additions & 0 deletions services/dev/editor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package dev

import (
"html/template"
"strings"

"code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
)

const KeyDevDefaultEditors = "dev.default_editors"

type Editor struct {
Name string
URL string
Icon string
}

func (e *Editor) RenderURL(repoURL string) template.URL {
return template.URL(strings.ReplaceAll(e.URL, "${repo_url}", repoURL))
}

var defaultEditors = []Editor{
{
Name: "VS Code",
URL: "vscode://vscode.git/clone?url=${repo_url}",
Icon: `gitea-vscode`,
},
{
Name: "VSCodium",
URL: "vscodium://vscode.git/clone?url=${repo_url}",
Icon: `gitea-vscodium`,
},
{
Name: "IDEA",
URL: "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=${repo_url}",
Icon: `gitea-idea`,
},
}

func GetEditorByName(name string) *Editor {
for _, editor := range defaultEditors {
if editor.Name == name {
return &editor
}
}
return nil
}

func GetEditorsByNames(names []string) []*Editor {
editors := make([]*Editor, 0, len(names))
for _, name := range names {
if editor := GetEditorByName(name); editor != nil {
editors = append(editors, editor)
} else {
log.Error("Unknown editor: %s", name)
}
}
return editors
}

// GetEditors returns all editors
func GetEditors() []Editor {
return defaultEditors
}

func DefaultEditorsNames() string {
return defaultEditors[0].Name
}

func GetDefaultEditors() ([]*Editor, error) {
defaultNames, err := system.GetSetting(KeyDevDefaultEditors)
if err != nil {
if system.IsErrSettingIsNotExist(err) {
return nil, nil
}
return nil, err
}
names := strings.Split(defaultNames, ",")
return GetEditorsByNames(names), nil
}

func SetDefaultEditors(names []string) error {
var validateNames []string
for _, name := range names {
if editor := GetEditorByName(name); editor != nil {
validateNames = append(validateNames, name)
}
}

return system.SetSetting(&system.Setting{
SettingKey: KeyDevDefaultEditors,
SettingValue: strings.Join(validateNames, ","),
})
}

func GetUserDefaultEditors(userID int64) ([]*Editor, error) {
defaultNames, err := user_model.GetSetting(userID, KeyDevDefaultEditors)
if err != nil {
if user_model.IsErrUserSettingIsNotExist(err) {
return nil, nil
}
return nil, err
}
names := strings.Split(defaultNames, ",")
return GetEditorsByNames(names), nil
}

func SetUserDefaultEditors(userID int64, names []string) error {
var validateNames []string
for _, name := range names {
if editor := GetEditorByName(name); editor != nil {
validateNames = append(validateNames, name)
}
}
return user_model.SetUserSetting(userID, KeyDevDefaultEditors, strings.Join(validateNames, ","))
}

func GetUserDefaultEditorsWithFallback(user *user_model.User) ([]*Editor, error) {
if user == nil || user.ID <= 0 {
return GetDefaultEditors()
}
editor, err := GetUserDefaultEditors(user.ID)
if err == nil {
return editor, nil
}

if user_model.IsErrUserSettingIsNotExist(err) {
return GetDefaultEditors()
}
return nil, err
}
27 changes: 27 additions & 0 deletions templates/admin/config.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,33 @@
{{end}}
</dl>
</div>

<h4 class="ui top attached header">
{{.locale.Tr "admin.config.dev_config"}}
</h4>
<div class="ui attached segment">
<div class="ui list">
<div class="item">
{{$.locale.Tr "admin.config.dev_default_editors"}}
</div>
<div class="field">
<div id="dev_editors_config" class="ui floating selection multiple dropdown">
<input type="hidden" name="dev.default_editors" value="{{$.DevDefaultEditorNames}}" version="{{$.DevDefaultEditorVersion}}">
<div class="default text">
{{$.locale.Tr "admin.config.dev_default_editors_desc"}}
</div>
<div class="menu floating">
{{range .DevEditors}}
<div class="item" data-value="{{.Name}}">
{{svg .Icon 14}}
{{.Name}} - {{.URL}}
</div>
{{end}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}
Loading