Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/pages/configuration/dev/connections/file-sync.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,12 @@ The `initialSync` option expects a string with an initial sync strategy. The fol

`disabled` disabled the initial sync completely

:::caution Remote-preferred conflicts
When resolving an initial-sync conflict, `mirrorRemote` and `preferRemote` replace the local file even if its last-modified timestamp is newer than the container file's timestamp. Save any local changes you want to keep before starting the sync. Use `preferNewest` to resolve conflicts by modification time instead.

This remote preference applies only to initial sync. Subsequent downloads retain the normal protection against overwriting newer local edits.
:::

```yaml
deployments:
app-backend:
Expand Down
105 changes: 105 additions & 0 deletions pkg/devspace/sync/initial_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package sync

import (
"context"
"net"
"os"
"path/filepath"
"testing"
"time"

"github.com/loft-sh/devspace/helper/remote"
"github.com/loft-sh/devspace/helper/server"
"github.com/loft-sh/devspace/pkg/devspace/config/versions/latest"
"github.com/loft-sh/devspace/pkg/util/log"
)

func TestInitialSyncRemotePreference(t *testing.T) {
for _, testCase := range []struct {
strategy latest.InitialSyncStrategy
want string
}{
{latest.InitialSyncStrategyMirrorRemote, "remote"},
{latest.InitialSyncStrategyPreferRemote, "remote"},
{latest.InitialSyncStrategyPreferNewest, "local"},
{latest.InitialSyncStrategyMirrorLocal, "local"},
{latest.InitialSyncStrategyPreferLocal, "local"},
{latest.InitialSyncStrategyKeepAll, "local"},
{latest.InitialSyncStrategyDisabled, "local"},
} {
t.Run(string(testCase.strategy), func(t *testing.T) {
local, remotePath := t.TempDir(), t.TempDir()
localFile := filepath.Join(local, "conflict.txt")
remoteFile := filepath.Join(remotePath, "conflict.txt")
writeFile := func(filename, content string, modified time.Time) {
t.Helper()
if err := os.WriteFile(filename, []byte(content), 0644); err != nil {
t.Fatal(err)
}
if err := os.Chtimes(filename, modified, modified); err != nil {
t.Fatal(err)
}
}
writeFile(localFile, "local", time.Unix(2000000000, 0))
writeFile(remoteFile, "remote", time.Unix(1700000000, 0))

client, err := NewSync(context.Background(), local, Options{
InitialSync: testCase.strategy,
UpstreamDisabled: true,
Log: log.Discard,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { client.Stop(nil) })
// Download-only sync still marks the upstream initial-sync state complete.
client.upstream = &upstream{}

clientConn, serverConn := net.Pipe()
t.Cleanup(func() {
_ = clientConn.Close()
_ = serverConn.Close()
})
go func() {
if err := server.StartDownstreamServer(serverConn, serverConn, &server.DownstreamOptions{
RemotePath: remotePath,
Polling: true,
}); err != nil {
panic(err)
}
}()
if err := client.InitDownstream(clientConn, clientConn); err != nil {
t.Fatal(err)
}
uploadDone := make(chan struct{})
if err := client.initialSync(uploadDone, nil); err != nil {
t.Fatal(err)
}
<-uploadDone
content, err := os.ReadFile(localFile)
if err != nil {
t.Fatal(err)
}
if string(content) != testCase.want {
t.Errorf("initial sync: got %q, want %q", content, testCase.want)
}

// Later downloads must continue to protect a newer, pending local edit.
writeFile(localFile, "pending local edit", time.Unix(2000000000, 0))
writeFile(remoteFile, "later remote edit", time.Unix(1700000001, 0))
if err := client.downstream.applyChanges([]*remote.Change{{
ChangeType: remote.ChangeType_CHANGE,
Path: "/conflict.txt",
}}, false); err != nil {
t.Fatal(err)
}
content, err = os.ReadFile(localFile)
if err != nil {
t.Fatal(err)
}
if string(content) != "pending local edit" {
t.Errorf("normal sync overwrote a newer local edit: %q", content)
}
})
}
}
6 changes: 6 additions & 0 deletions pkg/devspace/sync/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ func (s *Sync) startDownstream() {
}

func (s *Sync) initialSync(onInitUploadDone chan struct{}, onInitDownloadDone chan struct{}) error {
// Remote preference overrides mtimes only during initial sync, not while watching.
s.downstream.unarchiver.forceOverride = s.Options.InitialSync == latest.InitialSyncStrategyMirrorRemote || s.Options.InitialSync == latest.InitialSyncStrategyPreferRemote
defer func() {
s.downstream.unarchiver.forceOverride = false
}()

initialSync := newInitialSyncer(&initialSyncOptions{
LocalPath: s.LocalPath,
Strategy: s.Options.InitialSync,
Expand Down