From 26f80ae782aa7422ab8412385fb6aadd88b8381b Mon Sep 17 00:00:00 2001 From: AdamMagued Date: Mon, 5 Oct 2026 07:21:03 +0000 Subject: [PATCH] sync: repeat folder name on initial sync complete (#3309) --- pkg/devspace/services/sync/controller.go | 15 ++- pkg/devspace/services/sync/controller_test.go | 34 +++++++ pkg/devspace/sync/sync.go | 29 +++++- pkg/devspace/sync/sync_completion_test.go | 99 +++++++++++++++++++ 4 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 pkg/devspace/sync/sync_completion_test.go diff --git a/pkg/devspace/services/sync/controller.go b/pkg/devspace/services/sync/controller.go index 12ab892802..ba6eb9394c 100644 --- a/pkg/devspace/services/sync/controller.go +++ b/pkg/devspace/services/sync/controller.go @@ -122,7 +122,7 @@ func (c *controller) startWithWait(ctx devspacecontext.Context, options *Options // should wait for initial sync? if options.SyncConfig.WaitInitialSync == nil || *options.SyncConfig.WaitInitialSync { ctx.Log().Info("Waiting for initial sync to complete") - defer ctx.Log().Info("Initial sync completed") + defer ctx.Log().Info(InitialSyncCompletedMessage(options.SyncConfig.Path)) var ( uploadDone = false downloadDone = false @@ -320,6 +320,18 @@ func ParseSyncPath(path string) (localPath string, remotePath string, err error) return splitted[0], splitted[1], nil } +// InitialSyncCompletedMessage returns the message logged when initial sync completes +func InitialSyncCompletedMessage(path string) string { + localPath, remotePath, err := ParseSyncPath(path) + if err == nil { + return fmt.Sprintf("Initial sync completed for %s <-> %s", localPath, remotePath) + } + if path != "" { + return fmt.Sprintf("Initial sync completed for %s", path) + } + return "Initial sync completed" +} + func (c *controller) initClient(ctx devspacecontext.Context, pod *v1.Pod, arch, container string, syncConfig *latest.SyncConfig, starter sync.DelayedContainerStarter, verbose bool, customLog logpkg.Logger) (*sync.Sync, error) { localPath, containerPath, err := ParseSyncPath(syncConfig.Path) if err != nil { @@ -345,6 +357,7 @@ func (c *controller) initClient(ctx devspacecontext.Context, pod *v1.Pod, arch, Log: customLog, Polling: syncConfig.Polling, Starter: starter, + RemotePath: containerPath, ResolveCommand: func(command string, args []string) (string, []string, error) { return hook.ResolveCommand(ctx.Context(), command, args, ctx.WorkingDir(), ctx.Config(), ctx.Dependencies()) }, diff --git a/pkg/devspace/services/sync/controller_test.go b/pkg/devspace/services/sync/controller_test.go index d6b5091f9d..f04f2de604 100644 --- a/pkg/devspace/services/sync/controller_test.go +++ b/pkg/devspace/services/sync/controller_test.go @@ -30,3 +30,37 @@ func TestParseSyncPath(t *testing.T) { assert.Equal(t, remote, testCase.expectedRemote, "Expect remote path in "+testCase.name) } } + +func TestInitialSyncCompletedMessage(t *testing.T) { + testCases := []struct { + name string + in string + expected string + }{ + { + name: "empty path defaults to dot", + in: "", + expected: "Initial sync completed for . <-> .", + }, + { + name: "relative directory pair", + in: "../foo:/src/projects/foo", + expected: "Initial sync completed for ../foo <-> /src/projects/foo", + }, + { + name: "single path matching issue 3309", + in: "/projects/qux", + expected: "Initial sync completed for /projects/qux <-> /projects/qux", + }, + { + name: "windows path with colon", + in: "C:/codeproject:/home/dev/codeproject", + expected: "Initial sync completed for C:/codeproject <-> /home/dev/codeproject", + }, + } + + for _, tc := range testCases { + msg := InitialSyncCompletedMessage(tc.in) + assert.Equal(t, msg, tc.expected, tc.name) + } +} diff --git a/pkg/devspace/sync/sync.go b/pkg/devspace/sync/sync.go index 251021ffec..4f2f40d0ac 100644 --- a/pkg/devspace/sync/sync.go +++ b/pkg/devspace/sync/sync.go @@ -2,6 +2,7 @@ package sync import ( "context" + "fmt" "io" "os" "path/filepath" @@ -45,6 +46,7 @@ type Options struct { UpstreamLimit int64 DownstreamLimit int64 Verbose bool + RemotePath string UpstreamDisabled bool DownstreamDisabled bool @@ -135,6 +137,29 @@ func NewSync(ctx context.Context, localPath string, options Options) (*Sync, err return s, nil } +// TargetPath returns the target path context for sync logging +func (s *Sync) TargetPath() string { + if s.LocalPath != "" && s.Options.RemotePath != "" { + return fmt.Sprintf("%s <-> %s", s.LocalPath, s.Options.RemotePath) + } + if s.LocalPath != "" { + return s.LocalPath + } + if s.Options.RemotePath != "" { + return s.Options.RemotePath + } + return "" +} + +// InitialSyncCompletedMessage returns the message logged when initial sync completes +func (s *Sync) InitialSyncCompletedMessage(prefix string) string { + target := s.TargetPath() + if target != "" { + return fmt.Sprintf("%s - Initial sync completed for %s", prefix, target) + } + return fmt.Sprintf("%s - Initial sync completed", prefix) +} + // Error handles a sync error func (s *Sync) Error(err error) { s.log.Errorf("Sync Error on %s: %v", s.LocalPath, err) @@ -316,7 +341,7 @@ func (s *Sync) initialSync(onInitUploadDone chan struct{}, onInitDownloadDone ch if s.Options.InitialSync == latest.InitialSyncStrategyDisabled { s.log.Info("Upstream - Initial sync disabled") } else { - s.log.Info("Upstream - Initial sync completed") + s.log.Info(s.InitialSyncCompletedMessage("Upstream")) } close(onInitUploadDone) } @@ -326,7 +351,7 @@ func (s *Sync) initialSync(onInitUploadDone chan struct{}, onInitDownloadDone ch if s.Options.InitialSync == latest.InitialSyncStrategyDisabled { s.log.Info("Downstream - Initial sync disabled") } else { - s.log.Info("Downstream - Initial sync completed") + s.log.Info(s.InitialSyncCompletedMessage("Downstream")) } close(onInitDownloadDone) } diff --git a/pkg/devspace/sync/sync_completion_test.go b/pkg/devspace/sync/sync_completion_test.go new file mode 100644 index 0000000000..81f5f0ea83 --- /dev/null +++ b/pkg/devspace/sync/sync_completion_test.go @@ -0,0 +1,99 @@ +package sync + +import ( + "bytes" + "strings" + "testing" + + "github.com/loft-sh/devspace/pkg/util/log" + "github.com/sirupsen/logrus" + "gotest.tools/assert" +) + +func TestInitialSyncCompletedMessage(t *testing.T) { + testCases := []struct { + name string + localPath string + remotePath string + prefix string + expectedTarget string + expectedLogMessage string + }{ + { + name: "both local and remote path set", + localPath: "/workspace/project", + remotePath: "/app", + prefix: "Upstream", + expectedTarget: "/workspace/project <-> /app", + expectedLogMessage: "Upstream - Initial sync completed for /workspace/project <-> /app", + }, + { + name: "downstream with both paths set", + localPath: "/workspace/project", + remotePath: "/app", + prefix: "Downstream", + expectedTarget: "/workspace/project <-> /app", + expectedLogMessage: "Downstream - Initial sync completed for /workspace/project <-> /app", + }, + { + name: "only local path set", + localPath: "/workspace/project", + remotePath: "", + prefix: "Upstream", + expectedTarget: "/workspace/project", + expectedLogMessage: "Upstream - Initial sync completed for /workspace/project", + }, + { + name: "only remote path set", + localPath: "", + remotePath: "/app", + prefix: "Upstream", + expectedTarget: "/app", + expectedLogMessage: "Upstream - Initial sync completed for /app", + }, + { + name: "neither path set", + localPath: "", + remotePath: "", + prefix: "Upstream", + expectedTarget: "", + expectedLogMessage: "Upstream - Initial sync completed", + }, + } + + for _, tc := range testCases { + s := &Sync{ + LocalPath: tc.localPath, + Options: Options{ + RemotePath: tc.remotePath, + }, + } + + assert.Equal(t, s.TargetPath(), tc.expectedTarget, tc.name) + assert.Equal(t, s.InitialSyncCompletedMessage(tc.prefix), tc.expectedLogMessage, tc.name) + } +} + +func TestInitialSyncLoggingWithTargetContext(t *testing.T) { + var buf bytes.Buffer + testLogger := log.NewStreamLogger(&buf, &buf, logrus.InfoLevel) + + s := &Sync{ + LocalPath: "/workspace/frontend", + Options: Options{ + RemotePath: "/src/frontend", + Log: testLogger, + }, + log: testLogger, + } + + upstreamMsg := s.InitialSyncCompletedMessage("Upstream") + s.log.Info(upstreamMsg) + + downstreamMsg := s.InitialSyncCompletedMessage("Downstream") + s.log.Info(downstreamMsg) + + logOutput := buf.String() + assert.Assert(t, strings.Contains(logOutput, "Upstream - Initial sync completed for /workspace/frontend <-> /src/frontend"), "expected upstream completion log with target context") + assert.Assert(t, strings.Contains(logOutput, "Downstream - Initial sync completed for /workspace/frontend <-> /src/frontend"), "expected downstream completion log with target context") +}