Skip to content
Open
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
41 changes: 41 additions & 0 deletions cmd/clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cmd

import (
"fmt"
"os"
"os/signal"

"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/supabase/cli/internal/clone"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
)

var (
cloneCmd = &cobra.Command{
GroupID: groupQuickStart,
Use: "clone",
Short: "Clones a Supabase project to your local environment",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
if !viper.IsSet("WORKDIR") {
title := fmt.Sprintf("Enter a directory to clone your project to (or leave blank to use %s): ", utils.Bold(utils.CurrentDirAbs))
if workdir, err := utils.NewConsole().PromptText(ctx, title); err != nil {
return err
} else {
viper.Set("WORKDIR", workdir)
}
}
return clone.Run(ctx, afero.NewOsFs())
},
}
)

func init() {
cloneFlags := cloneCmd.Flags()
cloneFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.")
rootCmd.AddCommand(cloneCmd)
}
106 changes: 106 additions & 0 deletions internal/clone/clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package clone

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/cenkalti/backoff/v4"
"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/spf13/viper"
"github.com/supabase/cli/internal/db/pull"
"github.com/supabase/cli/internal/link"
"github.com/supabase/cli/internal/login"
"github.com/supabase/cli/internal/projects/apiKeys"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/internal/utils/tenant"
"github.com/supabase/cli/pkg/api"
"golang.org/x/term"
)

func Run(ctx context.Context, fsys afero.Fs) error {
if err := changeWorkDir(ctx, fsys); err != nil {
return err
}
// 1. Login
if err := checkLogin(ctx, fsys); err != nil {
return err
}
// 2. Link project
if err := linkProject(ctx, fsys); err != nil {
return err
}
// 3. Pull migrations
dbConfig := flags.NewDbConfigWithPassword(ctx, flags.ProjectRef)
if err := dumpRemoteSchema(ctx, dbConfig, fsys); err != nil {
return err
}
return nil
}

func changeWorkDir(ctx context.Context, fsys afero.Fs) error {
workdir := viper.GetString("WORKDIR")
if !filepath.IsAbs(workdir) {
workdir = filepath.Join(utils.CurrentDirAbs, workdir)
}
if err := utils.MkdirIfNotExistFS(fsys, workdir); err != nil {
return err
}
if empty, err := afero.IsEmpty(fsys, workdir); err != nil {
return errors.Errorf("failed to read workdir: %w", err)
} else if !empty {
title := fmt.Sprintf("Do you want to overwrite existing files in %s directory?", utils.Bold(workdir))
if shouldOverwrite, err := utils.NewConsole().PromptYesNo(ctx, title, true); err != nil {
return err
} else if !shouldOverwrite {
return errors.New(context.Canceled)
}
}
return utils.ChangeWorkDir(fsys)
}

func checkLogin(ctx context.Context, fsys afero.Fs) error {
if _, err := utils.LoadAccessTokenFS(fsys); !errors.Is(err, utils.ErrMissingToken) {
return err
}
params := login.RunParams{
OpenBrowser: term.IsTerminal(int(os.Stdin.Fd())),
Fsys: fsys,
}
return login.Run(ctx, os.Stdout, params)
}

func linkProject(ctx context.Context, fsys afero.Fs) error {
// Use an empty fs to skip loading from file
if err := flags.ParseProjectRef(ctx, afero.NewMemMapFs()); err != nil {
return err
}
policy := utils.NewBackoffPolicy(ctx)
keys, err := backoff.RetryNotifyWithData(func() ([]api.ApiKeyResponse, error) {
fmt.Fprintln(os.Stderr, "Linking project...")
return apiKeys.RunGetApiKeys(ctx, flags.ProjectRef)
}, policy, utils.NewErrorCallback())
if err != nil {
return err
}
// Load default config to update docker id
if err := flags.LoadConfig(fsys); err != nil {
return err
}
link.LinkServices(ctx, flags.ProjectRef, tenant.NewApiKey(keys).ServiceRole, fsys)
return utils.WriteFile(utils.ProjectRefPath, []byte(flags.ProjectRef), fsys)
}

func dumpRemoteSchema(ctx context.Context, config pgconn.Config, fsys afero.Fs) error {
schemaPath := filepath.Join(utils.SchemasDir, "remote.sql")
utils.Config.Db.Migrations.SchemaPaths = append(utils.Config.Db.Migrations.SchemaPaths, filepath.ToSlash(schemaPath))
if err := pull.CloneRemoteSchema(ctx, schemaPath, config, fsys); err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Schema written to "+utils.Bold(schemaPath))
return nil
}
16 changes: 15 additions & 1 deletion internal/db/diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,21 @@ func MigrateShadowDatabase(ctx context.Context, container string, fsys afero.Fs,
if _, err := conn.Exec(ctx, CREATE_TEMPLATE); err != nil {
return errors.Errorf("failed to create template database: %w", err)
}
return migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys))
// Migrations take precedence over declarative schemas
if len(migrations) > 0 {
return migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys))
}
declared, err := loadDeclaredSchemas(fsys)
if err != nil || len(declared) == 0 {
return err
}
fmt.Fprintln(os.Stderr, "Creating local database from declarative schemas:")
msg := make([]string, len(declared))
for i, m := range declared {
msg[i] = fmt.Sprintf(" • %s", utils.Bold(m))
}
fmt.Fprintln(os.Stderr, strings.Join(msg, "\n"))
return migration.SeedGlobals(ctx, declared, conn, afero.NewIOFS(fsys))
}

func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w io.Writer, fsys afero.Fs, differ DiffFunc, options ...func(*pgx.ConnConfig)) (string, error) {
Expand Down
22 changes: 13 additions & 9 deletions internal/db/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,7 @@ func run(ctx context.Context, schema []string, path string, conn *pgx.Conn, fsys
config := conn.Config().Config
// 1. Assert `supabase/migrations` and `schema_migrations` are in sync.
if err := assertRemoteInSync(ctx, conn, fsys); errors.Is(err, errMissing) {
// Ignore schemas flag when working on the initial pull
if err = dumpRemoteSchema(ctx, path, config, fsys); err != nil {
return err
}
// Pull changes in managed schemas automatically
if err = diffRemoteSchema(ctx, managedSchemas, path, config, fsys); errors.Is(err, errInSync) {
err = nil
}
return err
return CloneRemoteSchema(ctx, path, config, fsys)
} else if err != nil {
return err
}
Expand All @@ -82,6 +74,18 @@ func run(ctx context.Context, schema []string, path string, conn *pgx.Conn, fsys
return diffUserSchemas(ctx, schema, path, config, fsys)
}

func CloneRemoteSchema(ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error {
// Ignore schemas flag when working on the initial pull
if err := dumpRemoteSchema(ctx, path, config, fsys); err != nil {
return err
}
// Pull changes in managed schemas automatically
if err := diffRemoteSchema(ctx, managedSchemas, path, config, fsys); err != nil && !errors.Is(err, errInSync) {
return err
}
return nil
}

func dumpRemoteSchema(ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error {
// Special case if this is the first migration
fmt.Fprintln(os.Stderr, "Dumping schema from remote database...")
Expand Down
1 change: 1 addition & 0 deletions pkg/migration/scripts/dump_schema.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pg_dump \
| sed -E "s/^REVOKE (.+) ON (.+) \"(${EXCLUDED_SCHEMAS:-})\"/-- &/" \
| sed -E 's/^(CREATE EXTENSION IF NOT EXISTS "pg_tle").+/\1;/' \
| sed -E 's/^(CREATE EXTENSION IF NOT EXISTS "pgsodium").+/\1;/' \
| sed -E 's/^(CREATE EXTENSION IF NOT EXISTS "pgmq").+/\1;/' \
| sed -E 's/^COMMENT ON EXTENSION (.+)/-- &/' \
| sed -E 's/^CREATE POLICY "cron_job_/-- &/' \
| sed -E 's/^ALTER TABLE "cron"/-- &/' \
Expand Down