Skip to content

Commit 34ae74c

Browse files
committed
feat: add command to clone project to local
1 parent 01f6d1c commit 34ae74c

File tree

4 files changed

+176
-10
lines changed

4 files changed

+176
-10
lines changed

cmd/clone.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/signal"
7+
8+
"github.com/spf13/afero"
9+
"github.com/spf13/cobra"
10+
"github.com/spf13/viper"
11+
"github.com/supabase/cli/internal/clone"
12+
"github.com/supabase/cli/internal/utils"
13+
"github.com/supabase/cli/internal/utils/flags"
14+
)
15+
16+
var (
17+
cloneCmd = &cobra.Command{
18+
GroupID: groupQuickStart,
19+
Use: "clone",
20+
Short: "Clones a Supabase project to your local environment",
21+
Args: cobra.NoArgs,
22+
RunE: func(cmd *cobra.Command, args []string) error {
23+
ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
24+
if !viper.IsSet("WORKDIR") {
25+
title := fmt.Sprintf("Enter a directory to clone your project to (or leave blank to use %s): ", utils.Bold(utils.CurrentDirAbs))
26+
if workdir, err := utils.NewConsole().PromptText(ctx, title); err != nil {
27+
return err
28+
} else {
29+
viper.Set("WORKDIR", workdir)
30+
}
31+
}
32+
return clone.Run(ctx, afero.NewOsFs())
33+
},
34+
}
35+
)
36+
37+
func init() {
38+
cloneFlags := cloneCmd.Flags()
39+
cloneFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.")
40+
rootCmd.AddCommand(cloneCmd)
41+
}

internal/clone/clone.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package clone
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/cenkalti/backoff/v4"
10+
"github.com/go-errors/errors"
11+
"github.com/jackc/pgconn"
12+
"github.com/spf13/afero"
13+
"github.com/spf13/viper"
14+
"github.com/supabase/cli/internal/db/pull"
15+
"github.com/supabase/cli/internal/link"
16+
"github.com/supabase/cli/internal/login"
17+
"github.com/supabase/cli/internal/projects/apiKeys"
18+
"github.com/supabase/cli/internal/utils"
19+
"github.com/supabase/cli/internal/utils/flags"
20+
"github.com/supabase/cli/internal/utils/tenant"
21+
"github.com/supabase/cli/pkg/api"
22+
"golang.org/x/term"
23+
)
24+
25+
func Run(ctx context.Context, fsys afero.Fs) error {
26+
if err := changeWorkDir(ctx, fsys); err != nil {
27+
return err
28+
}
29+
// 1. Login
30+
if err := checkLogin(ctx, fsys); err != nil {
31+
return err
32+
}
33+
// 2. Link project
34+
if err := linkProject(ctx, fsys); err != nil {
35+
return err
36+
}
37+
// 3. Pull migrations
38+
dbConfig := flags.NewDbConfigWithPassword(ctx, flags.ProjectRef)
39+
if err := dumpRemoteSchema(ctx, dbConfig, fsys); err != nil {
40+
return err
41+
}
42+
return nil
43+
}
44+
45+
func changeWorkDir(ctx context.Context, fsys afero.Fs) error {
46+
workdir := viper.GetString("WORKDIR")
47+
if !filepath.IsAbs(workdir) {
48+
workdir = filepath.Join(utils.CurrentDirAbs, workdir)
49+
}
50+
if err := utils.MkdirIfNotExistFS(fsys, workdir); err != nil {
51+
return err
52+
}
53+
if empty, err := afero.IsEmpty(fsys, workdir); err != nil {
54+
return errors.Errorf("failed to read workdir: %w", err)
55+
} else if !empty {
56+
title := fmt.Sprintf("Do you want to overwrite existing files in %s directory?", utils.Bold(workdir))
57+
if shouldOverwrite, err := utils.NewConsole().PromptYesNo(ctx, title, true); err != nil {
58+
return err
59+
} else if !shouldOverwrite {
60+
return errors.New(context.Canceled)
61+
}
62+
}
63+
return utils.ChangeWorkDir(fsys)
64+
}
65+
66+
func checkLogin(ctx context.Context, fsys afero.Fs) error {
67+
if _, err := utils.LoadAccessTokenFS(fsys); !errors.Is(err, utils.ErrMissingToken) {
68+
return err
69+
}
70+
params := login.RunParams{
71+
OpenBrowser: term.IsTerminal(int(os.Stdin.Fd())),
72+
Fsys: fsys,
73+
}
74+
return login.Run(ctx, os.Stdout, params)
75+
}
76+
77+
func linkProject(ctx context.Context, fsys afero.Fs) error {
78+
// Use an empty fs to skip loading from file
79+
if err := flags.ParseProjectRef(ctx, afero.NewMemMapFs()); err != nil {
80+
return err
81+
}
82+
policy := utils.NewBackoffPolicy(ctx)
83+
keys, err := backoff.RetryNotifyWithData(func() ([]api.ApiKeyResponse, error) {
84+
fmt.Fprintln(os.Stderr, "Linking project...")
85+
return apiKeys.RunGetApiKeys(ctx, flags.ProjectRef)
86+
}, policy, utils.NewErrorCallback())
87+
if err != nil {
88+
return err
89+
}
90+
// Load default config to update docker id
91+
if err := flags.LoadConfig(fsys); err != nil {
92+
return err
93+
}
94+
link.LinkServices(ctx, flags.ProjectRef, tenant.NewApiKey(keys).ServiceRole, false, fsys)
95+
return utils.WriteFile(utils.ProjectRefPath, []byte(flags.ProjectRef), fsys)
96+
}
97+
98+
func dumpRemoteSchema(ctx context.Context, config pgconn.Config, fsys afero.Fs) error {
99+
schemaPath := filepath.Join(utils.SchemasDir, "remote.sql")
100+
utils.Config.Db.Migrations.SchemaPaths = append(utils.Config.Db.Migrations.SchemaPaths, filepath.ToSlash(schemaPath))
101+
if err := pull.CloneRemoteSchema(ctx, schemaPath, config, fsys); err != nil {
102+
return err
103+
}
104+
fmt.Fprintln(os.Stderr, "Schema written to "+utils.Bold(schemaPath))
105+
return nil
106+
}

internal/db/diff/diff.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,21 @@ func MigrateShadowDatabase(ctx context.Context, container string, fsys afero.Fs,
133133
if _, err := conn.Exec(ctx, CREATE_TEMPLATE); err != nil {
134134
return errors.Errorf("failed to create template database: %w", err)
135135
}
136-
return migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys))
136+
// Migrations take precedence over declarative schemas
137+
if len(migrations) > 0 {
138+
return migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys))
139+
}
140+
declared, err := loadDeclaredSchemas(fsys)
141+
if err != nil || len(declared) == 0 {
142+
return err
143+
}
144+
fmt.Fprintln(os.Stderr, "Creating local database from declarative schemas:")
145+
msg := make([]string, len(declared))
146+
for i, m := range declared {
147+
msg[i] = fmt.Sprintf(" • %s", utils.Bold(m))
148+
}
149+
fmt.Fprintln(os.Stderr, strings.Join(msg, "\n"))
150+
return migration.SeedGlobals(ctx, declared, conn, afero.NewIOFS(fsys))
137151
}
138152

139153
func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w io.Writer, fsys afero.Fs, differ DiffFunc, options ...func(*pgx.ConnConfig)) (string, error) {

internal/db/pull/pull.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,27 @@ func run(ctx context.Context, schema []string, path string, conn *pgx.Conn, fsys
5656
config := conn.Config().Config
5757
// 1. Assert `supabase/migrations` and `schema_migrations` are in sync.
5858
if err := assertRemoteInSync(ctx, conn, fsys); errors.Is(err, errMissing) {
59-
// Ignore schemas flag when working on the initial pull
60-
if err = dumpRemoteSchema(ctx, path, config, fsys); err != nil {
61-
return err
62-
}
63-
// Run a second pass to pull in changes from default privileges and managed schemas
64-
if err = diffRemoteSchema(ctx, nil, path, config, fsys); errors.Is(err, errInSync) {
65-
err = nil
66-
}
67-
return err
59+
return CloneRemoteSchema(ctx, path, config, fsys)
6860
} else if err != nil {
6961
return err
7062
}
7163
// 2. Fetch remote schema changes
7264
return diffRemoteSchema(ctx, schema, path, config, fsys)
7365
}
7466

67+
func CloneRemoteSchema(ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error {
68+
// Ignore schemas flag when working on the initial pull
69+
if err := dumpRemoteSchema(ctx, path, config, fsys); err != nil {
70+
return err
71+
}
72+
// Run a second pass to pull in changes from default privileges and managed schemas
73+
err := diffRemoteSchema(ctx, nil, path, config, fsys)
74+
if errors.Is(err, errInSync) {
75+
err = nil
76+
}
77+
return err
78+
}
79+
7580
func dumpRemoteSchema(ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error {
7681
// Special case if this is the first migration
7782
fmt.Fprintln(os.Stderr, "Dumping schema from remote database...")

0 commit comments

Comments
 (0)