Skip to content

Commit

Permalink
feat: Add working tree config option (#1459)
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne authored Sep 26, 2021
1 parent 73d8ceb commit f42866b
Show file tree
Hide file tree
Showing 12 changed files with 132 additions and 53 deletions.
24 changes: 18 additions & 6 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Manage your dotfiles across multiple machines, securely.
* [`--use-builtin-git` *value*](#--use-builtin-git-value)
* [`-v`, `--verbose`](#-v---verbose)
* [`--version`](#--version)
* [`-w`, `--working-tree` *directory*](#-w---working-tree-directory)
* [Common command line flags](#common-command-line-flags)
* [`-f`, `--format` `json`|`yaml`](#-f---format-jsonyaml)
* [`-i`, `--include` *types*](#-i---include-types)
Expand Down Expand Up @@ -152,6 +153,9 @@ destination directory, where:
* The *config file* contains machine-specific configuration, by default it is
`~/.config/chezmoi/chezmoi.toml`.

* The *working tree* is the git working tree. Normally it is the same as the
source directory, but can be a parent of the source directory.

---

## Global command line flags
Expand Down Expand Up @@ -244,6 +248,12 @@ state and the destination set are printed as unified diffs.
Print the version of chezmoi, the commit at which it was built, and the build
timestamp.

### `-w`, `--working-tree` *directory*

Use *directory* as the git working tree directory. By default, chezmoi searches
the source directory and then its ancestors for the first directory that
contains a `.git` directory.

---

## Common command line flags
Expand Down Expand Up @@ -327,6 +337,7 @@ The following configuration variables are available:
| | `umask` | int | *from system* | Umask |
| | `useBuiltinAge` | string | `auto` | Use builtin git if `age` command is not found in $PATH |
| | `useBuiltinGit` | string | `auto` | Use builtin git if `git` command is not found in $PATH |
| | `workingTree` | string | *source directory* | git working tree directory |
| `add` | `templateSymlinks` | bool | `false` | Template symlinks to source and home dirs |
| `age` | `args` | []string | *none* | Extra args to age CLI command |
| | `command` | string | `age` | age CLI command |
Expand Down Expand Up @@ -961,10 +972,10 @@ $ chezmoi cat ~/.bashrc

### `cd`

Launch a shell in the source directory. chezmoi will launch the command set by
the `cd.command` configuration variable with any extra arguments specified by
`cd.args`. If this is not set, chezmoi will attempt to detect your shell and
will finally fall back to an OS-specific default.
Launch a shell in the working tree (typically the source directory). chezmoi
will launch the command set by the `cd.command` configuration variable with any
extra arguments specified by `cd.args`. If this is not set, chezmoi will attempt
to detect your shell and will finally fall back to an OS-specific default.

#### `cd` examples

Expand Down Expand Up @@ -1244,8 +1255,9 @@ $ chezmoi forget ~/.bashrc

### `git` [*arg*...]

Run `git` *arg*s in the source directory. Note that flags in *arguments* must
occur after `--` to prevent chezmoi from interpreting them.
Run `git` *arg*s in the working tree (typically the source directory). Note that
flags in *arguments* must occur after `--` to prevent chezmoi from interpreting
them.

#### `git` examples

Expand Down
7 changes: 7 additions & 0 deletions internal/chezmoi/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func (p AbsPath) MustTrimDirPrefix(dirPrefix AbsPath) RelPath {

// Set implements github.com/spf13/pflag.Value.Set.
func (p *AbsPath) Set(s string) error {
if s == "" {
*p = ""
return nil
}
homeDirAbsPath, err := homeDirAbsPath()
if err != nil {
return err
Expand All @@ -68,6 +72,9 @@ func (p AbsPath) String() string {

// TrimDirPrefix trims prefix from p.
func (p AbsPath) TrimDirPrefix(dirPrefixAbsPath AbsPath) (RelPath, error) {
if p == dirPrefixAbsPath {
return "", nil
}
dirAbsPath := dirPrefixAbsPath
if dirAbsPath != "/" {
dirAbsPath += "/"
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/addcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func (c *Config) newAddCmd() *cobra.Command {
modifiesSourceDirectory: "true",
persistentStateMode: persistentStateModeReadWrite,
requiresSourceDirectory: "true",
requiresWorkingTree: "true",
},
}

Expand Down
3 changes: 2 additions & 1 deletion internal/cmd/cdcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func (c *Config) newCDCmd() *cobra.Command {
Annotations: map[string]string{
doesNotRequireValidConfig: "true",
requiresSourceDirectory: "true",
requiresWorkingTree: "true",
runsCommands: "true",
},
}
Expand All @@ -33,5 +34,5 @@ func (c *Config) runCDCmd(cmd *cobra.Command, args []string) error {
if shellCommand == "" {
shellCommand, _ = shell.CurrentUserShell()
}
return c.run(c.SourceDirAbsPath, shellCommand, c.CD.Args)
return c.run(c.WorkingTreeAbsPath, shellCommand, c.CD.Args)
}
1 change: 1 addition & 0 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
persistentStateMode = "chezmoi_persistent_state_mode"
requiresConfigDirectory = "chezmoi_requires_config_directory"
requiresSourceDirectory = "chezmoi_requires_source_directory"
requiresWorkingTree = "chezmoi_requires_working_tree"
runsCommands = "chezmoi_runs_commands"
)

Expand Down
79 changes: 47 additions & 32 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,20 @@ type templateConfig struct {
// A Config represents a configuration.
type Config struct {
// Global configuration, settable in the config file.
CacheDirAbsPath chezmoi.AbsPath `mapstructure:"cacheDir"`
Color autoBool `mapstructure:"color"`
Data map[string]interface{} `mapstructure:"data"`
DestDirAbsPath chezmoi.AbsPath `mapstructure:"destDir"`
Interpreters map[string]*chezmoi.Interpreter `mapstructure:"interpreters"`
Mode chezmoi.Mode `mapstructure:"mode"`
Pager string `mapstructure:"pager"`
Safe bool `mapstructure:"safe"`
SourceDirAbsPath chezmoi.AbsPath `mapstructure:"sourceDir"`
Template templateConfig `mapstructure:"template"`
Umask fs.FileMode `mapstructure:"umask"`
UseBuiltinAge autoBool `mapstructure:"useBuiltinAge"`
UseBuiltinGit autoBool `mapstructure:"useBuiltinGit"`
CacheDirAbsPath chezmoi.AbsPath `mapstructure:"cacheDir"`
Color autoBool `mapstructure:"color"`
Data map[string]interface{} `mapstructure:"data"`
DestDirAbsPath chezmoi.AbsPath `mapstructure:"destDir"`
Interpreters map[string]*chezmoi.Interpreter `mapstructure:"interpreters"`
Mode chezmoi.Mode `mapstructure:"mode"`
Pager string `mapstructure:"pager"`
Safe bool `mapstructure:"safe"`
SourceDirAbsPath chezmoi.AbsPath `mapstructure:"sourceDir"`
Template templateConfig `mapstructure:"template"`
Umask fs.FileMode `mapstructure:"umask"`
UseBuiltinAge autoBool `mapstructure:"useBuiltinAge"`
UseBuiltinGit autoBool `mapstructure:"useBuiltinGit"`
WorkingTreeAbsPath chezmoi.AbsPath `mapstructure:"workingTree"`

// Global configuration, not settable in the config file.
configFormat readDataFormat
Expand Down Expand Up @@ -839,6 +840,7 @@ func (c *Config) doPurge(purgeOptions *purgeOptions) error {
c.configFileAbsPath.Dir(),
c.configFileAbsPath,
persistentStateFileAbsPath,
c.WorkingTreeAbsPath,
c.SourceDirAbsPath,
}
if purgeOptions != nil && purgeOptions.binary {
Expand Down Expand Up @@ -981,10 +983,10 @@ func (c *Config) findConfigTemplate() (chezmoi.RelPath, string, []byte, error) {
}

func (c *Config) gitAutoAdd() (*git.Status, error) {
if err := c.run(c.SourceDirAbsPath, c.Git.Command, []string{"add", "."}); err != nil {
if err := c.run(c.WorkingTreeAbsPath, c.Git.Command, []string{"add", "."}); err != nil {
return nil, err
}
output, err := c.cmdOutput(c.SourceDirAbsPath, c.Git.Command, []string{"status", "--porcelain=v2"})
output, err := c.cmdOutput(c.WorkingTreeAbsPath, c.Git.Command, []string{"status", "--porcelain=v2"})
if err != nil {
return nil, err
}
Expand All @@ -1007,14 +1009,14 @@ func (c *Config) gitAutoCommit(status *git.Status) error {
if err := commitMessageTmpl.Execute(&commitMessage, status); err != nil {
return err
}
return c.run(c.SourceDirAbsPath, c.Git.Command, []string{"commit", "--message", commitMessage.String()})
return c.run(c.WorkingTreeAbsPath, c.Git.Command, []string{"commit", "--message", commitMessage.String()})
}

func (c *Config) gitAutoPush(status *git.Status) error {
if status.Empty() {
return nil
}
return c.run(c.SourceDirAbsPath, c.Git.Command, []string{"push"})
return c.run(c.WorkingTreeAbsPath, c.Git.Command, []string{"push"})
}

func (c *Config) makeRunEWithSourceState(runE func(*cobra.Command, []string, *chezmoi.SourceState) error) func(*cobra.Command, []string) error {
Expand Down Expand Up @@ -1064,6 +1066,7 @@ func (c *Config) newRootCmd() (*cobra.Command, error) {
persistentFlags.VarP(&c.SourceDirAbsPath, "source", "S", "Set source directory")
persistentFlags.Var(&c.UseBuiltinAge, "use-builtin-age", "Use builtin age")
persistentFlags.Var(&c.UseBuiltinGit, "use-builtin-git", "Use builtin git")
persistentFlags.VarP(&c.WorkingTreeAbsPath, "working-tree", "W", "Set working tree directory")
for viperKey, key := range map[string]string{
"color": "color",
"destDir": "destination",
Expand All @@ -1072,6 +1075,7 @@ func (c *Config) newRootCmd() (*cobra.Command, error) {
"sourceDir": "source",
"useBuiltinAge": "use-builtin-age",
"useBuiltinGit": "use-builtin-git",
"workingTree": "working-tree",
} {
if err := viper.BindPFlag(viperKey, persistentFlags.Lookup(key)); err != nil {
return nil, err
Expand Down Expand Up @@ -1430,6 +1434,32 @@ func (c *Config) persistentPreRunRootE(cmd *cobra.Command, args []string) error
}
}

if c.WorkingTreeAbsPath == "" {
workingTreeAbsPath := c.SourceDirAbsPath
FOR:
for {
if info, err := c.baseSystem.Stat(workingTreeAbsPath.Join(".git")); err == nil && info.IsDir() {
c.WorkingTreeAbsPath = workingTreeAbsPath
break FOR
}
prevWorkingTreeDirAbsPath := workingTreeAbsPath
workingTreeAbsPath = workingTreeAbsPath.Dir()
if len(workingTreeAbsPath) >= len(prevWorkingTreeDirAbsPath) {
c.WorkingTreeAbsPath = c.SourceDirAbsPath
break FOR
}
}
}

if boolAnnotation(cmd, requiresWorkingTree) {
if _, err := c.SourceDirAbsPath.TrimDirPrefix(c.WorkingTreeAbsPath); err != nil {
return err
}
if err := chezmoi.MkdirAll(c.baseSystem, c.WorkingTreeAbsPath, 0o777); err != nil {
return err
}
}

return nil
}

Expand Down Expand Up @@ -1633,21 +1663,6 @@ func (c *Config) validateData() error {
return validateKeys(c.Data, identifierRx)
}

// workingTree searches upwards to find the git working tree.
func (c *Config) workingTree() chezmoi.AbsPath {
workingTreeDirAbsPath := c.SourceDirAbsPath
for {
if info, err := c.baseSystem.Stat(workingTreeDirAbsPath.Join(".git")); err == nil && info.IsDir() {
return workingTreeDirAbsPath
}
prevWorkingTreeDirAbsPath := workingTreeDirAbsPath
workingTreeDirAbsPath = workingTreeDirAbsPath.Dir()
if len(workingTreeDirAbsPath) >= len(prevWorkingTreeDirAbsPath) {
return ""
}
}
}

func (c *Config) writeOutput(data []byte) error {
if c.outputAbsPath == "" || c.outputAbsPath == "-" {
_, err := c.stdout.Write(data)
Expand Down
4 changes: 4 additions & 0 deletions internal/cmd/doctorcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ func (c *Config) runDoctorCmd(cmd *cobra.Command, args []string) error {
&suspiciousEntriesCheck{
dirname: c.SourceDirAbsPath,
},
&dirCheck{
name: "working-tree",
dirname: c.WorkingTreeAbsPath,
},
&dirCheck{
name: "dest-dir",
dirname: c.DestDirAbsPath,
Expand Down
6 changes: 1 addition & 5 deletions internal/cmd/editcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ func (c *Config) newEditCmd() *cobra.Command {

func (c *Config) runEditCmd(cmd *cobra.Command, args []string, sourceState *chezmoi.SourceState) error {
if len(args) == 0 {
dirAbsPath := c.workingTree()
if dirAbsPath == "" {
dirAbsPath = c.SourceDirAbsPath
}
if err := c.runEditor([]string{string(dirAbsPath)}); err != nil {
if err := c.runEditor([]string{string(c.WorkingTreeAbsPath)}); err != nil {
return err
}
if c.Edit.apply {
Expand Down
3 changes: 2 additions & 1 deletion internal/cmd/gitcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func (c *Config) newGitCmd() *cobra.Command {
RunE: c.runGitCmd,
Annotations: map[string]string{
requiresSourceDirectory: "true",
requiresWorkingTree: "true",
runsCommands: "true",
},
}
Expand All @@ -28,5 +29,5 @@ func (c *Config) newGitCmd() *cobra.Command {
}

func (c *Config) runGitCmd(cmd *cobra.Command, args []string) error {
return c.run(c.SourceDirAbsPath, c.Git.Command, args)
return c.run(c.WorkingTreeAbsPath, c.Git.Command, args)
}
23 changes: 16 additions & 7 deletions internal/cmd/initcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"regexp"
"runtime"
Expand Down Expand Up @@ -104,6 +105,7 @@ func (c *Config) newInitCmd() *cobra.Command {
modifiesDestinationDirectory: "true",
persistentStateMode: persistentStateModeReadWrite,
requiresSourceDirectory: "true",
requiresWorkingTree: "true",
runsCommands: "true",
},
}
Expand Down Expand Up @@ -133,8 +135,13 @@ func (c *Config) runInitCmd(cmd *cobra.Command, args []string) error {
}

// If we're not in a working tree then init it or clone it.
if c.workingTree() == "" {
rawSourceDir, err := c.baseSystem.RawPath(c.SourceDirAbsPath)
gitDirAbsPath := c.WorkingTreeAbsPath.Join(".git")
switch info, err := c.baseSystem.Stat(gitDirAbsPath); {
case err == nil && info.IsDir():
case err == nil && !info.IsDir():
return fmt.Errorf("%s: not a directory", gitDirAbsPath)
case errors.Is(err, fs.ErrNotExist):
workingTreeRawPath, err := c.baseSystem.RawPath(c.WorkingTreeAbsPath)
if err != nil {
return err
}
Expand All @@ -144,10 +151,10 @@ func (c *Config) runInitCmd(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
if useBuiltinGit {
isBare := false
if _, err = git.PlainInit(string(rawSourceDir), isBare); err != nil {
if _, err = git.PlainInit(string(workingTreeRawPath), isBare); err != nil {
return err
}
} else if err := c.run(c.SourceDirAbsPath, c.Git.Command, []string{"init", "--quiet"}); err != nil {
} else if err := c.run(c.WorkingTreeAbsPath, c.Git.Command, []string{"init", "--quiet"}); err != nil {
return err
}
} else {
Expand All @@ -164,7 +171,7 @@ func (c *Config) runInitCmd(cmd *cobra.Command, args []string) error {
RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
}
isBare := false
_, err = git.PlainClone(string(rawSourceDir), isBare, &cloneOptions)
_, err = git.PlainClone(string(workingTreeRawPath), isBare, &cloneOptions)
if errors.Is(err, transport.ErrAuthenticationRequired) {
var basicAuth http.BasicAuth
if basicAuth.Username, err = c.readLine(fmt.Sprintf("Username [default %q]? ", username)); err != nil {
Expand All @@ -177,7 +184,7 @@ func (c *Config) runInitCmd(cmd *cobra.Command, args []string) error {
return err
}
cloneOptions.Auth = &basicAuth
_, err = git.PlainClone(string(rawSourceDir), isBare, &cloneOptions)
_, err = git.PlainClone(string(workingTreeRawPath), isBare, &cloneOptions)
}
if err != nil {
return err
Expand All @@ -199,13 +206,15 @@ func (c *Config) runInitCmd(cmd *cobra.Command, args []string) error {
}
args = append(args,
dotfilesRepoURL,
string(rawSourceDir),
string(workingTreeRawPath),
)
if err := c.run("", c.Git.Command, args); err != nil {
return err
}
}
}
case err != nil:
return err
}

// Find config template, execute it, and create config file.
Expand Down
Loading

0 comments on commit f42866b

Please sign in to comment.