Skip to content

feat: add --branch option to clone or checkout different dotfiles branch #8331

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 8 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
72 changes: 72 additions & 0 deletions cli/dotfiles.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"bytes"
"errors"
"fmt"
"io/fs"
Expand All @@ -18,6 +19,8 @@ import (

func (r *RootCmd) dotfiles() *clibase.Cmd {
var symlinkDir string
var gitbranch string

cmd := &clibase.Cmd{
Use: "dotfiles <git_repo_url>",
Middleware: clibase.RequireNArgs(1),
Expand Down Expand Up @@ -102,6 +105,9 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
}
gitCmdDir = cfgDir
subcommands = []string{"clone", inv.Args[0], dotfilesRepoDir}
if gitbranch != "" {
subcommands = append(subcommands, "--branch", gitbranch)
}
promptText = fmt.Sprintf("Cloning %s into directory %s.\n\n Continue?", gitRepo, dotfilesDir)
}

Expand Down Expand Up @@ -140,6 +146,23 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Error.Render("Failed to update repo, continuing..."))
}

if dotfilesExists && gitbranch != "" {
// If the repo exists and the git-branch is specified, we need to check out the branch. We do this after
// git pull to make sure the branch was pulled down locally. If we do this before the pull, we could be
// trying to checkout a branch that does not yet exist locally and get a git error.
_, _ = fmt.Fprintf(inv.Stdout, "Dotfiles git branch %q specified\n", gitbranch)
err := ensureCorrectGitBranch(inv, ensureCorrectGitBranchParams{
repoDir: dotfilesDir,
gitSSHCommand: gitsshCmd,
gitBranch: gitbranch,
})
if err != nil {
// Do not block on this error, just log it and continue
_, _ = fmt.Fprintln(inv.Stdout,
cliui.DefaultStyles.Error.Render(fmt.Sprintf("Failed to use branch %q (%s), continuing...", err.Error(), gitbranch)))
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to use a non-zero exit code if this happens? Otherwise the user might expect one thing but receive another due to the silent failure. (Not saying we can't continue from here, but set a branchFailed = true and then exit 1 at the end.

Copy link
Member Author

Choose a reason for hiding this comment

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

That is an idea. I am not sure, I was copying the existing behavior.

See this:

if !dotfilesExists {

If the git pull --ff-only fails then we continue, even though we are now operating in a stale dotfiles. I was maintaining that behavior of continuing.

If we decide to exit code 1, we should go back and also change the previous behavior.

}
}

// save git repo url so we can detect changes next time
err = cfg.DotfilesURL().Write(gitRepo)
if err != nil {
Expand Down Expand Up @@ -246,11 +269,60 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
Description: "Specifies the directory for the dotfiles symlink destinations. If empty, will use $HOME.",
Value: clibase.StringOf(&symlinkDir),
},
{
Flag: "branch",
FlagShorthand: "b",
Description: "Specifies which branch to clone. " +
"If empty, will default to cloning the default branch or using the existing branch in the cloned repo on disk.",
Value: clibase.StringOf(&gitbranch),
},
cliui.SkipPromptOption(),
}
return cmd
}

type ensureCorrectGitBranchParams struct {
repoDir string
gitSSHCommand string
gitBranch string
}

func ensureCorrectGitBranch(baseInv *clibase.Invocation, params ensureCorrectGitBranchParams) error {
dotfileCmd := func(cmd string, args ...string) *exec.Cmd {
c := exec.CommandContext(baseInv.Context(), cmd, args...)
c.Dir = params.repoDir
c.Env = append(baseInv.Environ.ToOS(), fmt.Sprintf(`GIT_SSH_COMMAND=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`, params.gitSSHCommand))
var out bytes.Buffer
c.Stdout = &out
c.Stderr = baseInv.Stderr
return c
}
c := dotfileCmd("git", "branch", "--show-current")
// Save the output
var out bytes.Buffer
c.Stdout = &out
err := c.Run()
if err != nil {
return xerrors.Errorf("getting current git branch: %w", err)
}

if strings.TrimSpace(out.String()) != params.gitBranch {
// Checkout and pull the branch
c := dotfileCmd("git", "checkout", params.gitBranch)
err := c.Run()
if err != nil {
return xerrors.Errorf("checkout git branch %q: %w", params.gitBranch, err)
}

c = dotfileCmd("git", "pull", "--ff-only")
err = c.Run()
if err != nil {
return xerrors.Errorf("pull git branch %q: %w", params.gitBranch, err)
}
}
return nil
}

// dirExists checks if the path exists and is a directory.
func dirExists(name string) (bool, error) {
fi, err := os.Stat(name)
Expand Down
51 changes: 51 additions & 0 deletions cli/dotfiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,52 @@ func TestDotfiles(t *testing.T) {
require.NoError(t, err)
require.Equal(t, string(b), "wow\n")
})
t.Run("InstallScriptChangeBranch", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("install scripts on windows require sh and aren't very practical")
}
_, root := clitest.New(t)
testRepo := testGitRepo(t, root)

// We need an initial commit to start the `main` branch
c := exec.Command("git", "commit", "--allow-empty", "-m", `"initial commit"`)
c.Dir = testRepo
err := c.Run()
require.NoError(t, err)

// nolint:gosec
err = os.WriteFile(filepath.Join(testRepo, "install.sh"), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0o750)
require.NoError(t, err)

c = exec.Command("git", "checkout", "-b", "other_branch")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)

c = exec.Command("git", "add", "install.sh")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)

c = exec.Command("git", "commit", "-m", `"add install.sh"`)
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)

c = exec.Command("git", "checkout", "main")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)

inv, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo, "-b", "other_branch")
err = inv.Run()
require.NoError(t, err)

b, err := os.ReadFile(filepath.Join(string(root), ".bashrc"))
require.NoError(t, err)
require.Equal(t, string(b), "wow\n")
})
t.Run("SymlinkBackup", func(t *testing.T) {
t.Parallel()
_, root := clitest.New(t)
Expand Down Expand Up @@ -152,5 +198,10 @@ func testGitRepo(t *testing.T, root config.Root) string {
err = c.Run()
require.NoError(t, err)

c = exec.Command("git", "checkout", "-b", "main")
c.Dir = dir
err = c.Run()
require.NoError(t, err)

return dir
}
5 changes: 5 additions & 0 deletions cli/testdata/coder_dotfiles_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Personalize your workspace by applying a canonical dotfiles repository
 $ coder dotfiles --yes git@github.com:example/dotfiles.git 

Options
-b, --branch string
Specifies which branch to clone. If empty, will default to cloning the
default branch or using the existing branch in the cloned repo on
disk.

--symlink-dir string, $CODER_SYMLINK_DIR
Specifies the directory for the dotfiles symlink destinations. If
empty, will use $HOME.
Expand Down
8 changes: 8 additions & 0 deletions docs/cli/dotfiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ coder dotfiles [flags] <git_repo_url>

## Options

### -b, --branch

| | |
| ---- | ------------------- |
| Type | <code>string</code> |

Specifies which branch to clone. If empty, will default to cloning the default branch or using the existing branch in the cloned repo on disk.

### --symlink-dir

| | |
Expand Down