Skip to content

Commit 6bd0f8b

Browse files
author
chientrinh
committed
Fix initial watch sync for existing files
1 parent d518da2 commit 6bd0f8b

2 files changed

Lines changed: 70 additions & 13 deletions

File tree

pkg/compose/watch.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -768,10 +768,6 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr
768768
if err != nil {
769769
return nil, err
770770
}
771-
timeImageCreated, err := s.imageCreatedTime(ctx, project, service.Name)
772-
if err != nil {
773-
return nil, err
774-
}
775771
var pathsToCopy []*sync.PathMapping
776772
switch mode := fi.Mode(); {
777773
case mode.IsDir():
@@ -793,15 +789,7 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr
793789
}
794790
return nil // skip file
795791
}
796-
info, err := d.Info()
797-
if err != nil {
798-
return err
799-
}
800792
if !d.IsDir() {
801-
if info.ModTime().Before(timeImageCreated) {
802-
// skip file if it was modified before image creation
803-
return nil
804-
}
805793
rel, err := filepath.Rel(trigger.Path, path)
806794
if err != nil {
807795
return err
@@ -816,7 +804,7 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr
816804
})
817805
case mode.IsRegular():
818806
// process file
819-
if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
807+
if !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
820808
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
821809
HostPath: trigger.Path,
822810
ContainerPath: trigger.Target,

pkg/compose/watch_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"context"
2020
"fmt"
2121
"os"
22+
"path/filepath"
2223
"slices"
2324
"testing"
2425
"time"
@@ -194,3 +195,71 @@ func (f *fakeSyncer) Sync(ctx context.Context, service string, paths []*sync.Pat
194195
f.synced <- paths
195196
return nil
196197
}
198+
199+
func TestInitialSyncFiles_DirectoryIncludesExistingFilesEvenIfOlderThanImage(t *testing.T) {
200+
mockCtrl := gomock.NewController(t)
201+
cli := mocks.NewMockCli(mockCtrl)
202+
cli.EXPECT().Err().Return(streams.NewOut(os.Stderr)).AnyTimes()
203+
apiClient := mocks.NewMockAPIClient(mockCtrl)
204+
cli.EXPECT().Client().Return(apiClient).AnyTimes()
205+
206+
dir := t.TempDir()
207+
hostDir := filepath.Join(dir, "src")
208+
assert.NilError(t, os.MkdirAll(hostDir, 0o755))
209+
hostFile := filepath.Join(hostDir, "test.txt")
210+
assert.NilError(t, os.WriteFile(hostFile, []byte("hello"), 0o644))
211+
212+
fileTime := time.Date(2026, 4, 1, 10, 0, 0, 0, time.UTC)
213+
assert.NilError(t, os.Chtimes(hostFile, fileTime, fileTime))
214+
215+
service := composeService{dockerCli: cli}
216+
project := &types.Project{Name: testProject}
217+
trigger := types.Trigger{
218+
Path: hostDir,
219+
Action: types.WatchActionSync,
220+
Target: "/app/src",
221+
InitialSync: true,
222+
}
223+
224+
got, err := service.initialSyncFiles(t.Context(), project, types.ServiceConfig{Name: "test"}, trigger, watch.EmptyMatcher{})
225+
assert.NilError(t, err)
226+
227+
expected := []*sync.PathMapping{{
228+
HostPath: hostFile,
229+
ContainerPath: "/app/src/test.txt",
230+
}}
231+
assert.DeepEqual(t, expected, got)
232+
}
233+
234+
func TestInitialSyncFiles_FileIncludesExistingFileEvenIfOlderThanImage(t *testing.T) {
235+
mockCtrl := gomock.NewController(t)
236+
cli := mocks.NewMockCli(mockCtrl)
237+
cli.EXPECT().Err().Return(streams.NewOut(os.Stderr)).AnyTimes()
238+
apiClient := mocks.NewMockAPIClient(mockCtrl)
239+
cli.EXPECT().Client().Return(apiClient).AnyTimes()
240+
241+
dir := t.TempDir()
242+
hostFile := filepath.Join(dir, "test.txt")
243+
assert.NilError(t, os.WriteFile(hostFile, []byte("hello"), 0o644))
244+
245+
fileTime := time.Date(2026, 4, 1, 10, 0, 0, 0, time.UTC)
246+
assert.NilError(t, os.Chtimes(hostFile, fileTime, fileTime))
247+
248+
service := composeService{dockerCli: cli}
249+
project := &types.Project{Name: testProject}
250+
trigger := types.Trigger{
251+
Path: hostFile,
252+
Action: types.WatchActionSync,
253+
Target: "/app/test.txt",
254+
InitialSync: true,
255+
}
256+
257+
got, err := service.initialSyncFiles(t.Context(), project, types.ServiceConfig{Name: "test"}, trigger, watch.EmptyMatcher{})
258+
assert.NilError(t, err)
259+
260+
expected := []*sync.PathMapping{{
261+
HostPath: hostFile,
262+
ContainerPath: "/app/test.txt",
263+
}}
264+
assert.DeepEqual(t, expected, got)
265+
}

0 commit comments

Comments
 (0)