Skip to content

Commit

Permalink
Add support for exact directories, fixes #11
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Jan 5, 2019
1 parent 532d788 commit 7bb40c2
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 50 deletions.
5 changes: 3 additions & 2 deletions README.md
Expand Up @@ -395,19 +395,20 @@ with a `.`. The following prefixes and suffixes are special.
| -------------------- | ----------------------------------------------------------------------------------|
| `private_` prefix | Remove all group and world permissions from the target file or directory. |
| `empty_` prefix | Ensure the file exists, even if is empty. By default, empty files are removed. |
| `exact_` prefix | Remove anything not managed by `chezmoi`. |
| `executable_` prefix | Add executable permissions to the target file. |
| `symlink_` prefix | Create a symlink instead of a regular file. |
| `dot_` prefix | Rename to use a leading dot, e.g. `dot_foo` becomes `.foo`. |
| `.tmpl` suffix | Treat the contents of the source file as a template. |

Order is important, the order is `private_`, `empty_`, `executable_`,
Order is important, the order is `exact_`, `private_`, `empty_`, `executable_`,
`symlink_`, `dot_`, `.tmpl`.

Different target types allow different prefixes and suffixes.

| Target type | Allowed prefixes and suffixes |
| ------------- | ---------------------------------------------------- |
| Directory | `private_`, `dot_` |
| Directory | `exact_, `private_`, `dot_` |
| Regular file | `private_`, `empty_`, `executable_`, `dot_`, `.tmpl` |
| Symbolic link | `symlink_`, `dot_`, `.tmpl` |

Expand Down
6 changes: 4 additions & 2 deletions cmd/add.go
Expand Up @@ -19,6 +19,7 @@ var addCommand = &cobra.Command{
// An AddCommandConfig is a configuration for the add command.
type addCommandConfig struct {
empty bool
exact bool
recursive bool
template bool
}
Expand All @@ -28,6 +29,7 @@ func init() {

persistentFlags := addCommand.PersistentFlags()
persistentFlags.BoolVarP(&config.add.empty, "empty", "e", false, "add empty files")
persistentFlags.BoolVarP(&config.add.exact, "exact", "x", false, "add directories exactly")
persistentFlags.BoolVarP(&config.add.recursive, "recursive", "r", false, "recurse in to subdirectories")
persistentFlags.BoolVarP(&config.add.template, "template", "T", false, "add files as templates")
}
Expand Down Expand Up @@ -65,12 +67,12 @@ func (c *Config) runAddCommand(fs vfs.FS, command *cobra.Command, args []string)
if err != nil {
return err
}
return ts.Add(fs, path, info, c.add.empty, c.add.template, mutator)
return ts.Add(fs, path, info, c.add.exact, c.add.empty, c.add.template, mutator)
}); err != nil {
return err
}
} else {
if err := ts.Add(fs, path, nil, c.add.empty, c.add.template, mutator); err != nil {
if err := ts.Add(fs, path, nil, c.add.exact, c.add.empty, c.add.template, mutator); err != nil {
return err
}
}
Expand Down
41 changes: 41 additions & 0 deletions cmd/add_test.go
Expand Up @@ -82,6 +82,47 @@ func TestAddCommand(t *testing.T) {
),
},
},
{
name: "add_exact_dir",
args: []string{"/home/jenkins/dir"},
add: addCommandConfig{
exact: true,
},
root: map[string]interface{}{
"/home/jenkins": &vfst.Dir{Perm: 0755},
"/home/jenkins/.chezmoi": &vfst.Dir{Perm: 0700},
"/home/jenkins/dir": &vfst.Dir{Perm: 0755},
},
tests: []vfst.Test{
vfst.TestPath("/home/jenkins/.chezmoi/exact_dir",
vfst.TestIsDir,
),
},
},
{
name: "add_exact_dir_recursive",
args: []string{"/home/jenkins/dir"},
add: addCommandConfig{
exact: true,
recursive: true,
},
root: map[string]interface{}{
"/home/jenkins": &vfst.Dir{Perm: 0755},
"/home/jenkins/.chezmoi": &vfst.Dir{Perm: 0700},
"/home/jenkins/dir": map[string]interface{}{
"foo": "bar",
},
},
tests: []vfst.Test{
vfst.TestPath("/home/jenkins/.chezmoi/exact_dir",
vfst.TestIsDir,
),
vfst.TestPath("/home/jenkins/.chezmoi/exact_dir/foo",
vfst.TestModeIsRegular,
vfst.TestContentsString("bar"),
),
},
},
{
name: "add_empty_file",
args: []string{"/home/jenkins/empty"},
Expand Down
4 changes: 3 additions & 1 deletion cmd/import.go
Expand Up @@ -28,6 +28,7 @@ var _importCommand = &cobra.Command{

type importCommandConfig struct {
destination string
exact bool
removeDestination bool
stripComponents int
}
Expand All @@ -37,6 +38,7 @@ func init() {

persistentFlags := _importCommand.PersistentFlags()
persistentFlags.StringVarP(&config._import.destination, "destination", "d", "", "destination prefix")
persistentFlags.BoolVarP(&config._import.exact, "exact", "x", false, "import directories exactly")
persistentFlags.IntVar(&config._import.stripComponents, "strip-components", 0, "strip components")
persistentFlags.BoolVarP(&config._import.removeDestination, "remove-destination", "r", false, "remove destination before import")
}
Expand Down Expand Up @@ -83,5 +85,5 @@ func (c *Config) runImportCommand(fs vfs.FS, cmd *cobra.Command, args []string)
return err
}
}
return ts.ImportTAR(tar.NewReader(r), c._import.destination, c._import.stripComponents, mutator)
return ts.ImportTAR(tar.NewReader(r), c._import.destination, c._import.exact, c._import.stripComponents, mutator)
}
29 changes: 18 additions & 11 deletions lib/chezmoi/chezmoi.go
Expand Up @@ -14,6 +14,7 @@ const (
symlinkPrefix = "symlink_"
privatePrefix = "private_"
emptyPrefix = "empty_"
exactPrefix = "exact_"
executablePrefix = "executable_"
dotPrefix = "dot_"
templateSuffix = ".tmpl"
Expand All @@ -37,7 +38,7 @@ type Entry interface {

type parsedSourceFilePath struct {
FileAttributes
dirNames []string
dirAttributes []DirAttributes
}

// ReturnTemplateFuncError causes template execution to return an error.
Expand All @@ -47,27 +48,33 @@ func ReturnTemplateFuncError(err error) {
})
}

// parseDirNameComponents parses multiple directory name components. It returns
// the target directory names, target permissions, and any error.
func parseDirNameComponents(components []string) ([]string, []os.FileMode) {
dirNames := []string{}
perms := []os.FileMode{}
// parseDirNameComponents parses multiple directory name components.
func parseDirNameComponents(components []string) []DirAttributes {
das := []DirAttributes{}
for _, component := range components {
da := ParseDirAttributes(component)
dirNames = append(dirNames, da.Name)
perms = append(perms, da.Perm)
das = append(das, da)
}
return dirNames, perms
return das
}

// dirNames returns the dir names from dirAttributes.
func dirNames(dirAttributes []DirAttributes) []string {
dns := []string{}
for _, da := range dirAttributes {
dns = append(dns, da.Name)
}
return dns
}

// parseSourceFilePath parses a single source file path.
func parseSourceFilePath(path string) parsedSourceFilePath {
components := splitPathList(path)
dirNames, _ := parseDirNameComponents(components[0 : len(components)-1])
das := parseDirNameComponents(components[0 : len(components)-1])
fa := ParseFileAttributes(components[len(components)-1])
return parsedSourceFilePath{
FileAttributes: fa,
dirNames: dirNames,
dirAttributes: das,
}
}

Expand Down
40 changes: 34 additions & 6 deletions lib/chezmoi/dir.go
Expand Up @@ -11,14 +11,16 @@ import (

// DirAttributes holds attributes parsed from a source directory name.
type DirAttributes struct {
Name string
Perm os.FileMode
Name string
Exact bool
Perm os.FileMode
}

// A Dir represents the target state of a directory.
type Dir struct {
sourceName string
targetName string
Exact bool
Perm os.FileMode
Entries map[string]Entry
}
Expand All @@ -27,6 +29,7 @@ type dirConcreteValue struct {
Type string `json:"type" yaml:"type"`
SourcePath string `json:"sourcePath" yaml:"sourcePath"`
TargetPath string `json:"targetPath" yaml:"targetPath"`
Exact bool `json:"exact" yaml:"exact"`
Perm int `json:"perm" yaml:"perm"`
Entries []interface{} `json:"entries" yaml:"entries"`
}
Expand All @@ -35,6 +38,11 @@ type dirConcreteValue struct {
func ParseDirAttributes(sourceName string) DirAttributes {
name := sourceName
perm := os.FileMode(0777)
exact := false
if strings.HasPrefix(name, exactPrefix) {
name = strings.TrimPrefix(name, exactPrefix)
exact = true
}
if strings.HasPrefix(name, privatePrefix) {
name = strings.TrimPrefix(name, privatePrefix)
perm &= 0700
Expand All @@ -43,16 +51,20 @@ func ParseDirAttributes(sourceName string) DirAttributes {
name = "." + strings.TrimPrefix(name, dotPrefix)
}
return DirAttributes{
Name: name,
Perm: perm,
Name: name,
Exact: exact,
Perm: perm,
}
}

// SourceName returns da's source name.
func (da DirAttributes) SourceName() string {
sourceName := ""
if da.Exact {
sourceName += exactPrefix
}
if da.Perm&os.FileMode(077) == os.FileMode(0) {
sourceName = privatePrefix
sourceName += privatePrefix
}
if strings.HasPrefix(da.Name, ".") {
sourceName += dotPrefix + strings.TrimPrefix(da.Name, ".")
Expand All @@ -63,10 +75,11 @@ func (da DirAttributes) SourceName() string {
}

// newDir returns a new directory state.
func newDir(sourceName string, targetName string, perm os.FileMode) *Dir {
func newDir(sourceName string, targetName string, exact bool, perm os.FileMode) *Dir {
return &Dir{
sourceName: sourceName,
targetName: targetName,
Exact: exact,
Perm: perm,
Entries: make(map[string]Entry),
}
Expand Down Expand Up @@ -100,6 +113,20 @@ func (d *Dir) Apply(fs vfs.FS, targetDir string, umask os.FileMode, mutator Muta
return err
}
}
if d.Exact {
infos, err := fs.ReadDir(targetPath)
if err != nil {
return err
}
for _, info := range infos {
name := info.Name()
if _, ok := d.Entries[name]; !ok {
if err := mutator.RemoveAll(filepath.Join(targetPath, name)); err != nil {
return err
}
}
}
}
return nil
}

Expand All @@ -119,6 +146,7 @@ func (d *Dir) ConcreteValue(targetDir, sourceDir string, recursive bool) (interf
Type: "dir",
SourcePath: filepath.Join(sourceDir, d.SourceName()),
TargetPath: filepath.Join(targetDir, d.TargetName()),
Exact: d.Exact,
Perm: int(d.Perm),
Entries: entryConcreteValues,
}, nil
Expand Down
36 changes: 28 additions & 8 deletions lib/chezmoi/dir_test.go
Expand Up @@ -14,29 +14,49 @@ func TestDirAttributes(t *testing.T) {
{
sourceName: "foo",
da: DirAttributes{
Name: "foo",
Perm: 0777,
Name: "foo",
Exact: false,
Perm: 0777,
},
},
{
sourceName: "dot_foo",
da: DirAttributes{
Name: ".foo",
Perm: 0777,
Name: ".foo",
Exact: false,
Perm: 0777,
},
},
{
sourceName: "private_foo",
da: DirAttributes{
Name: "foo",
Perm: 0700,
Name: "foo",
Exact: false,
Perm: 0700,
},
},
{
sourceName: "exact_foo",
da: DirAttributes{
Name: "foo",
Exact: true,
Perm: 0777,
},
},
{
sourceName: "private_dot_foo",
da: DirAttributes{
Name: ".foo",
Perm: 0700,
Name: ".foo",
Exact: false,
Perm: 0700,
},
},
{
sourceName: "exact_private_dot_foo",
da: DirAttributes{
Name: ".foo",
Exact: true,
Perm: 0700,
},
},
} {
Expand Down

0 comments on commit 7bb40c2

Please sign in to comment.