Skip to content

Commit b926a8c

Browse files
authored
[pull] Implement pull <git> (#1061)
## Summary This implements `devbox global pull <git>` and offers familiar `--force` flag to overwrite conflicts. ### Existing functionality change: It also changes the behavior of existing `pull <url>` and `pull <local-path>` to also warn and overwrite on `--force` instead of merging. We do this to standardize behavior. ## How was it tested? ``` devbox global pull git@github.com:mikeland86/global.git devbox global pull https://fleekgen.fly.dev/high --force devbox global pull devbox.json --force devbox global pull https://raw.githubusercontent.com/mikeland86/global/main/devbox.json\?token=<token> ```
1 parent 5748d0c commit b926a8c

File tree

9 files changed

+279
-155
lines changed

9 files changed

+279
-155
lines changed

devbox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type Devbox interface {
3434
PrintEnv(ctx context.Context, includeHooks bool) (string, error)
3535
PrintGlobalList() error
3636
PrintEnvrcContent(w io.Writer) error
37-
PullGlobal(ctx context.Context, overwrite bool, path string) error
37+
Pull(ctx context.Context, overwrite bool, path string) error
3838
// Remove removes Nix packages from the config so that it no longer exists in
3939
// the devbox environment.
4040
Remove(ctx context.Context, pkgs ...string) error

internal/boxcli/global.go

Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,13 @@ package boxcli
55

66
import (
77
"fmt"
8-
"io/fs"
98

10-
"github.com/AlecAivazis/survey/v2"
119
"github.com/pkg/errors"
1210
"github.com/spf13/cobra"
1311
"go.jetpack.io/devbox"
1412
"go.jetpack.io/devbox/internal/ux"
1513
)
1614

17-
type globalPullCmdFlags struct {
18-
force bool
19-
}
20-
2115
func globalCmd() *cobra.Command {
2216

2317
globalCmd := &cobra.Command{}
@@ -31,6 +25,7 @@ func globalCmd() *cobra.Command {
3125

3226
addCommandAndHideConfigFlag(globalCmd, addCmd())
3327
addCommandAndHideConfigFlag(globalCmd, installCmd())
28+
addCommandAndHideConfigFlag(globalCmd, pullCmd())
3429
addCommandAndHideConfigFlag(globalCmd, removeCmd())
3530
addCommandAndHideConfigFlag(globalCmd, runCmd())
3631
addCommandAndHideConfigFlag(globalCmd, servicesCmd())
@@ -39,7 +34,6 @@ func globalCmd() *cobra.Command {
3934

4035
// Create list for non-global? Mike: I want it :)
4136
globalCmd.AddCommand(globalListCmd())
42-
globalCmd.AddCommand(globalPullCmd())
4337

4438
return globalCmd
4539
}
@@ -59,27 +53,6 @@ func globalListCmd() *cobra.Command {
5953
}
6054
}
6155

62-
func globalPullCmd() *cobra.Command {
63-
flags := globalPullCmdFlags{}
64-
cmd := &cobra.Command{
65-
Use: "pull <file> | <url>",
66-
Short: "Pull a global config from a file or URL",
67-
Long: "Pull a global config from a file or URL. URLs must be prefixed with 'http://' or 'https://'.",
68-
PreRunE: ensureNixInstalled,
69-
RunE: func(cmd *cobra.Command, args []string) error {
70-
return pullGlobalCmdFunc(cmd, args, flags.force)
71-
},
72-
Args: cobra.ExactArgs(1),
73-
}
74-
75-
cmd.Flags().BoolVarP(
76-
&flags.force, "force", "f", false,
77-
"Force overwrite of existing global config files",
78-
)
79-
80-
return cmd
81-
}
82-
8356
func listGlobalCmdFunc(cmd *cobra.Command, args []string) error {
8457
path, err := ensureGlobalConfig(cmd)
8558
if err != nil {
@@ -93,40 +66,6 @@ func listGlobalCmdFunc(cmd *cobra.Command, args []string) error {
9366
return box.PrintGlobalList()
9467
}
9568

96-
func pullGlobalCmdFunc(
97-
cmd *cobra.Command,
98-
args []string,
99-
overwrite bool,
100-
) error {
101-
path, err := ensureGlobalConfig(cmd)
102-
if err != nil {
103-
return errors.WithStack(err)
104-
}
105-
106-
box, err := devbox.Open(path, cmd.ErrOrStderr())
107-
if err != nil {
108-
return errors.WithStack(err)
109-
}
110-
err = box.PullGlobal(cmd.Context(), overwrite, args[0])
111-
if errors.Is(err, fs.ErrExist) {
112-
prompt := &survey.Confirm{
113-
Message: "File(s) already exists. Overwrite?",
114-
}
115-
if err = survey.AskOne(prompt, &overwrite); err != nil {
116-
return errors.WithStack(err)
117-
}
118-
if !overwrite {
119-
return nil
120-
}
121-
err = box.PullGlobal(cmd.Context(), overwrite, args[0])
122-
}
123-
if err != nil {
124-
return err
125-
}
126-
127-
return installCmdFunc(cmd, runCmdFlags{config: configFlags{path: path}})
128-
}
129-
13069
var globalConfigPath string
13170

13271
func ensureGlobalConfig(cmd *cobra.Command) (string, error) {

internal/boxcli/pull.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package boxcli
5+
6+
import (
7+
"io/fs"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/AlecAivazis/survey/v2"
12+
"github.com/pkg/errors"
13+
"github.com/spf13/cobra"
14+
"go.jetpack.io/devbox"
15+
)
16+
17+
type pullCmdFlags struct {
18+
config configFlags
19+
force bool
20+
}
21+
22+
func pullCmd() *cobra.Command {
23+
flags := pullCmdFlags{}
24+
cmd := &cobra.Command{
25+
Use: "pull <file> | <url>",
26+
Short: "Pull a config from a file or URL",
27+
Long: "Pull a config from a file or URL. URLs must be prefixed with 'http://' or 'https://'.",
28+
PreRunE: ensureNixInstalled,
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
return pullCmdFunc(cmd, args[0], &flags)
31+
},
32+
Args: cobra.ExactArgs(1),
33+
}
34+
35+
cmd.Flags().BoolVarP(
36+
&flags.force, "force", "f", false,
37+
"Force overwrite of existing [global] config files",
38+
)
39+
40+
flags.config.register(cmd)
41+
42+
return cmd
43+
}
44+
45+
func pullCmdFunc(
46+
cmd *cobra.Command,
47+
url string,
48+
flags *pullCmdFlags,
49+
) error {
50+
box, err := devbox.Open(flags.config.path, cmd.ErrOrStderr())
51+
if err != nil {
52+
return errors.WithStack(err)
53+
}
54+
55+
pullPath, err := absolutizeIfLocal(url)
56+
if err != nil {
57+
return errors.WithStack(err)
58+
}
59+
60+
err = box.Pull(cmd.Context(), flags.force, pullPath)
61+
if prompt := pullErrorPrompt(err); prompt != "" {
62+
prompt := &survey.Confirm{Message: prompt}
63+
if err = survey.AskOne(prompt, &flags.force); err != nil {
64+
return errors.WithStack(err)
65+
}
66+
if !flags.force {
67+
return nil
68+
}
69+
err = box.Pull(cmd.Context(), flags.force, pullPath)
70+
}
71+
if err != nil {
72+
return err
73+
}
74+
75+
return installCmdFunc(
76+
cmd,
77+
runCmdFlags{config: configFlags{path: flags.config.path}},
78+
)
79+
}
80+
81+
func pullErrorPrompt(err error) string {
82+
switch {
83+
case errors.Is(err, fs.ErrExist):
84+
return "Global profile already exists. Overwrite?"
85+
default:
86+
return ""
87+
}
88+
}
89+
90+
func absolutizeIfLocal(path string) (string, error) {
91+
if _, err := os.Stat(path); err == nil {
92+
return filepath.Abs(path)
93+
}
94+
return path, nil
95+
}

internal/devconfig/config.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package devconfig
66
import (
77
"io"
88
"net/http"
9-
"net/url"
109
"path/filepath"
1110
"regexp"
1211
"strings"
@@ -127,8 +126,8 @@ func Load(path string) (*Config, error) {
127126
return cfg, validateConfig(cfg)
128127
}
129128

130-
func LoadConfigFromURL(url *url.URL) (*Config, error) {
131-
res, err := http.Get(url.String())
129+
func LoadConfigFromURL(url string) (*Config, error) {
130+
res, err := http.Get(url)
132131
if err != nil {
133132
return nil, errors.WithStack(err)
134133
}
@@ -138,7 +137,7 @@ func LoadConfigFromURL(url *url.URL) (*Config, error) {
138137
if err != nil {
139138
return nil, errors.WithStack(err)
140139
}
141-
ext := filepath.Ext(url.Path)
140+
ext := filepath.Ext(url)
142141
if !cuecfg.IsSupportedExtension(ext) {
143142
ext = ".json"
144143
}

internal/impl/global.go

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,25 @@ import (
77
"context"
88
"fmt"
99
"io/fs"
10-
"net/url"
1110
"os"
1211
"path/filepath"
13-
"strings"
1412

1513
"github.com/pkg/errors"
16-
"github.com/samber/lo"
1714

18-
"go.jetpack.io/devbox/internal/devconfig"
1915
"go.jetpack.io/devbox/internal/pullbox"
2016
"go.jetpack.io/devbox/internal/xdg"
2117
)
2218

2319
// In the future we will support multiple global profiles
2420
const currentGlobalProfile = "default"
2521

26-
func (d *Devbox) PullGlobal(
22+
func (d *Devbox) Pull(
2723
ctx context.Context,
2824
force bool,
2925
path string,
3026
) error {
31-
u, err := url.Parse(path)
32-
if err == nil && u.Scheme != "" {
33-
return d.pullGlobalFromURL(ctx, force, u)
34-
}
35-
return d.pullGlobalFromPath(ctx, path)
27+
fmt.Fprintf(d.writer, "Pulling global config from %s\n", path)
28+
return pullbox.New(d, path, force).Pull()
3629
}
3730

3831
func (d *Devbox) PrintGlobalList() error {
@@ -42,58 +35,6 @@ func (d *Devbox) PrintGlobalList() error {
4235
return nil
4336
}
4437

45-
func (d *Devbox) pullGlobalFromURL(
46-
ctx context.Context,
47-
overwrite bool,
48-
configURL *url.URL,
49-
) error {
50-
fmt.Fprintf(d.writer, "Pulling global config from %s\n", configURL)
51-
puller := pullbox.New()
52-
if ok, err := puller.URLIsArchive(configURL.String()); ok {
53-
fmt.Fprintf(
54-
d.writer,
55-
"%s is an archive, extracting to %s\n",
56-
configURL,
57-
d.ProjectDir(),
58-
)
59-
return puller.DownloadAndExtract(
60-
overwrite,
61-
configURL.String(),
62-
d.projectDir,
63-
)
64-
} else if err != nil {
65-
return err
66-
}
67-
cfg, err := devconfig.LoadConfigFromURL(configURL)
68-
if err != nil {
69-
return err
70-
}
71-
return d.addFromPull(ctx, cfg)
72-
}
73-
74-
func (d *Devbox) pullGlobalFromPath(ctx context.Context, path string) error {
75-
fmt.Fprintf(d.writer, "Pulling global config from %s\n", path)
76-
cfg, err := devconfig.Load(path)
77-
if err != nil {
78-
return err
79-
}
80-
return d.addFromPull(ctx, cfg)
81-
}
82-
83-
func (d *Devbox) addFromPull(ctx context.Context, cfg *devconfig.Config) error {
84-
diff, _ := lo.Difference(cfg.Packages, d.cfg.Packages)
85-
if len(diff) == 0 {
86-
fmt.Fprint(d.writer, "No new packages to install\n")
87-
return nil
88-
}
89-
fmt.Fprintf(
90-
d.writer,
91-
"Installing the following packages: %s\n",
92-
strings.Join(diff, ", "),
93-
)
94-
return d.Add(ctx, diff...)
95-
}
96-
9738
func GlobalDataPath() (string, error) {
9839
path := xdg.DataSubpath(filepath.Join("devbox/global", currentGlobalProfile))
9940
if err := os.MkdirAll(path, 0755); err != nil {

internal/pullbox/config.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package pullbox
5+
6+
import (
7+
"net/url"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/pkg/errors"
12+
"go.jetpack.io/devbox/internal/cuecfg"
13+
"go.jetpack.io/devbox/internal/devconfig"
14+
)
15+
16+
func (p *pullbox) IsTextDevboxConfig() bool {
17+
if u, err := url.Parse(p.url); err == nil {
18+
ext := filepath.Ext(u.Path)
19+
return cuecfg.IsSupportedExtension(ext)
20+
}
21+
// For invalid URLS, just look at the extension
22+
ext := filepath.Ext(p.url)
23+
return cuecfg.IsSupportedExtension(ext)
24+
}
25+
26+
func (p *pullbox) pullTextDevboxConfig() error {
27+
if p.isLocalConfig() {
28+
return p.copy(p.overwrite, p.url, p.ProjectDir())
29+
}
30+
31+
cfg, err := devconfig.LoadConfigFromURL(p.url)
32+
if err != nil {
33+
return err
34+
}
35+
36+
tmpDir, err := os.MkdirTemp("", "devbox")
37+
if err != nil {
38+
return errors.WithStack(err)
39+
}
40+
if err = cfg.SaveTo(tmpDir); err != nil {
41+
return err
42+
}
43+
44+
return p.copy(p.overwrite, tmpDir, p.ProjectDir())
45+
}
46+
47+
func (p *pullbox) isLocalConfig() bool {
48+
_, err := os.Stat(p.url)
49+
return err == nil
50+
}

0 commit comments

Comments
 (0)