Skip to content

Commit 45ae193

Browse files
authored
Merge pull request #1944 from go-git/fix-perms
dotgit: skip writing pack files that already exist on disk
2 parents 2212dc7 + fda4f74 commit 45ae193

2 files changed

Lines changed: 134 additions & 12 deletions

File tree

storage/filesystem/dotgit/writers.go

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -132,30 +132,64 @@ func (w *PackWriter) clean() error {
132132

133133
func (w *PackWriter) save() error {
134134
base := w.fs.Join(objectsPath, packPath, fmt.Sprintf("pack-%s", w.checksum))
135-
idx, err := w.fs.Create(fmt.Sprintf("%s.idx", base))
135+
136+
// Pack files are content addressable. Each file is checked
137+
// individually — if it already exists on disk, skip creating it.
138+
idxPath := fmt.Sprintf("%s.idx", base)
139+
exists, err := fileExists(w.fs, idxPath)
136140
if err != nil {
137141
return err
138142
}
143+
if !exists {
144+
idx, err := w.fs.Create(idxPath)
145+
if err != nil {
146+
return err
147+
}
139148

140-
if err := w.encodeIdx(idx); err != nil {
141-
_ = idx.Close()
142-
return err
143-
}
149+
if err := w.encodeIdx(idx); err != nil {
150+
_ = idx.Close()
151+
return err
152+
}
144153

145-
if err := idx.Close(); err != nil {
146-
return err
154+
if err := idx.Close(); err != nil {
155+
return err
156+
}
157+
fixPermissions(w.fs, idxPath)
147158
}
148-
fixPermissions(w.fs, fmt.Sprintf("%s.idx", base))
149159

150160
packPath := fmt.Sprintf("%s.pack", base)
151-
if err := w.fs.Rename(w.fw.Name(), packPath); err != nil {
161+
exists, err = fileExists(w.fs, packPath)
162+
if err != nil {
152163
return err
153164
}
154-
fixPermissions(w.fs, packPath)
165+
if !exists {
166+
if err := w.fs.Rename(w.fw.Name(), packPath); err != nil {
167+
return err
168+
}
169+
fixPermissions(w.fs, packPath)
170+
} else {
171+
// Pack already exists, clean up the temp file.
172+
return w.clean()
173+
}
155174

156175
return nil
157176
}
158177

178+
// fileExists checks whether path already exists as a regular file.
179+
// It returns (true, nil) for an existing regular file, (false, nil) when the
180+
// path does not exist, and (false, err) if the path exists but is not a
181+
// regular file (e.g. a directory or symlink).
182+
func fileExists(fs billy.Filesystem, path string) (bool, error) {
183+
fi, err := fs.Lstat(path)
184+
if err != nil {
185+
return false, nil
186+
}
187+
if !fi.Mode().IsRegular() {
188+
return false, fmt.Errorf("unexpected file type for %q: %s", path, fi.Mode().Type())
189+
}
190+
return true, nil
191+
}
192+
159193
func (w *PackWriter) encodeIdx(writer io.Writer) error {
160194
idx, err := w.writer.Index()
161195
if err != nil {
@@ -235,7 +269,6 @@ func (s *syncedReader) sleep() {
235269
atomic.StoreUint32(&s.blocked, 1)
236270
<-s.news
237271
}
238-
239272
}
240273

241274
func (s *syncedReader) Seek(offset int64, whence int) (int64, error) {
@@ -293,7 +326,7 @@ func (w *ObjectWriter) save() error {
293326
// Loose objects are content addressable, if they already exist
294327
// we can safely delete the temporary file and short-circuit the
295328
// operation.
296-
if _, err := w.fs.Stat(file); err == nil || os.IsExist(err) {
329+
if _, err := w.fs.Lstat(file); err == nil || os.IsExist(err) {
297330
return w.fs.Remove(w.f.Name())
298331
}
299332

storage/filesystem/dotgit/writers_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,95 @@ func TestPackWriterPermissions(t *testing.T) {
181181
}
182182
}
183183

184+
func TestPackWriterExistingReadOnly(t *testing.T) {
185+
t.Parallel()
186+
187+
for _, tc := range []struct {
188+
name string
189+
fs billy.Filesystem
190+
}{
191+
{"BoundOS", osfs.New(t.TempDir(), osfs.WithBoundOS())},
192+
{"ChrootOS", osfs.New(t.TempDir(), osfs.WithChrootOS())},
193+
} {
194+
t.Run(tc.name, func(t *testing.T) {
195+
t.Parallel()
196+
197+
f := fixtures.Basic().One()
198+
199+
dot := New(tc.fs)
200+
require.NoError(t, dot.Initialize())
201+
202+
pfPath := filepath.Join("objects", "pack", fmt.Sprintf("pack-%s.pack", f.PackfileHash))
203+
idxPath := filepath.Join("objects", "pack", fmt.Sprintf("pack-%s.idx", f.PackfileHash))
204+
205+
writePack := func() {
206+
t.Helper()
207+
w, err := dot.NewObjectPack()
208+
require.NoError(t, err)
209+
210+
_, err = io.Copy(w, f.Packfile())
211+
require.NoError(t, err)
212+
require.NoError(t, w.Close())
213+
}
214+
215+
writePack()
216+
217+
ro, err := isReadOnly(tc.fs, pfPath)
218+
require.NoError(t, err)
219+
assert.True(t, ro, "file %q is not read-only", pfPath)
220+
221+
ro, err = isReadOnly(tc.fs, idxPath)
222+
require.NoError(t, err)
223+
assert.True(t, ro, "file %q is not read-only", idxPath)
224+
225+
writePack()
226+
227+
// Remove .idx only, keep .pack — the next write must
228+
// recreate .idx without touching the existing .pack.
229+
require.NoError(t, tc.fs.Remove(idxPath))
230+
writePack()
231+
232+
_, err = tc.fs.Lstat(idxPath)
233+
require.NoError(t, err, ".idx should have been recreated")
234+
235+
ro, err = isReadOnly(tc.fs, idxPath)
236+
require.NoError(t, err)
237+
assert.True(t, ro, "recreated %q is not read-only", idxPath)
238+
})
239+
}
240+
}
241+
242+
func TestPackWriterRejectsNonRegularFile(t *testing.T) {
243+
t.Parallel()
244+
245+
for _, ext := range []string{".idx", ".pack"} {
246+
t.Run(ext, func(t *testing.T) {
247+
t.Parallel()
248+
249+
f := fixtures.Basic().One()
250+
fs := osfs.New(t.TempDir(), osfs.WithBoundOS())
251+
252+
dot := New(fs)
253+
require.NoError(t, dot.Initialize())
254+
255+
// Place a directory where the pack file should go.
256+
path := filepath.Join("objects", "pack",
257+
fmt.Sprintf("pack-%s%s", f.PackfileHash, ext))
258+
require.NoError(t, fs.MkdirAll(path, 0o755))
259+
260+
w, err := dot.NewObjectPack()
261+
require.NoError(t, err)
262+
263+
_, err = io.Copy(w, f.Packfile())
264+
require.NoError(t, err)
265+
266+
err = w.Close()
267+
require.Error(t, err)
268+
assert.Contains(t, err.Error(), "unexpected file type")
269+
})
270+
}
271+
}
272+
184273
func TestObjectWriterPermissions(t *testing.T) {
185274
t.Parallel()
186275

0 commit comments

Comments
 (0)