1
1
package cli
2
2
3
3
import (
4
+ "errors"
4
5
"fmt"
5
6
"io/fs"
6
7
"os"
7
8
"os/exec"
8
9
"path/filepath"
9
10
"strings"
11
+ "time"
10
12
11
13
"github.com/spf13/cobra"
12
14
"golang.org/x/xerrors"
@@ -17,7 +19,7 @@ import (
17
19
18
20
func dotfiles () * cobra.Command {
19
21
var (
20
- homeDir string
22
+ symlinkDir string
21
23
)
22
24
cmd := & cobra.Command {
23
25
Use : "dotfiles [git_repo_url]" ,
@@ -28,11 +30,9 @@ func dotfiles() *cobra.Command {
28
30
var (
29
31
dotfilesRepoDir = "dotfiles"
30
32
gitRepo = args [0 ]
31
- cfgDir = string (createConfig (cmd ))
33
+ cfg = createConfig (cmd )
34
+ cfgDir = string (cfg )
32
35
dotfilesDir = filepath .Join (cfgDir , dotfilesRepoDir )
33
- subcommands = []string {"clone" , args [0 ], dotfilesRepoDir }
34
- gitCmdDir = cfgDir
35
- promptText = fmt .Sprintf ("Cloning %s into directory %s.\n Continue?" , gitRepo , dotfilesDir )
36
36
// This follows the same pattern outlined by others in the market:
37
37
// https://github.com/coder/coder/pull/1696#issue-1245742312
38
38
installScriptSet = []string {
@@ -53,14 +53,49 @@ func dotfiles() *cobra.Command {
53
53
return xerrors .Errorf ("checking dir %s: %w" , dotfilesDir , err )
54
54
}
55
55
56
- // if repo exists already do a git pull instead of clone
56
+ moved := false
57
+ if dotfilesExists {
58
+ du , err := cfg .DotfilesURL ().Read ()
59
+ if err != nil {
60
+ return xerrors .Errorf ("reading dotfiles url config: %w" , err )
61
+ }
62
+ // if the git url has changed we create a backup and clone fresh
63
+ if gitRepo != du {
64
+ backupDir := fmt .Sprintf ("%s_backup_%s" , dotfilesDir , time .Now ().Format (time .RFC3339 ))
65
+ _ , err = cliui .Prompt (cmd , cliui.PromptOptions {
66
+ Text : fmt .Sprintf ("The dotfiles URL has changed from %s to %s. Coder will backup the existing repo to %s.\n \n Continue?" , du , gitRepo , backupDir ),
67
+ IsConfirm : true ,
68
+ })
69
+ if err != nil {
70
+ return err
71
+ }
72
+
73
+ err = os .Rename (dotfilesDir , backupDir )
74
+ if err != nil {
75
+ return xerrors .Errorf ("renaming dir %s: %w" , dotfilesDir , err )
76
+ }
77
+ dotfilesExists = false
78
+ moved = true
79
+ }
80
+ }
81
+
82
+ var (
83
+ gitCmdDir string
84
+ subcommands []string
85
+ promptText string
86
+ )
57
87
if dotfilesExists {
58
88
_ , _ = fmt .Fprintf (cmd .OutOrStdout (), "Found dotfiles repository at %s\n " , dotfilesDir )
59
89
gitCmdDir = dotfilesDir
60
90
subcommands = []string {"pull" , "--ff-only" }
61
91
promptText = fmt .Sprintf ("Pulling latest from %s into directory %s.\n Continue?" , gitRepo , dotfilesDir )
62
92
} else {
63
- _ , _ = fmt .Fprintf (cmd .OutOrStdout (), "Did not find dotfiles repository at %s\n " , dotfilesDir )
93
+ if ! moved {
94
+ _ , _ = fmt .Fprintf (cmd .OutOrStdout (), "Did not find dotfiles repository at %s\n " , dotfilesDir )
95
+ }
96
+ gitCmdDir = cfgDir
97
+ subcommands = []string {"clone" , args [0 ], dotfilesRepoDir }
98
+ promptText = fmt .Sprintf ("Cloning %s into directory %s.\n Continue?" , gitRepo , dotfilesDir )
64
99
}
65
100
66
101
_ , err = cliui .Prompt (cmd , cliui.PromptOptions {
@@ -87,12 +122,16 @@ func dotfiles() *cobra.Command {
87
122
c := exec .CommandContext (cmd .Context (), "git" , subcommands ... )
88
123
c .Dir = gitCmdDir
89
124
c .Env = append (os .Environ (), fmt .Sprintf (`GIT_SSH_COMMAND=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no` , gitsshCmd ))
90
- out , err := c .CombinedOutput ()
125
+ c .Stdout = cmd .OutOrStdout ()
126
+ c .Stderr = cmd .ErrOrStderr ()
127
+ err = c .Run ()
91
128
if err != nil {
92
- _ , _ = fmt .Fprintln (cmd .OutOrStdout (), cliui .Styles .Error .Render (string (out )))
93
- return err
129
+ if ! dotfilesExists {
130
+ return err
131
+ }
132
+ // if the repo exists we soft fail the update operation and try to continue
133
+ _ , _ = fmt .Fprintln (cmd .OutOrStdout (), cliui .Styles .Error .Render ("Failed to update repo, continuing..." ))
94
134
}
95
- _ , _ = fmt .Fprintln (cmd .OutOrStdout (), string (out ))
96
135
97
136
files , err := os .ReadDir (dotfilesDir )
98
137
if err != nil {
@@ -122,24 +161,29 @@ func dotfiles() *cobra.Command {
122
161
return err
123
162
}
124
163
125
- if homeDir == "" {
126
- homeDir , err = os .UserHomeDir ()
164
+ if symlinkDir == "" {
165
+ symlinkDir , err = os .UserHomeDir ()
127
166
if err != nil {
128
167
return xerrors .Errorf ("getting user home: %w" , err )
129
168
}
130
169
}
131
170
132
171
for _ , df := range dotfiles {
133
172
from := filepath .Join (dotfilesDir , df )
134
- to := filepath .Join (homeDir , df )
173
+ to := filepath .Join (symlinkDir , df )
135
174
_ , _ = fmt .Fprintf (cmd .OutOrStdout (), "Symlinking %s to %s...\n " , from , to )
136
- // if file already exists at destination remove it
137
- // this behavior matches `ln -f`
138
- _ , err := os .Lstat (to )
139
- if err == nil {
140
- err := os .Remove (to )
175
+
176
+ isRegular , err := isRegular (to )
177
+ if err != nil {
178
+ return xerrors .Errorf ("checking symlink for %s: %w" , to , err )
179
+ }
180
+ // move conflicting non-symlink files to file.ext.bak
181
+ if isRegular {
182
+ backup := fmt .Sprintf ("%s.bak" , to )
183
+ _ , _ = fmt .Fprintf (cmd .OutOrStdout (), "Moving %s to %s...\n " , to , backup )
184
+ err = os .Rename (to , backup )
141
185
if err != nil {
142
- return xerrors .Errorf ("removing destination file %s: %w" , to , err )
186
+ return xerrors .Errorf ("renaming dir %s: %w" , to , err )
143
187
}
144
188
}
145
189
@@ -167,33 +211,36 @@ func dotfiles() *cobra.Command {
167
211
// nolint:gosec
168
212
scriptCmd := exec .CommandContext (cmd .Context (), fmt .Sprintf ("./%s" , script ))
169
213
scriptCmd .Dir = dotfilesDir
170
- out , err = scriptCmd .CombinedOutput ()
214
+ scriptCmd .Stdout = cmd .OutOrStdout ()
215
+ scriptCmd .Stderr = cmd .ErrOrStderr ()
216
+ err = scriptCmd .Run ()
171
217
if err != nil {
172
- _ , _ = fmt .Fprintln (cmd .OutOrStdout (), cliui .Styles .Error .Render (string (out )))
173
218
return xerrors .Errorf ("running %s: %w" , script , err )
174
219
}
175
- _ , _ = fmt .Fprintln (cmd .OutOrStdout (), string (out ))
176
220
177
221
_ , _ = fmt .Fprintln (cmd .OutOrStdout (), "Dotfiles installation complete." )
178
222
return nil
179
223
},
180
224
}
181
225
cliui .AllowSkipPrompt (cmd )
182
- cliflag .StringVarP (cmd .Flags (), & homeDir , "home -dir" , "" , "CODER_HOME_DIR " , "" , "Specifies the home directory for the dotfiles symlink destination . If empty will use $HOME." )
226
+ cliflag .StringVarP (cmd .Flags (), & symlinkDir , "symlink -dir" , "" , "CODER_SYMLINK_DIR " , "" , "Specifies the directory for the dotfiles symlink destinations . If empty will use $HOME." )
183
227
184
228
return cmd
185
229
}
186
230
187
231
// dirExists checks if the dir already exists.
188
232
func dirExists (name string ) (bool , error ) {
189
- _ , err := os .Stat (name )
233
+ fi , err := os .Stat (name )
190
234
if err != nil {
191
235
if os .IsNotExist (err ) {
192
236
return false , nil
193
237
}
194
238
195
239
return false , xerrors .Errorf ("stat dir: %w" , err )
196
240
}
241
+ if ! fi .IsDir () {
242
+ return false , xerrors .New ("exists but not a directory" )
243
+ }
197
244
198
245
return true , nil
199
246
}
@@ -210,3 +257,15 @@ func findScript(scriptSet []string, files []fs.DirEntry) string {
210
257
211
258
return ""
212
259
}
260
+
261
+ func isRegular (to string ) (bool , error ) {
262
+ fi , err := os .Lstat (to )
263
+ if err != nil {
264
+ if errors .Is (err , os .ErrNotExist ) {
265
+ return false , nil
266
+ }
267
+ return false , xerrors .Errorf ("lstat %s: %w" , to , err )
268
+ }
269
+
270
+ return fi .Mode ().IsRegular (), nil
271
+ }
0 commit comments