Skip to content

Commit 0f67f88

Browse files
committed
osfs: Rename() to read-only file on Windows
Unlike Linux, Windows os.Rename() fails if the destination file is read-only. Therefore, in this commit, we make the read-only file writable, then os.Rename() it, and restore the original permissions. This fixes a problem in go-git/go-git where Worktree.Add() always fails on repositories checked out with the git command on Windows because the object files are read-only. Signed-off-by: MURAOKA Taro <koron.kaoriya@gmail.com>
1 parent 37866f8 commit 0f67f88

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

osfs/os_chroot_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/go-git/go-billy/v6"
12+
"github.com/go-git/go-billy/v6/util"
1213
"github.com/stretchr/testify/assert"
1314
"github.com/stretchr/testify/require"
1415
)
@@ -68,3 +69,48 @@ func TestCreateWithChroot(t *testing.T) {
6869
t, expected, actual, "Permission mismatch - expected: 0o%o, actual: 0o%o", expected, actual,
6970
)
7071
}
72+
73+
// Verify that the Rename() is successful even if the destination is a
74+
// read-only file.
75+
func TestRenameToReadonly(t *testing.T) {
76+
fs, _ := setup(t)
77+
chroot, _ := fs.Chroot("rename")
78+
79+
// Prepare two files: rename source and destination
80+
err := util.WriteFile(chroot, "src.txt", []byte("hello"), 0644)
81+
if err != nil {
82+
t.Fatalf("failed to write src.txt: %s", err)
83+
}
84+
err = util.WriteFile(chroot, "dst.txt", []byte("world"), 0444)
85+
if err != nil {
86+
t.Fatalf("failed to write dst.txt: %s", err)
87+
}
88+
89+
err = chroot.Rename("src.txt", "dst.txt")
90+
if err != nil {
91+
t.Fatalf("failed to rename to overwrite read-only file: %s", err)
92+
}
93+
94+
// src.txt must not exist
95+
_, err = chroot.Stat("src.txt")
96+
if err == nil {
97+
t.Error("src.txt must not exist, but does it")
98+
} else if !os.IsNotExist(err) {
99+
t.Errorf("unexpected error on src.txt: %s", err)
100+
}
101+
102+
// Check dst.txt's permission and contents.
103+
fi, err := chroot.Stat("dst.txt")
104+
if err != nil {
105+
t.Errorf("unexpected error on dst.txt: %s", err)
106+
}
107+
if perm := fi.Mode().Perm(); perm != 0444 {
108+
t.Errorf("unexpected permission of dst.txt: %04o", perm)
109+
}
110+
b, err := util.ReadFile(chroot, "dst.txt")
111+
if err != nil {
112+
t.Errorf("failed to read dst.txt: %s", err)
113+
} else if string(b) != "hello" {
114+
t.Errorf("unexpected contents of dst.txt: want=%q got=%q", "hello", string(b))
115+
}
116+
}

osfs/os_windows.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package osfs
44

55
import (
6+
"io/fs"
67
"os"
78
"runtime"
89
"unsafe"
@@ -52,7 +53,35 @@ func (f *file) Sync() error {
5253
}
5354

5455
func rename(from, to string) error {
55-
return os.Rename(from, to)
56+
// On Windows, os.Rename() fails when a read-only file is specified as the
57+
// destination. (On Linux, for example, it succeeds even if the file is
58+
// read-only if you have write permission in the parent directory.)
59+
// Therefore, for read-only files, we must first change the permissions to
60+
// allow writing, then rename them with os.Rename(), and then restore their
61+
// original permissions.
62+
var (
63+
modeChanged bool
64+
originalMode fs.FileMode
65+
)
66+
if fi, err := os.Stat(to); err == nil {
67+
originalMode = fi.Mode()
68+
if originalMode&0200 == 0 {
69+
err := os.Chmod(to, originalMode|0200)
70+
if err != nil {
71+
return err
72+
}
73+
modeChanged = true
74+
}
75+
}
76+
err := os.Rename(from, to)
77+
if err != nil {
78+
return err
79+
}
80+
// If we changed permissions, change them back
81+
if modeChanged {
82+
return os.Chmod(to, originalMode)
83+
}
84+
return nil
5685
}
5786

5887
func umask(_ int) func() {

0 commit comments

Comments
 (0)