Skip to content

feat: add dotfiles command #1723

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
May 25, 2022
Merged
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
pr comments
  • Loading branch information
f0ssel committed May 24, 2022
commit 8cf1c4f7bff2397b693ef60cb60ce890840bc5d1
111 changes: 59 additions & 52 deletions cli/dotfiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
Expand All @@ -10,14 +11,19 @@ import (
"github.com/spf13/cobra"
"golang.org/x/xerrors"

"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
)

func dotfiles() *cobra.Command {
var (
homeDir string
)
cmd := &cobra.Command{
Use: "dotfiles [git_repo_url]",
Args: cobra.ExactArgs(1),
Short: "Checkout and install a dotfiles repository.",
Use: "dotfiles [git_repo_url]",
Args: cobra.ExactArgs(1),
Short: "Checkout and install a dotfiles repository.",
Example: "coder dotfiles [-y] git@github.com:example/dotfiles.git",
Comment on lines +27 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should improve the usage here. It will not be immediately obvious to users how we intend for them to use this (inside or outside of a workspace).

A few examples will go a long way!

RunE: func(cmd *cobra.Command, args []string) error {
var (
dotfilesRepoDir = "dotfiles"
Expand All @@ -26,7 +32,7 @@ func dotfiles() *cobra.Command {
dotfilesDir = filepath.Join(cfgDir, dotfilesRepoDir)
subcommands = []string{"clone", args[0], dotfilesRepoDir}
gitCmdDir = cfgDir
promtText = fmt.Sprintf("Cloning %s into directory %s.\n Continue?", gitRepo, dotfilesDir)
promptText = fmt.Sprintf("Cloning %s into directory %s.\n Continue?", gitRepo, dotfilesDir)
// This follows the same pattern outlined by others in the market:
// https://github.com/coder/coder/pull/1696#issue-1245742312
installScriptSet = []string{
Expand All @@ -52,13 +58,13 @@ func dotfiles() *cobra.Command {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Found dotfiles repository at %s\n", dotfilesDir)
gitCmdDir = dotfilesDir
subcommands = []string{"pull", "--ff-only"}
promtText = fmt.Sprintf("Pulling latest from %s into directory %s.\n Continue?", gitRepo, dotfilesDir)
promptText = fmt.Sprintf("Pulling latest from %s into directory %s.\n Continue?", gitRepo, dotfilesDir)
} else {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Did not find dotfiles repository at %s\n", dotfilesDir)
}

_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: promtText,
Text: promptText,
IsConfirm: true,
})
if err != nil {
Expand Down Expand Up @@ -94,57 +100,21 @@ func dotfiles() *cobra.Command {
return xerrors.Errorf("reading files in dir %s: %w", dotfilesDir, err)
}

var scripts []string
var dotfiles []string
for _, f := range files {
for _, i := range installScriptSet {
if f.Name() == i {
scripts = append(scripts, f.Name())
}
}

// make sure we do not copy `.git*` files
if strings.HasPrefix(f.Name(), ".") && !strings.HasPrefix(f.Name(), ".git") {
dotfiles = append(dotfiles, f.Name())
}
}

// run found install scripts
if len(scripts) > 0 {
t := "Found install script(s). The following script(s) will be executed in order:\n\n"
for _, s := range scripts {
t = fmt.Sprintf("%s - %s\n", t, s)
}
t = fmt.Sprintf("%s\n Continue?", t)
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: t,
IsConfirm: true,
})
if err != nil {
return err
}

for _, s := range scripts {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nRunning %s...\n", s)
// it is safe to use a variable command here because it's from
// a filtered list of pre-approved install scripts
// nolint:gosec
c := exec.CommandContext(cmd.Context(), fmt.Sprintf("./%s", s))
c.Dir = dotfilesDir
out, err := c.CombinedOutput()
if err != nil {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Error.Render(string(out)))
return xerrors.Errorf("running %s: %w", s, err)
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(out))
script := findScript(installScriptSet, files)
if script == "" {
if len(dotfiles) == 0 {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No install scripts or dotfiles found, nothing to do.")
return nil
}

_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Dotfiles installation complete.")
return nil
}

// otherwise symlink dotfiles
if len(dotfiles) > 0 {
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "No install scripts found, symlinking dotfiles to home directory.\n\n Continue?",
IsConfirm: true,
Expand All @@ -153,14 +123,16 @@ func dotfiles() *cobra.Command {
return err
}

home, err := os.UserHomeDir()
if err != nil {
return xerrors.Errorf("getting user home: %w", err)
if homeDir == "" {
homeDir, err = os.UserHomeDir()
if err != nil {
return xerrors.Errorf("getting user home: %w", err)
}
}

for _, df := range dotfiles {
from := filepath.Join(dotfilesDir, df)
to := filepath.Join(home, df)
to := filepath.Join(homeDir, df)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Symlinking %s to %s...\n", from, to)
// if file already exists at destination remove it
// this behavior matches `ln -f`
Expand All @@ -182,11 +154,34 @@ func dotfiles() *cobra.Command {
return nil
}

_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No install scripts or dotfiles found, nothing to do.")
// run found install scripts
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: fmt.Sprintf("Running install script %s.\n\n Continue?", script),
IsConfirm: true,
})
if err != nil {
return err
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Running %s...\n", script)
// it is safe to use a variable command here because it's from
// a filtered list of pre-approved install scripts
// nolint:gosec
scriptCmd := exec.CommandContext(cmd.Context(), fmt.Sprintf("./%s", script))
scriptCmd.Dir = dotfilesDir
out, err = scriptCmd.CombinedOutput()
if err != nil {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Error.Render(string(out)))
return xerrors.Errorf("running %s: %w", script, err)
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(out))

_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Dotfiles installation complete.")
return nil
},
}
cliui.AllowSkipPrompt(cmd)
cliflag.StringVarP(cmd.Flags(), &homeDir, "home-dir", "-d", "CODER_HOME_DIR", "", "Specifies the home directory for the dotfiles symlink destination. If empty will use $HOME.")

return cmd
}
Expand All @@ -203,3 +198,15 @@ func dirExists(name string) (bool, error) {

return true, nil
}

func findScript(installScriptSet []string, files []fs.DirEntry) string {
for _, i := range installScriptSet {
for _, f := range files {
if f.Name() == i {
return f.Name()
}
}
}

return ""
}