Skip to content

Commit fb9961a

Browse files
NoamTDmvdan
authored andcommitted
cue/cmd: avoid overwriting formatted files in cue fmt
Currently, `cue fmt` will always write to cue files, regardless of whether they are formatted or not. In the case of formatted files, their contents will remain exactly the same, but their modification time will change. This is problematic for tools that rely on the mod time. To fix this, we buffer the original/formatted bytes. If the result is exactly the same, the file is not written to. Fixes #1731, #1744 Signed-off-by: Noam Dolovich <[email protected]> Change-Id: I67e7fc80b0e108a2cb6c7bfb4090e29bd47cb995 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1193934 TryBot-Result: CUEcueckoo <[email protected]> Unity-Result: CUE porcuepine <[email protected]> Reviewed-by: Daniel Martí <[email protected]>
1 parent fcae10b commit fb9961a

File tree

4 files changed

+70
-23
lines changed

4 files changed

+70
-23
lines changed

cmd/cue/cmd/fmt.go

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,19 @@ func newFmtCmd(c *Command) *cobra.Command {
8585
continue
8686
}
8787

88-
// When using --check and --diff, we need to buffer the input and output bytes to compare them.
88+
// We buffer the input and output bytes to compare them.
89+
// This allows us to determine whether a file is already
90+
// formatted, without modifying the file.
8991
var original []byte
9092
var formatted bytes.Buffer
91-
if doDiff || check {
92-
if bs, ok := file.Source.([]byte); ok {
93-
original = bs
94-
} else {
95-
original, err = source.ReadAll(file.Filename, file.Source)
96-
exitOnErr(cmd, err, true)
97-
file.Source = original
98-
}
99-
cfg.Out = &formatted
93+
if bs, ok := file.Source.([]byte); ok {
94+
original = bs
95+
} else {
96+
original, err = source.ReadAll(file.Filename, file.Source)
97+
exitOnErr(cmd, err, true)
98+
file.Source = original
10099
}
100+
cfg.Out = &formatted
101101

102102
var files []*ast.File
103103
d := encoding.NewDecoder(cmd.ctx, file, &cfg)
@@ -130,20 +130,32 @@ func newFmtCmd(c *Command) *cobra.Command {
130130
exitOnErr(cmd, err, true)
131131
}
132132

133-
if (doDiff || check) && !bytes.Equal(formatted.Bytes(), original) {
134-
foundBadlyFormatted = true
135-
var path string
136-
f := file.Filename
137-
path, err = filepath.Rel(cwd, f)
138-
if err != nil {
139-
path = f
140-
}
133+
// File is already well formatted; we can stop here.
134+
if bytes.Equal(formatted.Bytes(), original) {
135+
continue
136+
}
141137

142-
if doDiff {
143-
d := diff.Diff(path+".orig", original, path, formatted.Bytes())
144-
fmt.Fprintln(stdout, string(d))
145-
} else {
146-
fmt.Fprintln(stdout, path)
138+
foundBadlyFormatted = true
139+
var path string
140+
f := file.Filename
141+
path, err = filepath.Rel(cwd, f)
142+
if err != nil {
143+
path = f
144+
}
145+
146+
switch {
147+
case doDiff:
148+
d := diff.Diff(path+".orig", original, path, formatted.Bytes())
149+
fmt.Fprintln(stdout, string(d))
150+
case check:
151+
fmt.Fprintln(stdout, path)
152+
case file.Filename == "-":
153+
if _, err := fmt.Fprint(stdout, formatted.String()); err != nil {
154+
exitOnErr(cmd, err, false)
155+
}
156+
default:
157+
if err := os.WriteFile(file.Filename, formatted.Bytes(), 0644); err != nil {
158+
exitOnErr(cmd, err, false)
147159
}
148160
}
149161
}

cmd/cue/cmd/script_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ func TestScript(t *testing.T) {
116116
ts.Check(os.WriteFile(path, []byte(data), 0o666))
117117
}
118118
},
119+
// mod-time prints the modification time of a file to stdout.
120+
// The time is displayed as nanoseconds since the Unix epoch.
121+
"mod-time": func(ts *testscript.TestScript, neg bool, args []string) {
122+
if neg || len(args) != 1 {
123+
ts.Fatalf("usage: mod-time PATH")
124+
}
125+
path := ts.MkAbs(args[0])
126+
fi, err := os.Stat(path)
127+
ts.Check(err)
128+
_, err = fmt.Fprint(ts.Stdout(), fi.ModTime().UnixNano())
129+
ts.Check(err)
130+
},
119131
// get-manifest writes the manifest for a given reference within an OCI
120132
// registry to a file in JSON format.
121133
"get-manifest": func(ts *testscript.TestScript, neg bool, args []string) {

cmd/cue/cmd/testdata/script/fmt.txtar

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
! exec cue fmt ./fmt
22
cmp stderr expect-stderr
3+
4+
# Issue 1744: if we encounter an error while formatting,
5+
# don't overwrite the input file with zero bytes.
6+
cp issue1744/invalid.cue issue1744/invalid.cue.orig
7+
! exec cue fmt issue1744/invalid.cue
8+
cmp stderr issue1744/stderr-golden
9+
cmp issue1744/invalid.cue issue1744/invalid.cue.orig
10+
311
-- expect-stderr --
412
expected 'STRING', found '.':
513
./fmt/error.cue:1:9
@@ -9,3 +17,8 @@ import a.b "foo"
917
a: 2
1018
bb: 3
1119
-- fmt/cue.mod/module.cue --
20+
-- issue1744/invalid.cue --
21+
~
22+
-- issue1744/stderr-golden --
23+
illegal character U+007E '~':
24+
./issue1744/invalid.cue:1:1
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Verify that cue fmt does not write to files
2+
# if they are already well formatted
3+
mod-time formatted.cue
4+
cp stdout mod-time.txt
5+
exec cue fmt formatted.cue
6+
mod-time formatted.cue
7+
cmp stdout mod-time.txt
8+
9+
-- formatted.cue --
10+
a: 1

0 commit comments

Comments
 (0)