From 5f4d83b04062e3893ecc3980cb5d21d0c8059eb1 Mon Sep 17 00:00:00 2001 From: Garret Buell Date: Wed, 4 Mar 2026 13:23:51 -0800 Subject: [PATCH] feat: add default-repo-visibility server setting Add a configurable server setting to control whether new repositories are created as public or private by default. Affects push-to-create (SSH and HTTP) and the explicit `repo create` command. Usage: `ssh ... settings default-repo-visibility [public|private]` --- pkg/backend/settings.go | 36 +++++++++++++++++++ .../migrate/0004_default_repo_visibility.go | 23 ++++++++++++ ..._default_repo_visibility_postgres.down.sql | 1 + ...04_default_repo_visibility_postgres.up.sql | 1 + ...04_default_repo_visibility_sqlite.down.sql | 1 + ...0004_default_repo_visibility_sqlite.up.sql | 1 + pkg/db/migrate/migrations.go | 1 + pkg/ssh/cmd/create.go | 3 ++ pkg/ssh/cmd/git.go | 2 +- pkg/ssh/cmd/settings.go | 29 +++++++++++++++ pkg/store/database/settings.go | 17 +++++++++ pkg/store/settings.go | 2 ++ pkg/web/git.go | 2 +- testscript/testdata/settings.txtar | 18 ++++++++++ 14 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 pkg/db/migrate/0004_default_repo_visibility.go create mode 100644 pkg/db/migrate/0004_default_repo_visibility_postgres.down.sql create mode 100644 pkg/db/migrate/0004_default_repo_visibility_postgres.up.sql create mode 100644 pkg/db/migrate/0004_default_repo_visibility_sqlite.down.sql create mode 100644 pkg/db/migrate/0004_default_repo_visibility_sqlite.up.sql diff --git a/pkg/backend/settings.go b/pkg/backend/settings.go index 3b7ddbf18..8689b202c 100644 --- a/pkg/backend/settings.go +++ b/pkg/backend/settings.go @@ -56,3 +56,39 @@ func (b *Backend) SetAnonAccess(ctx context.Context, level access.AccessLevel) e return b.store.SetAnonAccess(ctx, tx, level) }) } + +// IsDefaultRepoPrivate returns whether new repositories are created as private +// by default. +func (b *Backend) IsDefaultRepoPrivate(ctx context.Context) bool { + var visibility string + if err := b.db.TransactionContext(ctx, func(tx *db.Tx) error { + var err error + visibility, err = b.store.GetDefaultRepoVisibility(ctx, tx) + return err + }); err != nil { + return false + } + + return visibility == "private" +} + +// DefaultRepoVisibility returns the default repo visibility setting. +func (b *Backend) DefaultRepoVisibility(ctx context.Context) string { + var visibility string + if err := b.db.TransactionContext(ctx, func(tx *db.Tx) error { + var err error + visibility, err = b.store.GetDefaultRepoVisibility(ctx, tx) + return err + }); err != nil { + return "public" + } + + return visibility +} + +// SetDefaultRepoVisibility sets the default repo visibility. +func (b *Backend) SetDefaultRepoVisibility(ctx context.Context, visibility string) error { + return b.db.TransactionContext(ctx, func(tx *db.Tx) error { + return b.store.SetDefaultRepoVisibility(ctx, tx, visibility) + }) +} diff --git a/pkg/db/migrate/0004_default_repo_visibility.go b/pkg/db/migrate/0004_default_repo_visibility.go new file mode 100644 index 000000000..36a0b3e12 --- /dev/null +++ b/pkg/db/migrate/0004_default_repo_visibility.go @@ -0,0 +1,23 @@ +package migrate + +import ( + "context" + + "github.com/charmbracelet/soft-serve/pkg/db" +) + +const ( + defaultRepoVisibilityName = "default_repo_visibility" + defaultRepoVisibilityVersion = 4 +) + +var defaultRepoVisibility = Migration{ + Name: defaultRepoVisibilityName, + Version: defaultRepoVisibilityVersion, + Migrate: func(ctx context.Context, tx *db.Tx) error { + return migrateUp(ctx, tx, defaultRepoVisibilityVersion, defaultRepoVisibilityName) + }, + Rollback: func(ctx context.Context, tx *db.Tx) error { + return migrateDown(ctx, tx, defaultRepoVisibilityVersion, defaultRepoVisibilityName) + }, +} diff --git a/pkg/db/migrate/0004_default_repo_visibility_postgres.down.sql b/pkg/db/migrate/0004_default_repo_visibility_postgres.down.sql new file mode 100644 index 000000000..a214525d7 --- /dev/null +++ b/pkg/db/migrate/0004_default_repo_visibility_postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM settings WHERE "key" = 'default_repo_visibility'; diff --git a/pkg/db/migrate/0004_default_repo_visibility_postgres.up.sql b/pkg/db/migrate/0004_default_repo_visibility_postgres.up.sql new file mode 100644 index 000000000..67efe5ca9 --- /dev/null +++ b/pkg/db/migrate/0004_default_repo_visibility_postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO settings ("key", value, updated_at) VALUES ('default_repo_visibility', 'public', CURRENT_TIMESTAMP); diff --git a/pkg/db/migrate/0004_default_repo_visibility_sqlite.down.sql b/pkg/db/migrate/0004_default_repo_visibility_sqlite.down.sql new file mode 100644 index 000000000..a214525d7 --- /dev/null +++ b/pkg/db/migrate/0004_default_repo_visibility_sqlite.down.sql @@ -0,0 +1 @@ +DELETE FROM settings WHERE "key" = 'default_repo_visibility'; diff --git a/pkg/db/migrate/0004_default_repo_visibility_sqlite.up.sql b/pkg/db/migrate/0004_default_repo_visibility_sqlite.up.sql new file mode 100644 index 000000000..67efe5ca9 --- /dev/null +++ b/pkg/db/migrate/0004_default_repo_visibility_sqlite.up.sql @@ -0,0 +1 @@ +INSERT INTO settings ("key", value, updated_at) VALUES ('default_repo_visibility', 'public', CURRENT_TIMESTAMP); diff --git a/pkg/db/migrate/migrations.go b/pkg/db/migrate/migrations.go index d134f2304..ad15b9319 100644 --- a/pkg/db/migrate/migrations.go +++ b/pkg/db/migrate/migrations.go @@ -18,6 +18,7 @@ var migrations = []Migration{ createTables, webhooks, migrateLfsObjects, + defaultRepoVisibility, } func execMigration(ctx context.Context, tx *db.Tx, version int, name string, down bool) error { diff --git a/pkg/ssh/cmd/create.go b/pkg/ssh/cmd/create.go index fa8ea3001..53789b57d 100644 --- a/pkg/ssh/cmd/create.go +++ b/pkg/ssh/cmd/create.go @@ -27,6 +27,9 @@ func createCommand() *cobra.Command { be := backend.FromContext(ctx) user := proto.UserFromContext(ctx) name := args[0] + if !cmd.Flags().Changed("private") { + private = be.IsDefaultRepoPrivate(ctx) + } r, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{ Private: private, Description: description, diff --git a/pkg/ssh/cmd/git.go b/pkg/ssh/cmd/git.go index caf1e1595..1e8a25195 100644 --- a/pkg/ssh/cmd/git.go +++ b/pkg/ssh/cmd/git.go @@ -238,7 +238,7 @@ func gitRunE(cmd *cobra.Command, args []string) error { return git.ErrNotAuthed } if repo == nil { - if _, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{Private: false}); err != nil { + if _, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{Private: be.IsDefaultRepoPrivate(ctx)}); err != nil { log.Errorf("failed to create repo: %s", err) return err } diff --git a/pkg/ssh/cmd/settings.go b/pkg/ssh/cmd/settings.go index cf0ffe9e0..a0132ceaf 100644 --- a/pkg/ssh/cmd/settings.go +++ b/pkg/ssh/cmd/settings.go @@ -69,5 +69,34 @@ func SettingsCommand() *cobra.Command { }, ) + vls := []string{"public", "private"} + cmd.AddCommand( + &cobra.Command{ + Use: "default-repo-visibility [public|private]", + Short: "Set or get the default visibility for new repositories", + Args: cobra.RangeArgs(0, 1), + ValidArgs: vls, + PersistentPreRunE: checkIfAdmin, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + be := backend.FromContext(ctx) + switch len(args) { + case 0: + cmd.Println(be.DefaultRepoVisibility(ctx)) + case 1: + v := args[0] + if v != "public" && v != "private" { + return fmt.Errorf("invalid visibility: %s. Please choose one of the following: %s", v, vls) + } + if err := be.SetDefaultRepoVisibility(ctx, v); err != nil { + return err + } + } + + return nil + }, + }, + ) + return cmd } diff --git a/pkg/store/database/settings.go b/pkg/store/database/settings.go index 8fd8294f8..d753397ca 100644 --- a/pkg/store/database/settings.go +++ b/pkg/store/database/settings.go @@ -45,3 +45,20 @@ func (*settingsStore) SetAnonAccess(ctx context.Context, tx db.Handler, level ac _, err := tx.ExecContext(ctx, query, level.String()) return db.WrapError(err) } + +// GetDefaultRepoVisibility implements store.SettingStore. +func (*settingsStore) GetDefaultRepoVisibility(ctx context.Context, tx db.Handler) (string, error) { + var visibility string + query := tx.Rebind(`SELECT value FROM settings WHERE "key" = 'default_repo_visibility'`) + if err := tx.GetContext(ctx, &visibility, query); err != nil { + return "public", db.WrapError(err) + } + return visibility, nil +} + +// SetDefaultRepoVisibility implements store.SettingStore. +func (*settingsStore) SetDefaultRepoVisibility(ctx context.Context, tx db.Handler, visibility string) error { + query := tx.Rebind(`UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE "key" = 'default_repo_visibility'`) + _, err := tx.ExecContext(ctx, query, visibility) + return db.WrapError(err) +} diff --git a/pkg/store/settings.go b/pkg/store/settings.go index f4833d02a..3d01c828c 100644 --- a/pkg/store/settings.go +++ b/pkg/store/settings.go @@ -13,4 +13,6 @@ type SettingStore interface { SetAnonAccess(ctx context.Context, h db.Handler, level access.AccessLevel) error GetAllowKeylessAccess(ctx context.Context, h db.Handler) (bool, error) SetAllowKeylessAccess(ctx context.Context, h db.Handler, allow bool) error + GetDefaultRepoVisibility(ctx context.Context, h db.Handler) (string, error) + SetDefaultRepoVisibility(ctx context.Context, h db.Handler, visibility string) error } diff --git a/pkg/web/git.go b/pkg/web/git.go index 6723ac6f0..592563dee 100644 --- a/pkg/web/git.go +++ b/pkg/web/git.go @@ -262,7 +262,7 @@ func withAccess(next http.Handler) http.HandlerFunc { // Create the repo if it doesn't exist. if repo == nil { - repo, err = be.CreateRepository(ctx, repoName, user, proto.RepositoryOptions{}) + repo, err = be.CreateRepository(ctx, repoName, user, proto.RepositoryOptions{Private: be.IsDefaultRepoPrivate(ctx)}) if err != nil { logger.Error("failed to create repository", "repo", repoName, "err", err) renderInternalServerError(w, r) diff --git a/testscript/testdata/settings.txtar b/testscript/testdata/settings.txtar index fc6bdf463..e3d91404d 100644 --- a/testscript/testdata/settings.txtar +++ b/testscript/testdata/settings.txtar @@ -41,6 +41,24 @@ stdout 'admin-access.*' ! stdout . stderr . +# check default repo visibility +soft settings default-repo-visibility +stdout 'public.*' + +# change default-repo-visibility and check +soft settings default-repo-visibility private +soft settings default-repo-visibility +stdout 'private.*' + +soft settings default-repo-visibility public +soft settings default-repo-visibility +stdout 'public.*' + +# try to set a bad visibility +! soft settings default-repo-visibility nope +! stdout . +stderr . + # stop the server [windows] stopserver