Skip to content

Commit 29fedf0

Browse files
committed
safer output
1 parent cdbf7f4 commit 29fedf0

2 files changed

Lines changed: 204 additions & 11 deletions

File tree

main.go

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,69 @@ import (
55
"flag"
66
"fmt"
77
"io"
8+
"io/fs"
9+
"math/rand/v2"
810
"os"
911
"path/filepath"
1012
"syscall"
1113

1214
gitsemver "github.com/linkdata/gitsemver/internal/gitsemver"
1315
)
1416

15-
func writeOutput(fileName, content string) (err error) {
16-
f := os.Stdout
17-
if len(fileName) > 0 {
17+
func replaceFile(source, target string) (err error) {
18+
bakname := fmt.Sprintf("%s.gitsemver-%x", target, rand.Uint64()) // #nosec G404
19+
if renameerr := os.Rename(target, bakname); renameerr == nil || errors.Is(renameerr, fs.ErrNotExist) { // #nosec G703
20+
if err = os.Rename(source, target); err == nil { // #nosec G703
21+
if renameerr == nil {
22+
err = os.Remove(bakname) // #nosec G703
23+
}
24+
return
25+
}
26+
if renameerr == nil {
27+
_ = os.Rename(bakname, target)
28+
}
29+
} else {
30+
err = renameerr
31+
}
32+
return
33+
}
34+
35+
func prepareOutput(fileName, content string) (publish func() error, cleanup func(), err error) {
36+
cleanup = func() {}
37+
publish = func() error {
38+
_, err := os.Stdout.WriteString(content)
39+
return err
40+
}
41+
if fileName != "" {
1842
fileName = filepath.Clean(fileName)
19-
if f, err = os.Create(fileName); /* #nosec G703 */ err != nil /* #nosec G304 */ {
43+
if fi, statErr := os.Stat(fileName); statErr == nil {
44+
if fi.IsDir() {
45+
err = fmt.Errorf("%q is a directory", fileName)
46+
return
47+
}
48+
} else if !errors.Is(statErr, fs.ErrNotExist) {
49+
err = statErr
2050
return
2151
}
22-
defer f.Close()
52+
// File output is always staged: write to temp first, then publish by replace.
53+
var f *os.File
54+
if f, err = os.CreateTemp(filepath.Dir(fileName), filepath.Base(fileName)+".gitsemver-*"); err == nil {
55+
tempFile := f.Name()
56+
cleanup = func() {
57+
_ = os.Remove(tempFile) // #nosec G703
58+
}
59+
if _, err = f.WriteString(content); err == nil {
60+
if err = f.Close(); err == nil {
61+
publish = func() error {
62+
return replaceFile(tempFile, fileName)
63+
}
64+
return
65+
}
66+
}
67+
_ = f.Close()
68+
cleanup()
69+
}
2370
}
24-
_, err = f.WriteString(content)
2571
return
2672
}
2773

@@ -82,10 +128,15 @@ func mainfn() int {
82128
if !*flagNoNewline {
83129
content += "\n"
84130
}
85-
if err = vs.Git.CreateTag(repoDir, createTag); err == nil {
86-
if err = writeOutput(outpath, content); err == nil {
131+
var publish func() error
132+
var cleanup func()
133+
if publish, cleanup, err = prepareOutput(outpath, content); err == nil {
134+
defer cleanup()
135+
if err = vs.Git.CreateTag(repoDir, createTag); err == nil {
87136
if err = vs.Git.PushTag(repoDir, createTag); err == nil {
88-
return 0
137+
if err = publish(); err == nil {
138+
return 0
139+
}
89140
}
90141
}
91142
}

main_test.go

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ func TestMainFn(t *testing.T) {
3030
flag.Parse()
3131
*flagGoPackage = true
3232
*flagOut = "test.out"
33-
mainfn()
33+
if code := mainfn(); code != 0 {
34+
t.Fatalf("mainfn failed with code %d", code)
35+
}
3436
b, err := os.ReadFile("test.out")
3537
if err == nil {
3638
defer os.Remove("test.out")
@@ -48,7 +50,9 @@ func TestMainFnBranch(t *testing.T) {
4850
*flagBranch = true
4951
*flagOut = "test.out"
5052
*flagIncPatch = true
51-
mainfn()
53+
if code := mainfn(); code != 0 {
54+
t.Fatalf("mainfn failed with code %d", code)
55+
}
5256
b, err := os.ReadFile("test.out")
5357
if err == nil {
5458
defer os.Remove("test.out")
@@ -141,3 +145,141 @@ func TestMainFnIncPatchDoesNotPushOnWriteError(t *testing.T) {
141145
t.Fatalf("unexpected remote tag v1.0.1: %q", remoteTags)
142146
}
143147
}
148+
149+
func TestMainFnIncPatchDoesNotWriteOutputOnPushError(t *testing.T) {
150+
flag.Parse()
151+
oldWD, err := os.Getwd()
152+
if err != nil {
153+
t.Fatal(err)
154+
}
155+
defer func() { _ = os.Chdir(oldWD) }()
156+
157+
origGit, origOut, origName := *flagGit, *flagOut, *flagName
158+
origDebug, origGoPackage := *flagDebug, *flagGoPackage
159+
origNoFetch, origNoNewline := *flagNoFetch, *flagNoNewline
160+
origIncPatch, origBranch := *flagIncPatch, *flagBranch
161+
origTestMode := testMode
162+
defer func() {
163+
*flagGit, *flagOut, *flagName = origGit, origOut, origName
164+
*flagDebug, *flagGoPackage = origDebug, origGoPackage
165+
*flagNoFetch, *flagNoNewline = origNoFetch, origNoNewline
166+
*flagIncPatch, *flagBranch = origIncPatch, origBranch
167+
testMode = origTestMode
168+
}()
169+
170+
work := t.TempDir()
171+
runGit(t, work, "init", "-q")
172+
runGit(t, work, "config", "user.email", "test@example.com")
173+
runGit(t, work, "config", "user.name", "Test")
174+
if err := os.WriteFile(filepath.Join(work, "a.txt"), []byte("a\n"), 0o644); err != nil {
175+
t.Fatal(err)
176+
}
177+
runGit(t, work, "add", "a.txt")
178+
runGit(t, work, "commit", "-q", "-m", "c1")
179+
runGit(t, work, "tag", "v1.0.0")
180+
181+
if err := os.Chdir(work); err != nil {
182+
t.Fatal(err)
183+
}
184+
185+
*flagGit = "git"
186+
*flagOut = "out.txt"
187+
*flagName = ""
188+
*flagDebug = false
189+
*flagGoPackage = false
190+
*flagNoFetch = true
191+
*flagNoNewline = false
192+
*flagIncPatch = true
193+
*flagBranch = false
194+
testMode = false
195+
196+
if code := mainfn(); code == 0 {
197+
t.Fatal("mainfn unexpectedly succeeded")
198+
}
199+
200+
if _, err := os.Stat(filepath.Join(work, "out.txt")); err == nil {
201+
t.Fatal("unexpected output file out.txt")
202+
} else if !os.IsNotExist(err) {
203+
t.Fatal(err)
204+
}
205+
localTags := runGit(t, work, "tag", "--list")
206+
if strings.Contains(localTags, "v1.0.1") {
207+
t.Fatalf("unexpected local tag v1.0.1: %q", localTags)
208+
}
209+
}
210+
211+
func TestMainFnIncPatchOverwritesExistingOutputFile(t *testing.T) {
212+
flag.Parse()
213+
oldWD, err := os.Getwd()
214+
if err != nil {
215+
t.Fatal(err)
216+
}
217+
defer func() { _ = os.Chdir(oldWD) }()
218+
219+
origGit, origOut, origName := *flagGit, *flagOut, *flagName
220+
origDebug, origGoPackage := *flagDebug, *flagGoPackage
221+
origNoFetch, origNoNewline := *flagNoFetch, *flagNoNewline
222+
origIncPatch, origBranch := *flagIncPatch, *flagBranch
223+
origTestMode := testMode
224+
defer func() {
225+
*flagGit, *flagOut, *flagName = origGit, origOut, origName
226+
*flagDebug, *flagGoPackage = origDebug, origGoPackage
227+
*flagNoFetch, *flagNoNewline = origNoFetch, origNoNewline
228+
*flagIncPatch, *flagBranch = origIncPatch, origBranch
229+
testMode = origTestMode
230+
}()
231+
232+
base := t.TempDir()
233+
origin := filepath.Join(base, "origin.git")
234+
work := filepath.Join(base, "work")
235+
236+
runGit(t, "", "init", "--bare", "-q", origin)
237+
runGit(t, "", "clone", "-q", origin, work)
238+
runGit(t, work, "config", "user.email", "test@example.com")
239+
runGit(t, work, "config", "user.name", "Test")
240+
if err := os.WriteFile(filepath.Join(work, "a.txt"), []byte("a\n"), 0o644); err != nil {
241+
t.Fatal(err)
242+
}
243+
runGit(t, work, "add", "a.txt")
244+
runGit(t, work, "commit", "-q", "-m", "c1")
245+
runGit(t, work, "tag", "v1.0.0")
246+
runGit(t, work, "push", "-q", "origin", "HEAD", "--tags")
247+
248+
if err := os.WriteFile(filepath.Join(work, "out.txt"), []byte("old\n"), 0o644); err != nil {
249+
t.Fatal(err)
250+
}
251+
252+
if err := os.Chdir(work); err != nil {
253+
t.Fatal(err)
254+
}
255+
256+
*flagGit = "git"
257+
*flagOut = "out.txt"
258+
*flagName = ""
259+
*flagDebug = false
260+
*flagGoPackage = false
261+
*flagNoFetch = true
262+
*flagNoNewline = false
263+
*flagIncPatch = true
264+
*flagBranch = false
265+
testMode = false
266+
267+
if code := mainfn(); code != 0 {
268+
t.Fatalf("mainfn failed with code %d", code)
269+
}
270+
271+
out, err := os.ReadFile(filepath.Join(work, "out.txt"))
272+
if err != nil {
273+
t.Fatal(err)
274+
}
275+
if strings.Contains(string(out), "old") {
276+
t.Fatalf("expected out.txt to be replaced, got %q", string(out))
277+
}
278+
if !strings.Contains(string(out), "v1.0.1") {
279+
t.Fatalf("expected out.txt to contain new version, got %q", string(out))
280+
}
281+
remoteTags := runGit(t, work, "ls-remote", "--tags", "origin")
282+
if !strings.Contains(remoteTags, "refs/tags/v1.0.1") {
283+
t.Fatalf("expected remote tag v1.0.1, got %q", remoteTags)
284+
}
285+
}

0 commit comments

Comments
 (0)