Skip to content

Commit

Permalink
Merge pull request twpayne#1283 from twpayne/literal
Browse files Browse the repository at this point in the history
Make all filenames representable in the source state
  • Loading branch information
twpayne committed Jul 2, 2021
2 parents 406cf8d + d506456 commit 509bda7
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 55 deletions.
15 changes: 3 additions & 12 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,21 +197,12 @@ better than just having the correct dotfile contents.
There are a number of criticisms of how chezmoi's source state is represented on
disk:

1. The source file naming system cannot handle all possible filenames.
2. Not all possible file permissions can be represented.
3. The long source file names are weird and verbose.
4. Everything is in a single directory, which can end up containing many entries.
1. Not all possible file permissions can be represented.
2. The long source file names are weird and verbose.
3. Everything is in a single directory, which can end up containing many entries.

chezmoi's source state representation is a deliberate, practical compromise.

Certain target filenames, for example `~/dot_example`, are incompatible with
chezmoi's
[attributes](https://github.com/twpayne/chezmoi/blob/master/docs/REFERENCE.md#source-state-attributes)
used in the source state. In practice, dotfile filenames are unlikely to
conflict with chezmoi's attributes. If this does cause a genuine problem for
you, please [open an issue on
GitHub](https://github.com/twpayne/chezmoi/issues/new/choose).

The `dot_` attribute makes it transparent which dotfiles are managed by chezmoi
and which files are ignored by chezmoi. chezmoi ignores all files and
directories that start with `.` so no special whitelists are needed for version
Expand Down
12 changes: 9 additions & 3 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,15 +387,17 @@ to as "attributes":
| `encrypted_` | Encrypt the file in the source state. |
| `exact_` | Remove anything not managed by chezmoi. |
| `executable_`| Add executable permissions to the target file. |
| `literal_` | Stop parsing prefix attributes. |
| `modify_` | Treat the contents as a script that modifies an existing file. |
| `once_` | Run script once. |
| `private_` | Remove all group and world permissions from the target file or directory. |
| `run_` | Treat the contents as a script to run. |
| `symlink_` | Create a symlink instead of a regular file. |

| Suffix | Effect |
| ------- | ---------------------------------------------------- |
| `.tmpl` | Treat the contents of the source file as a template. |
| Suffix | Effect |
| ---------- | ---------------------------------------------------- |
| `.literal` | Stop parsing suffix attributes. |
| `.tmpl` | Treat the contents of the source file as a template. |

Different target types allow different prefixes and suffixes. The order of
prefixes is important.
Expand All @@ -409,6 +411,10 @@ prefixes is important.
| Script | File | `run_`, `once_`, `before_` or `after_` | `.tmpl` |
| Symbolic link | File | `symlink_`, `dot_`, | `.tmpl` |

The `literal_` prefix and `.literal` suffix can appear anywhere and stop
attribute parsing. This permits filenames that would otherwise conflict with
chezmoi's attributes to be represented.

In addition, if the source file is encrypted, the suffix `.age` (when age
encryption is used) or `.asc` (when gpg encryption is used) is stripped. These
suffixes can be overridden with the `age.suffix` and `gpg.suffix` configuration
Expand Down
35 changes: 28 additions & 7 deletions internal/chezmoi/attr.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,11 @@ func parseDirAttr(sourceName string) DirAttr {
name = mustTrimPrefix(name, privatePrefix)
private = true
}
if strings.HasPrefix(name, dotPrefix) {
switch {
case strings.HasPrefix(name, dotPrefix):
name = "." + mustTrimPrefix(name, dotPrefix)
case strings.HasPrefix(name, literalPrefix):
name = name[len(literalPrefix):]
}
return DirAttr{
TargetName: name,
Expand All @@ -73,9 +76,12 @@ func (da DirAttr) SourceName() string {
if da.Private {
sourceName += privatePrefix
}
if strings.HasPrefix(da.TargetName, ".") {
switch {
case strings.HasPrefix(da.TargetName, "."):
sourceName += dotPrefix + mustTrimPrefix(da.TargetName, ".")
} else {
case dirPrefixRegexp.MatchString(da.TargetName):
sourceName += literalPrefix + da.TargetName
default:
sourceName += da.TargetName
}
return sourceName
Expand Down Expand Up @@ -166,15 +172,24 @@ func parseFileAttr(sourceName, encryptedSuffix string) FileAttr {
executable = true
}
}
if strings.HasPrefix(name, dotPrefix) {
switch {
case strings.HasPrefix(name, dotPrefix):
name = "." + mustTrimPrefix(name, dotPrefix)
case strings.HasPrefix(name, literalPrefix):
name = name[len(literalPrefix):]
}
if encrypted {
name = strings.TrimSuffix(name, encryptedSuffix)
}
if strings.HasSuffix(name, TemplateSuffix) {
switch {
case strings.HasSuffix(name, literalSuffix):
name = mustTrimSuffix(name, literalSuffix)
case strings.HasSuffix(name, TemplateSuffix):
name = mustTrimSuffix(name, TemplateSuffix)
template = true
if strings.HasSuffix(name, literalSuffix) {
name = mustTrimSuffix(name, literalSuffix)
}
}
return FileAttr{
TargetName: name,
Expand Down Expand Up @@ -239,11 +254,17 @@ func (fa FileAttr) SourceName(encryptedSuffix string) string {
case SourceFileTypeSymlink:
sourceName = symlinkPrefix
}
if strings.HasPrefix(fa.TargetName, ".") {
switch {
case strings.HasPrefix(fa.TargetName, "."):
sourceName += dotPrefix + mustTrimPrefix(fa.TargetName, ".")
} else {
case filePrefixRegexp.MatchString(fa.TargetName):
sourceName += literalPrefix + fa.TargetName
default:
sourceName += fa.TargetName
}
if fileSuffixRegexp.MatchString(fa.TargetName) {
sourceName += literalSuffix
}
if fa.Template {
sourceName += TemplateSuffix
}
Expand Down
182 changes: 150 additions & 32 deletions internal/chezmoi/attr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestDirAttr(t *testing.T) {
".dir",
"dir.tmpl",
"dir",
"exact_dir",
"empty_dir",
"encrypted_dir",
"executable_dir",
Expand All @@ -39,8 +40,54 @@ func TestDirAttr(t *testing.T) {
}
}

func TestDirAttrLiteral(t *testing.T) {
for _, tc := range []struct {
sourceName string
dirAttr DirAttr
}{
{
sourceName: "exact_dir",
dirAttr: DirAttr{
TargetName: "dir",
Exact: true,
},
},
{
sourceName: "literal_exact_dir",
dirAttr: DirAttr{
TargetName: "exact_dir",
},
},
{
sourceName: "literal_literal_dir",
dirAttr: DirAttr{
TargetName: "literal_dir",
},
},
} {
t.Run(tc.sourceName, func(t *testing.T) {
assert.Equal(t, tc.sourceName, tc.dirAttr.SourceName())
assert.Equal(t, tc.dirAttr, parseDirAttr(tc.sourceName))
})
}
}

func TestFileAttr(t *testing.T) {
var fas []FileAttr
targetNames := []string{
".name",
"create_name",
"dot_name",
"exact_name",
"literal_name",
"literal_name",
"modify_name",
"name.literal",
"name",
"run_name",
"symlink_name",
"template.tmpl",
}
require.NoError(t, combinator.Generate(&fas, struct {
Type SourceFileTargetType
TargetName []string
Expand All @@ -49,12 +96,8 @@ func TestFileAttr(t *testing.T) {
Private []bool
Template []bool
}{
Type: SourceFileTypeCreate,
TargetName: []string{
".name",
"exact_name",
"name",
},
Type: SourceFileTypeCreate,
TargetName: []string{},
Encrypted: []bool{false, true},
Executable: []bool{false, true},
Private: []bool{false, true},
Expand All @@ -69,12 +112,8 @@ func TestFileAttr(t *testing.T) {
Private []bool
Template []bool
}{
Type: SourceFileTypeFile,
TargetName: []string{
".name",
"exact_name",
"name",
},
Type: SourceFileTypeFile,
TargetName: targetNames,
Empty: []bool{false, true},
Encrypted: []bool{false, true},
Executable: []bool{false, true},
Expand All @@ -88,12 +127,8 @@ func TestFileAttr(t *testing.T) {
Private []bool
Template []bool
}{
Type: SourceFileTypeModify,
TargetName: []string{
".name",
"exact_name",
"name",
},
Type: SourceFileTypeModify,
TargetName: targetNames,
Executable: []bool{false, true},
Private: []bool{false, true},
Template: []bool{false, true},
Expand All @@ -104,25 +139,17 @@ func TestFileAttr(t *testing.T) {
Once []bool
Order []int
}{
Type: SourceFileTypeScript,
TargetName: []string{
".name",
"exact_name",
"name",
},
Once: []bool{false, true},
Order: []int{-1, 0, 1},
Type: SourceFileTypeScript,
TargetName: targetNames,
Once: []bool{false, true},
Order: []int{-1, 0, 1},
}))
require.NoError(t, combinator.Generate(&fas, struct {
Type SourceFileTargetType
TargetName []string
}{
Type: SourceFileTypeSymlink,
TargetName: []string{
".name",
"exact_name",
"name",
},
Type: SourceFileTypeSymlink,
TargetName: targetNames,
}))
for _, fa := range fas {
actualSourceName := fa.SourceName("")
Expand Down Expand Up @@ -154,3 +181,94 @@ func TestFileAttrEncryptedSuffix(t *testing.T) {
assert.Equal(t, tc.expectedTargetName, fa.TargetName)
}
}

func TestFileAttrLiteral(t *testing.T) {
for _, tc := range []struct {
sourceName string
encryptedSuffix string
fileAttr FileAttr
nonCanonical bool
}{
{
sourceName: "dot_file",
fileAttr: FileAttr{
TargetName: ".file",
Type: SourceFileTypeFile,
},
},
{
sourceName: "literal_dot_file",
fileAttr: FileAttr{
TargetName: "dot_file",
Type: SourceFileTypeFile,
},
},
{
sourceName: "literal_literal_file",
fileAttr: FileAttr{
TargetName: "literal_file",
Type: SourceFileTypeFile,
},
},
{
sourceName: "run_once_script",
fileAttr: FileAttr{
TargetName: "script",
Type: SourceFileTypeScript,
Once: true,
},
},
{
sourceName: "run_literal_once_script",
fileAttr: FileAttr{
TargetName: "once_script",
Type: SourceFileTypeScript,
},
},
{
sourceName: "file.literal",
fileAttr: FileAttr{
TargetName: "file",
Type: SourceFileTypeFile,
},
nonCanonical: true,
},
{
sourceName: "file.literal.literal",
fileAttr: FileAttr{
TargetName: "file.literal",
Type: SourceFileTypeFile,
},
},
{
sourceName: "file.tmpl",
fileAttr: FileAttr{
TargetName: "file",
Type: SourceFileTypeFile,
Template: true,
},
},
{
sourceName: "file.tmpl.literal",
fileAttr: FileAttr{
TargetName: "file.tmpl",
Type: SourceFileTypeFile,
},
},
{
sourceName: "file.tmpl.literal.tmpl",
fileAttr: FileAttr{
TargetName: "file.tmpl",
Type: SourceFileTypeFile,
Template: true,
},
},
} {
t.Run(tc.sourceName, func(t *testing.T) {
assert.Equal(t, tc.fileAttr, parseFileAttr(tc.sourceName, tc.encryptedSuffix))
if !tc.nonCanonical {
assert.Equal(t, tc.sourceName, tc.fileAttr.SourceName(tc.encryptedSuffix))
}
})
}
}
Loading

0 comments on commit 509bda7

Please sign in to comment.