Reject identical source and target endpoints · Entire
Reject identical source and target endpoints
79e093e→main· pjbgf·2mo ago·4 files·+120 added/-0 removed
Catch the obvious misconfiguration where source and target URLs point at the same repository before any network I/O.
Assisted-by: Claude Opus 4.7 noreply@anthropic.com Signed-off-by: Paulo Gomes paulo@entire.io
Sessions
358f2d5f22f6View transcript
Changes
4
internal
syncer
Msyncer.go+5
Msyncer_test.go+64
validation
Mvalidation.go+15
Mvalidation_test.go+36
380 unmodified lines
381
382
383
384
385
386
387
388
389
390
391
380 unmodified lines
if cfg.Mode == modeReplicate && cfg.Force {
return nil, errors.New("replicate does not support --force; use sync instead")
}
if needTarget {
if err := validation.ValidateEndpoints(cfg.Source.URL, cfg.Target.URL); err != nil {
return nil, fmt.Errorf("validate endpoints: %w", err)
}
}
s := &syncSession{
cfg: cfg,
Minternal/syncer/syncer.go+5
1
2
3
4
5
6
7
8
25 unmodified lines
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package syncer
import (
"context"
"strings"
"testing"
bstrap "entire.io/entire/gitsync/internal/strategy/bootstrap"
25 unmodified lines
}
// TestPublicAPIRejectsIdenticalSourceAndTarget covers every entry point that
// touches both endpoints: same URL on source and target must fail before any
// network I/O. Probe with no target and Fetch are intentionally excluded
// because they do not have a target.
func TestPublicAPIRejectsIdenticalSourceAndTarget(t *testing.T) {
t.Parallel()
const url = "https://example.com/repo.git"
cfg := Config{
Source: Endpoint{URL: url},
Target: Endpoint{URL: url},
}
tests := []struct {
name string
call func() error
}{
{name: "Run", call: func() error {
_, err := Run(context.Background(), cfg)
return err
}},
{name: "Bootstrap", call: func() error {
_, err := Bootstrap(context.Background(), cfg)
return err
}},
{name: "Probe with target", call: func() error {
_, err := Probe(context.Background(), cfg)
return err
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.call()
if err == nil {
t.Fatalf("%s with identical source/target URLs returned nil error", tt.name)
}
if !strings.Contains(err.Error(), "source and target must not be the same repository") {
t.Fatalf("%s error = %v, want same-repository rejection", tt.name, err)
}
})
}
}
// TestProbeWithoutTargetIgnoresEndpointEqualityCheck guards against a regression
// where the source-vs-target check would fire for a probe that never set a
// target — there is nothing to compare against.
func TestProbeWithoutTargetIgnoresEndpointEqualityCheck(t *testing.T) {
t.Parallel()
cfg := Config{Source: Endpoint{URL: "https://example.com/repo.git"}}
_, err := Probe(context.Background(), cfg)
if err == nil {
return
}
if strings.Contains(err.Error(), "source and target must not be the same repository") {
t.Fatalf("Probe without target tripped same-repository check: %v", err)
}
}
// TestNewConn_PropagatesFollowInfoRefsRedirect proves the plumbing from
// Endpoint → gitproto.Conn is in place. Without this the flag on
// Endpoint is dead config.
Minternal/syncer/syncer_test.go+64
24 unmodified lines
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
24 unmodified lines
TargetRef plumbing.ReferenceName
}
// ValidateEndpoints rejects configurations where the source and target URLs
// point at the same repository. Empty URLs are ignored so the caller can
// surface a more specific "missing URL" error.
func ValidateEndpoints(sourceURL, targetURL string) error {
src := strings.TrimSpace(sourceURL)
dst := strings.TrimSpace(targetURL)
if src == "" || dst == "" {
return nil
}
if src == dst {
return fmt.Errorf("source and target must not be the same repository: %s", src)
}
return nil
}
// NormalizeProtocolMode validates the configured protocol mode and applies the
// default auto mode when the user did not specify one.
Minternal/validation/validation.go+15
72 unmodified lines
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
72 unmodified lines
}
func TestValidateEndpoints(t *testing.T) {
t.Parallel()
tests := []struct {
name string
source string
target string
wantErr bool
}{
{name: "different URLs", source: "https://src.example/r.git", target: "https://dst.example/r.git"},
{name: "same URL", source: "https://example.com/r.git", target: "https://example.com/r.git", wantErr: true},
{name: "same URL with surrounding whitespace", source: " https://example.com/r.git", target: "https://example.com/r.git\t", wantErr: true},
{name: "empty source defers to other checks", source: "", target: "https://example.com/r.git"},
{name: "empty target defers to other checks", source: "https://example.com/r.git", target: ""},
{name: "both empty defers to other checks", source: "", target: ""},
{name: "differ only by trailing slash", source: "https://example.com/r.git", target: "https://example.com/r.git/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := ValidateEndpoints(tt.source, tt.target)
if tt.wantErr {
if err == nil {
t.Fatalf("ValidateEndpoints(%q, %q) = nil, want error", tt.source, tt.target)
}
return
}
if err != nil {
t.Fatalf("ValidateEndpoints(%q, %q) = %v, want nil", tt.source, tt.target, err)
}
})
}
}
func TestParseHaveRef(t *testing.T) {
tests := []struct {
name string