Skip to content

Commit 9f5bc7c

Browse files
authored
feat: add --branch option to clone or checkout different dotfiles branch (#8331)
* feat: --branch option to clone different dotfiles branch * chore: checkout specified branch if dotfiles already exist
1 parent 5bb6bc5 commit 9f5bc7c

File tree

4 files changed

+135
-0
lines changed

4 files changed

+135
-0
lines changed

cli/dotfiles.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"bytes"
45
"errors"
56
"fmt"
67
"io/fs"
@@ -18,6 +19,8 @@ import (
1819

1920
func (r *RootCmd) dotfiles() *clibase.Cmd {
2021
var symlinkDir string
22+
var gitbranch string
23+
2124
cmd := &clibase.Cmd{
2225
Use: "dotfiles <git_repo_url>",
2326
Middleware: clibase.RequireNArgs(1),
@@ -102,6 +105,9 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
102105
}
103106
gitCmdDir = cfgDir
104107
subcommands = []string{"clone", inv.Args[0], dotfilesRepoDir}
108+
if gitbranch != "" {
109+
subcommands = append(subcommands, "--branch", gitbranch)
110+
}
105111
promptText = fmt.Sprintf("Cloning %s into directory %s.\n\n Continue?", gitRepo, dotfilesDir)
106112
}
107113

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

149+
if dotfilesExists && gitbranch != "" {
150+
// If the repo exists and the git-branch is specified, we need to check out the branch. We do this after
151+
// git pull to make sure the branch was pulled down locally. If we do this before the pull, we could be
152+
// trying to checkout a branch that does not yet exist locally and get a git error.
153+
_, _ = fmt.Fprintf(inv.Stdout, "Dotfiles git branch %q specified\n", gitbranch)
154+
err := ensureCorrectGitBranch(inv, ensureCorrectGitBranchParams{
155+
repoDir: dotfilesDir,
156+
gitSSHCommand: gitsshCmd,
157+
gitBranch: gitbranch,
158+
})
159+
if err != nil {
160+
// Do not block on this error, just log it and continue
161+
_, _ = fmt.Fprintln(inv.Stdout,
162+
cliui.DefaultStyles.Error.Render(fmt.Sprintf("Failed to use branch %q (%s), continuing...", err.Error(), gitbranch)))
163+
}
164+
}
165+
143166
// save git repo url so we can detect changes next time
144167
err = cfg.DotfilesURL().Write(gitRepo)
145168
if err != nil {
@@ -246,11 +269,59 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
246269
Description: "Specifies the directory for the dotfiles symlink destinations. If empty, will use $HOME.",
247270
Value: clibase.StringOf(&symlinkDir),
248271
},
272+
{
273+
Flag: "branch",
274+
FlagShorthand: "b",
275+
Description: "Specifies which branch to clone. " +
276+
"If empty, will default to cloning the default branch or using the existing branch in the cloned repo on disk.",
277+
Value: clibase.StringOf(&gitbranch),
278+
},
249279
cliui.SkipPromptOption(),
250280
}
251281
return cmd
252282
}
253283

284+
type ensureCorrectGitBranchParams struct {
285+
repoDir string
286+
gitSSHCommand string
287+
gitBranch string
288+
}
289+
290+
func ensureCorrectGitBranch(baseInv *clibase.Invocation, params ensureCorrectGitBranchParams) error {
291+
dotfileCmd := func(cmd string, args ...string) *exec.Cmd {
292+
c := exec.CommandContext(baseInv.Context(), cmd, args...)
293+
c.Dir = params.repoDir
294+
c.Env = append(baseInv.Environ.ToOS(), fmt.Sprintf(`GIT_SSH_COMMAND=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`, params.gitSSHCommand))
295+
c.Stdout = baseInv.Stdout
296+
c.Stderr = baseInv.Stderr
297+
return c
298+
}
299+
c := dotfileCmd("git", "branch", "--show-current")
300+
// Save the output
301+
var out bytes.Buffer
302+
c.Stdout = &out
303+
err := c.Run()
304+
if err != nil {
305+
return xerrors.Errorf("getting current git branch: %w", err)
306+
}
307+
308+
if strings.TrimSpace(out.String()) != params.gitBranch {
309+
// Checkout and pull the branch
310+
c := dotfileCmd("git", "checkout", params.gitBranch)
311+
err := c.Run()
312+
if err != nil {
313+
return xerrors.Errorf("checkout git branch %q: %w", params.gitBranch, err)
314+
}
315+
316+
c = dotfileCmd("git", "pull", "--ff-only")
317+
err = c.Run()
318+
if err != nil {
319+
return xerrors.Errorf("pull git branch %q: %w", params.gitBranch, err)
320+
}
321+
}
322+
return nil
323+
}
324+
254325
// dirExists checks if the path exists and is a directory.
255326
func dirExists(name string) (bool, error) {
256327
fi, err := os.Stat(name)

cli/dotfiles_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,52 @@ func TestDotfiles(t *testing.T) {
8080
require.NoError(t, err)
8181
require.Equal(t, string(b), "wow\n")
8282
})
83+
t.Run("InstallScriptChangeBranch", func(t *testing.T) {
84+
t.Parallel()
85+
if runtime.GOOS == "windows" {
86+
t.Skip("install scripts on windows require sh and aren't very practical")
87+
}
88+
_, root := clitest.New(t)
89+
testRepo := testGitRepo(t, root)
90+
91+
// We need an initial commit to start the `main` branch
92+
c := exec.Command("git", "commit", "--allow-empty", "-m", `"initial commit"`)
93+
c.Dir = testRepo
94+
err := c.Run()
95+
require.NoError(t, err)
96+
97+
// nolint:gosec
98+
err = os.WriteFile(filepath.Join(testRepo, "install.sh"), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0o750)
99+
require.NoError(t, err)
100+
101+
c = exec.Command("git", "checkout", "-b", "other_branch")
102+
c.Dir = testRepo
103+
err = c.Run()
104+
require.NoError(t, err)
105+
106+
c = exec.Command("git", "add", "install.sh")
107+
c.Dir = testRepo
108+
err = c.Run()
109+
require.NoError(t, err)
110+
111+
c = exec.Command("git", "commit", "-m", `"add install.sh"`)
112+
c.Dir = testRepo
113+
err = c.Run()
114+
require.NoError(t, err)
115+
116+
c = exec.Command("git", "checkout", "main")
117+
c.Dir = testRepo
118+
err = c.Run()
119+
require.NoError(t, err)
120+
121+
inv, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo, "-b", "other_branch")
122+
err = inv.Run()
123+
require.NoError(t, err)
124+
125+
b, err := os.ReadFile(filepath.Join(string(root), ".bashrc"))
126+
require.NoError(t, err)
127+
require.Equal(t, string(b), "wow\n")
128+
})
83129
t.Run("SymlinkBackup", func(t *testing.T) {
84130
t.Parallel()
85131
_, root := clitest.New(t)
@@ -152,5 +198,10 @@ func testGitRepo(t *testing.T, root config.Root) string {
152198
err = c.Run()
153199
require.NoError(t, err)
154200

201+
c = exec.Command("git", "checkout", "-b", "main")
202+
c.Dir = dir
203+
err = c.Run()
204+
require.NoError(t, err)
205+
155206
return dir
156207
}

cli/testdata/coder_dotfiles_--help.golden

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ Personalize your workspace by applying a canonical dotfiles repository
77
 $ coder dotfiles --yes git@github.com:example/dotfiles.git 
88

99
Options
10+
-b, --branch string
11+
Specifies which branch to clone. If empty, will default to cloning the
12+
default branch or using the existing branch in the cloned repo on
13+
disk.
14+
1015
--symlink-dir string, $CODER_SYMLINK_DIR
1116
Specifies the directory for the dotfiles symlink destinations. If
1217
empty, will use $HOME.

docs/cli/dotfiles.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ coder dotfiles [flags] <git_repo_url>
2020

2121
## Options
2222

23+
### -b, --branch
24+
25+
| | |
26+
| ---- | ------------------- |
27+
| Type | <code>string</code> |
28+
29+
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.
30+
2331
### --symlink-dir
2432

2533
| | |

0 commit comments

Comments
 (0)