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

Customizable "Open with" applications for repository clone #29320

Merged
merged 5 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func Contexter() func(next http.Handler) http.Handler {
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)

ctx.Data["SystemConfig"] = setting.Config()
ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)

Expand Down
49 changes: 46 additions & 3 deletions modules/setting/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,45 @@ type PictureStruct struct {
EnableFederatedAvatar *config.Value[bool]
}

type OpenWithEditorApp struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there perhaps any way to set a custom icon, i.e. as a file path?
Once we have this option, this feature is truly complete.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have left a TODO in code:

iconHTML = svg.RenderHTML("gitea-git", 16, "gt-mr-3") // TODO: it could support user's customized icon in the future

To support "custom icon", it needs far more changes: it needs an extra field, and needs a new UI editor for the config items (at least, a YAML or JSON editor). It still needs the end user to upload the icon to "somewhere", I guess most users would be quite lazy to do so .....

So at the moment, the default "git" icons looks not bad.

Copy link
Member

@silverwind silverwind Feb 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SVG icons could be entered as text in that box, e.g. name=url,icon=<svg/>. That said, I kind of dislike that the current syntax is not pure key=value pairs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that's why I think "and needs a new UI editor for the config items (at least, a YAML or JSON editor)".

Let's collect more user feedbacks, to see how to move on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we better already change the syntax to yaml/json now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's quite easy because it is stored as JSON in database. So, it could be rendered as JSON or YAML in the editor easily. It won't block anything.

DisplayName string
OpenURL string
}

type OpenWithEditorAppsType []OpenWithEditorApp

func (t OpenWithEditorAppsType) ToTextareaString() string {
ret := ""
for _, app := range t {
ret += app.DisplayName + " = " + app.OpenURL + "\n"
}
return ret
}

func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
return OpenWithEditorAppsType{
{
DisplayName: "VS Code",
OpenURL: "vscode://vscode.git/clone?url={url}",
},
{
DisplayName: "VSCodium",
OpenURL: "vscodium://vscode.git/clone?url={url}",
},
{
DisplayName: "Intellij IDEA",
OpenURL: "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo={url}",
},
}
}

type RepositoryStruct struct {
OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
}

type ConfigStruct struct {
Picture *PictureStruct
Picture *PictureStruct
Repository *RepositoryStruct
}

var (
Expand All @@ -28,8 +65,11 @@ func initDefaultConfig() {
config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
defaultConfig = &ConfigStruct{
Picture: &PictureStruct{
DisableGravatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"),
EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"),
DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
},
Repository: &RepositoryStruct{
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
},
}
}
Expand All @@ -42,6 +82,9 @@ func Config() *ConfigStruct {
type cfgSecKeyGetter struct{}

func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
if key == "" {
return "", false
}
cfgSec, err := CfgProvider.GetSection(sec)
if err != nil {
log.Error("Unable to get config section: %q", sec)
Expand Down
35 changes: 24 additions & 11 deletions modules/setting/config/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ package config

import (
"context"
"strconv"
"sync"

"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)

type CfgSecKey struct {
Expand All @@ -23,14 +26,14 @@ type Value[T any] struct {
revision int
}

func (value *Value[T]) parse(s string) (v T) {
switch any(v).(type) {
case bool:
b, _ := strconv.ParseBool(s)
return any(b).(T)
default:
panic("unsupported config type, please complete the code")
func (value *Value[T]) parse(key, valStr string) (v T) {
v = value.def
if valStr != "" {
if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
}
}
return v
}

func (value *Value[T]) Value(ctx context.Context) (v T) {
Expand Down Expand Up @@ -62,7 +65,7 @@ func (value *Value[T]) Value(ctx context.Context) (v T) {
if valStr == nil {
v = value.def
} else {
v = value.parse(*valStr)
v = value.parse(value.dynKey, *valStr)
}

value.mu.Lock()
Expand All @@ -76,6 +79,16 @@ func (value *Value[T]) DynKey() string {
return value.dynKey
}

func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] {
return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey}
func (value *Value[T]) WithDefault(def T) *Value[T] {
value.def = def
return value
}

func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] {
value.cfgSecKey = cfgSecKey
return value
}

func ValueJSON[T any](dynKey string) *Value[T] {
return &Value[T]{dynKey: dynKey}
}
5 changes: 4 additions & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ fork_branch = Branch to be cloned to the fork
all_branches = All branches
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
use_template = Use this template
clone_in_vsc = Clone in VS Code
open_with_editor = Open with %s
download_zip = Download ZIP
download_tar = Download TAR.GZ
download_bundle = Download BUNDLE
Expand Down Expand Up @@ -2737,6 +2737,8 @@ integrations = Integrations
authentication = Authentication Sources
emails = User Emails
config = Configuration
config_summary = Summary
config_settings = Settings
notices = System Notices
monitor = Monitoring
first_page = First
Expand Down Expand Up @@ -3176,6 +3178,7 @@ config.picture_config = Picture and Avatar Configuration
config.picture_service = Picture Service
config.disable_gravatar = Disable Gravatar
config.enable_federated_avatar = Enable Federated Avatars
config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default.

config.git_config = Git Configuration
config.git_disable_diff_highlight = Disable Diff Syntax Highlight
Expand Down
1 change: 1 addition & 0 deletions public/assets/img/svg/gitea-open-with-jetbrains.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/assets/img/svg/gitea-open-with-vscode.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/assets/img/svg/gitea-open-with-vscodium.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: 0 additions & 1 deletion public/assets/img/svg/gitea-vscode.svg

This file was deleted.

67 changes: 59 additions & 8 deletions routers/web/admin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ package admin
import (
"net/http"
"net/url"
"strconv"
"strings"

system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
Expand All @@ -24,7 +24,10 @@ import (
"gitea.com/go-chi/session"
)

const tplConfig base.TplName = "admin/config"
const (
tplConfig base.TplName = "admin/config"
tplConfigSettings base.TplName = "admin/config_settings"
)

// SendTestMail send test mail to confirm mail service is OK
func SendTestMail(ctx *context.Context) {
Expand Down Expand Up @@ -98,8 +101,9 @@ func shadowPassword(provider, cfgItem string) string {

// Config show admin config page
func Config(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.config")
ctx.Data["Title"] = ctx.Tr("admin.config_summary")
ctx.Data["PageIsAdminConfig"] = true
ctx.Data["PageIsAdminConfigSummary"] = true

ctx.Data["CustomConf"] = setting.CustomConf
ctx.Data["AppUrl"] = setting.AppURL
Expand Down Expand Up @@ -161,23 +165,70 @@ func Config(ctx *context.Context) {

ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
config.GetDynGetter().InvalidateCache()
ctx.Data["SystemConfig"] = setting.Config()
prepareDeprecatedWarningsAlert(ctx)

ctx.HTML(http.StatusOK, tplConfig)
}

func ConfigSettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.config_settings")
ctx.Data["PageIsAdminConfig"] = true
ctx.Data["PageIsAdminConfigSettings"] = true
ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
ctx.HTML(http.StatusOK, tplConfigSettings)
}

func ChangeConfig(ctx *context.Context) {
key := strings.TrimSpace(ctx.FormString("key"))
value := ctx.FormString("value")
cfg := setting.Config()
allowedKeys := container.SetOf(cfg.Picture.DisableGravatar.DynKey(), cfg.Picture.EnableFederatedAvatar.DynKey())
if !allowedKeys.Contains(key) {

marshalBool := func(v string) (string, error) {
if b, _ := strconv.ParseBool(v); b {
return "true", nil
}
return "false", nil
}
marshalOpenWithApps := func(value string) (string, error) {
lines := strings.Split(value, "\n")
var openWithEditorApps setting.OpenWithEditorAppsType
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
displayName, openURL, ok := strings.Cut(line, "=")
displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL)
if !ok || displayName == "" || openURL == "" {
continue
}
openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{
DisplayName: strings.TrimSpace(displayName),
OpenURL: strings.TrimSpace(openURL),
})
}
b, err := json.Marshal(openWithEditorApps)
if err != nil {
return "", err
}
return string(b), nil
}
marshallers := map[string]func(string) (string, error){
cfg.Picture.DisableGravatar.DynKey(): marshalBool,
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
}
marshaller, hasMarshaller := marshallers[key]
if !hasMarshaller {
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
return
}
marshaledValue, err := marshaller(value)
if err != nil {
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
return
}
if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil {
log.Error("set setting failed: %v", err)
if err = system_model.SetSettings(ctx, map[string]string{key: marshaledValue}); err != nil {
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
return
}
Expand Down
29 changes: 27 additions & 2 deletions routers/web/repo/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/feed"
Expand Down Expand Up @@ -792,7 +793,7 @@ func Home(ctx *context.Context) {
return
}

renderCode(ctx)
renderHomeCode(ctx)
}

// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
Expand Down Expand Up @@ -919,9 +920,33 @@ func renderRepoTopics(ctx *context.Context) {
ctx.Data["Topics"] = topics
}

func renderCode(ctx *context.Context) {
func prepareOpenWithEditorApps(ctx *context.Context) {
var tmplApps []map[string]any
apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
if len(apps) == 0 {
apps = setting.DefaultOpenWithEditorApps()
}
for _, app := range apps {
schema, _, _ := strings.Cut(app.OpenURL, ":")
var iconHTML template.HTML
if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-open-with-%s", schema), 16, "gt-mr-3")
} else {
iconHTML = svg.RenderHTML("gitea-git", 16, "gt-mr-3") // TODO: it could support user's customized icon in the future
}
tmplApps = append(tmplApps, map[string]any{
"DisplayName": app.DisplayName,
"OpenURL": app.OpenURL,
"IconHTML": iconHTML,
})
}
ctx.Data["OpenWithEditorApps"] = tmplApps
}

func renderHomeCode(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
prepareOpenWithEditorApps(ctx)

if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
showEmpty := true
Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ func registerRoutes(m *web.Route) {
m.Get("", admin.Config)
m.Post("", admin.ChangeConfig)
m.Post("/test_mail", admin.SendTestMail)
m.Get("/settings", admin.ConfigSettings)
})

m.Group("/monitor", func() {
Expand Down
21 changes: 0 additions & 21 deletions templates/admin/config.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -283,27 +283,6 @@
</dl>
</div>

<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.config.picture_config"}}
</h4>
<div class="ui attached table segment">
<dl class="admin-dl-horizontal">
<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
<dd>
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
</div>
</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
<dd>
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
</div>
</dd>
</dl>
</div>

<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.config.git_config"}}
</h4>
Expand Down
Loading
Loading