# syncer: warn that SSH stats and progress are incomplete

`9aa3e87`→[main](/content/gh/entireio/git-sync/commits/main/index.html) ·   
Soph·2mo ago·2 files·+87 added/-2 removed

## Sessions

1ae05f312d98 View transcript

[?\ yes, start implementing, make a new branch, make meaningful commits, and add tests as you go, when it makes sense create tests first Codex·GPT-5.4·1 step](/content/gh/entireio/git-sync/session/019e260b-71e8-73a1-9e68-5857379ffab6#timeline-1ae05f312d98/index.html)

## Changes

2

- internal/syncer

- Msyncer.go +37/-1

- Msyncer_test.go +50/-1

```
535 unmodified lines

536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
83 unmodified lines

644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
5 unmodified lines

671
672
673
674
675
676
677
10 unmodified lines

688
689
690
691
692
693
694
22 unmodified lines

717
718
719
683
720
721
722

535 unmodified lines

return false
}

func sshStatsWarning(cfg Config, sourceConn, targetConn gitproto.Conn) string {
	if !cfg.Progress && !cfg.ShowStats {
		return ""
	}
	hasSSH := false
	if _, ok := sourceConn.(*gitproto.SSHConn); ok {
		hasSSH = true
	}
	if !hasSSH && targetConn != nil {
		if _, ok := targetConn.(*gitproto.SSHConn); ok {
			hasSSH = true
		}
	}
	if !hasSSH {
		return ""
	}
	return "warning: SSH transport does not yet expose byte-counted throughput; --progress and --show-stats output will omit SSH transfer bytes"
}

// --- Session setup (issue #12) ---

// syncSession holds the shared state for a sync operation, reducing
83 unmodified lines

stats:           newStats(cfg.ShowStats),
	measurementDone: startMeasurement(cfg.MeasureMemory),
}
var warnedSSHStats bool
warnSSHStats := func(sourceConn, targetConn gitproto.Conn) {
	if warnedSSHStats {
		return
	}
	warning := sshStatsWarning(cfg, sourceConn, targetConn)
	if warning == "" {
		return
	}
	warnedSSHStats = true
	out := cfg.progressOut
	if out == nil {
		out = os.Stderr
	}
	fmt.Fprintln(out, warning)
}
if cfg.Verbose {
	s.logger = slog.New(slog.NewTextHandler(&sessionStderr{s: s}, &slog.HandlerOptions{
		Level: slog.LevelInfo,
5 unmodified lines

return nil, fmt.Errorf("create source transport: %w", err)
}
s.sourceConn.SetProgressWriter(&sessionStderr{s: s})
warnSSHStats(s.sourceConn, nil)

refPrefixes := planner.RefPrefixes(planConfig(cfg))
sourceRefs, sourceService, err := gitproto.ListSourceRefs(ctx, s.sourceConn, cfg.ProtocolMode, refPrefixes)
10 unmodified lines

return nil, fmt.Errorf("create target transport: %w", err)
	}
targetConn.SetProgressWriter(&sessionStderr{s: s})
warnSSHStats(s.sourceConn, targetConn)
targetAdv, err := gitproto.AdvertisedRefsV1(ctx, targetConn, transport.ReceivePackService)
if err != nil {
	return nil, fmt.Errorf("list target refs: %w", err)
}
22 unmodified lines

}
		}
	}

// Start the live progress ticker only after auth resolution and the
	// initial ref-listing round trips have completed. The auth path may
	// shell out to `git credential fill`, which inherits our stderr and
```

Minternal/syncer/syncer.go +37/-1

```
1 unmodified line

2
3
4
5
6
7
8
9
236 unmodified lines

246
247
248
249
250
251
252
253
248
254
255
256
257
14 unmodified lines

272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317

1 unmodified line

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"

236 unmodified lines

func TestNewConnBuildsSSHTransport(t *testing.T) {
	orig := gitproto.SSHLookPath
	t.Cleanup(func() { gitproto.SSHLookPath = orig })
	script := filepath.Join(t.TempDir(), "ssh-stub.sh")
	if err := os.WriteFile(script, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
		t.Fatalf("write ssh stub: %v", err)
	}
	gitproto.SSHLookPath = func(string) (string, error) {
		return "/usr/bin/ssh", nil
		return script, nil
	}

stats := newStats(false)
14 unmodified lines

})
	}
}

func TestSSHStatsWarning(t *testing.T) {
	tests := []struct {
		name   string
		cfg    Config
		source gitproto.Conn
		target gitproto.Conn
		want   bool
	}{
		{
		name:   "no flags",
		cfg:    Config{},
		source: &gitproto.SSHConn{},
		},
		{
		name:   "http only",
		cfg:    Config{Progress: true},
		source: &gitproto.HTTPConn{},
		target: &gitproto.HTTPConn{},
		},
		{
		name:   "progress with ssh source",
		cfg:    Config{Progress: true},
		source: &gitproto.SSHConn{},
		want:   true,
		},
		{
		name:   "show stats with ssh target",
		cfg:    Config{ShowStats: true},
		source: &gitproto.HTTPConn{},
		target: &gitproto.SSHConn{},
		want:   true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := sshStatsWarning(tt.cfg, tt.source, tt.target)
			if (got != "") != tt.want {
				t.Fatalf("sshStatsWarning() = %q, want warning=%t", got, tt.want)
			}
		})
	}
}
