Add HTTP client injection to gitsync · Entire

Add HTTP client injection to gitsync

cd93f12→main·

Soph·3mo ago·7 files·+101 added/-31 removed

Sessions

15ba661d1843View transcript

Changes

7

49 unmodified lines

50
51
52
53
54
55
56
57
58
59
60
61
62
63

49 unmodified lines

// NewConn creates a new connection to the given endpoint.
func NewConn(ep *transport.Endpoint, label string, auth transport.AuthMethod, rt http.RoundTripper) *Conn {
    httpClient := &http.Client{Transport: rt}
    return NewConnWithHTTPClient(ep, label, auth, httpClient)
}

// NewConnWithHTTPClient creates a new connection using the provided HTTP client.
func NewConnWithHTTPClient(ep *transport.Endpoint, label string, auth transport.AuthMethod, httpClient *http.Client) *Conn {
    if httpClient == nil {
        httpClient = &http.Client{Transport: http.DefaultTransport}
    }
    return &Conn{
        Label:     label,
        Endpoint:  ep,
    }
}

Minternal/gitproto/smarthttp.go+8

51 unmodified lines

52 53 54 55 55 56 57 58 10 unmodified lines

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 97 unmodified lines

193 194 195 175

51 unmodified lines

conn, err := newConn(Endpoint{ URL: "https://example.com/repo.git", SkipTLSVerify: true, }, "source", stats) }, "source", stats, nil) if err != nil { t.Fatalf("new conn: %v", err) } 10 unmodified lines

} }

func TestNewConnUsesProvidedHTTPClient(t *testing.T) { stats := newStats(false) baseTransport := http.DefaultTransport baseClient := &http.Client{Transport: baseTransport}

conn, err := newConn(Endpoint{URL: "https://example.com/repo.git"}, "source", stats, baseClient) if err != nil { t.Fatalf("new conn: %v", err) } if conn.HTTP == baseClient { t.Fatalf("expected cloned HTTP client, got original pointer") } rt, ok := conn.HTTP.Transport.(*countingRoundTripper) if !ok { t.Fatalf("expected countingRoundTripper, got %T", conn.HTTP.Transport) } if rt.base != baseTransport { t.Fatalf("wrapped base transport = %T, want %T", rt.base, baseTransport) } }

func TestResolveAuthMethodUsesEntireDBStoredToken(t *testing.T) { configDir := t.TempDir() tokenStorePath := filepath.Join(t.TempDir(), "tokens.json")

97 unmodified lines

t.Fatalf("write hosts: %v", err) } }


Minternal/syncer/auth_test.go+22/-2

514 unmodified lines

515 516 517 518 518 519 520 521 90 unmodified lines

612 613 614 615 615 616 617 618 13 unmodified lines

632 633 634 635 635 636 637 638 637 638 639 640 641

514 unmodified lines

}

stats := newStats(false) sourceConn, err := newConn(cfg.Source, "source", stats) sourceConn, err := newConn(cfg.Source, "source", stats, nil) if err != nil { t.Fatalf("create source transport: %v", err) } 90 unmodified lines

}

stats := newStats(false) sourceConn, err := newConn(cfg.Source, "source", stats) sourceConn, err := newConn(cfg.Source, "source", stats, nil) if err != nil { t.Fatalf("create source transport: %v", err) } 13 unmodified lines

plan := func(limit int64) []plumbing.Hash { t.Helper() checkpoints, err := bstrap.PlanCheckpoints(context.Background(), bstrap.Params{ SourceConn: sourceConn, SourceConn: sourceConn, SourceService: sourceService, BatchMaxPack: limit, Verbose: cfg.Verbose, BatchMaxPack: limit, Verbose: cfg.Verbose, }, ref) if err != nil { t.Fatalf("plan checkpoints with limit %d: %v", limit, err) } }

Minternal/syncer/git_http_backend_test.go+5/-5


8 unmodified lines

9
10
11
12
13
14
15
39 unmodified lines

55
56
57
58
59
60
61
211 unmodified lines

273
274
275
274
276
277
278
279
8 unmodified lines

288
289
290
289
290
291
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
22 unmodified lines

331
332
333
320
321
322
323
324
325
334
335
336
337
338
339
340
341
342
19 unmodified lines

362
363
364
351
365
366
367
368
7 unmodified lines

376
377
378
365
379
380
381
382

8 unmodified lines

"errors"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "sort"
    "strings"
39 unmodified lines

type Config struct {
        Source                 Endpoint
        Target                 Endpoint
        HTTPClient             *http.Client
        Branches               []string
        Mappings               []RefMapping
        IncludeTags            bool
211 unmodified lines

// --- Session setup ---

func newConn(raw Endpoint, label string, stats *statsCollector) (*gitproto.Conn, error) {
func newConn(raw Endpoint, label string, stats *statsCollector, httpClient *http.Client) (*gitproto.Conn, error) {
    ep, err := transport.NewEndpoint(raw.URL)
    if err != nil {
        return nil, err
    }
8 unmodified lines

if err != nil {
        return nil, err
    }
    baseRT := gitproto.NewHTTPTransport(raw.SkipTLSVerify)
    rt := &countingRoundTripper{base: baseRT, label: label, stats: stats}
    return gitproto.NewConn(ep, label, authMethod, rt), nil
    client := instrumentHTTPClient(httpClient, raw.SkipTLSVerify, label, stats)
    return gitproto.NewConnWithHTTPClient(ep, label, authMethod, client), nil
}

func instrumentHTTPClient(base *http.Client, skipTLS bool, label string, stats *statsCollector) *http.Client {
    if base == nil {
        base = &http.Client{Transport: gitproto.NewHTTPTransport(skipTLS)}
    }
    clone := *base
    baseRT := clone.Transport
    if baseRT == nil {
        baseRT = gitproto.NewHTTPTransport(skipTLS)
    }
    clone.Transport = &countingRoundTripper{base: baseRT, label: label, stats: stats}
    return &clone
}

func planConfig(cfg Config) planner.PlanConfig {
22 unmodified lines

}

type targetSession struct {
    conn      *gitproto.Conn
    adv       *packp.AdvRefs
    refMap    map[plumbing.ReferenceName]plumbing.Hash
    features  gitproto.TargetFeatures
    policy    planner.RelayTargetPolicy
    pusher    gitproto.Pusher
    conn     *gitproto.Conn
    adv      *packp.AdvRefs
    refMap   map[plumbing.ReferenceName]plumbing.Hash
    features gitproto.TargetFeatures
    policy   planner.RelayTargetPolicy
    pusher   gitproto.Pusher
}

// newSession performs the shared setup: protocol validation, mapping validation,
19 unmodified lines

}))

s.sourceConn, err = newConn(cfg.Source, "source", s.stats)
    s.sourceConn, err = newConn(cfg.Source, "source", s.stats, cfg.HTTPClient)
    if err != nil {
        return nil, fmt.Errorf("create source transport: %w", err)
    }
7 unmodified lines

s.sourceRefMap = gitproto.RefHashMap(sourceRefs)

if needTarget {
        targetConn, err := newConn(cfg.Target, "target", s.stats)
        targetConn, err := newConn(cfg.Target, "target", s.stats, cfg.HTTPClient)
        if err != nil {
            return nil, fmt.Errorf("create target transport: %w", err)
        }
}

Minternal/syncer/syncer.go+26/-12

7 unmodified lines

8 9 10 11 11 12 13 14 8 unmodified lines

23 24 25 26 26 27 28 29 1 unmodified line

31 32 33 34

7 unmodified lines

func TestGitHubOwnerRepo(t *testing.T) { stats := newStats(false) conn, err := newConn(Endpoint{URL: "https://github.com/torvalds/linux.git"}, "source", stats) conn, err := newConn(Endpoint{URL: "https://github.com/torvalds/linux.git"}, "source", stats, nil) if err != nil { t.Fatalf("new conn: %v", err) } 8 unmodified lines

func TestGitHubOwnerRepoRejectsNonGitHubSource(t *testing.T) { stats := newStats(false) conn, err := newConn(Endpoint{URL: "https://gitlab.com/group/project.git"}, "source", stats) conn, err := newConn(Endpoint{URL: "https://gitlab.com/group/project.git"}, "source", stats, nil) if err != nil { t.Fatalf("new conn: %v", err) } 1 unmodified line

t.Fatalf("expected non-github source to be rejected") } }

Minternal/syncer/syncer_test.go+2/-3


2 unmodified lines

3
4
5
6
7
8
9
10
11
11
12
13
14
15
16
14
17
18
19
20
21
22
18
19
23
24
25
26
1 unmodified line

28
29
30
27
31
32
33
34
5 unmodified lines

40
41
42
39
43
44
45
46
5 unmodified lines

52
53
54
51
55
56
57
58
13 unmodified lines

72
73
74
71
75
76
77
78
79
80
81
82
83
84
85
86
87
88

2 unmodified lines

import (
    "context"
    "fmt"
    "net/http"

"github.com/soph/git-sync/internal/syncer"
)

// Options configures a Client. It is intentionally small in the first public cut.
type Options struct{}
type Options struct {
    HTTPClient *http.Client
}

// Client provides the public orchestration API for git-sync.
type Client struct{}
type Client struct {
    httpClient *http.Client
}

// New constructs a new Client.
func New(opts Options) *Client {
    _ = opts
    return &Client{}
    return &Client{httpClient: opts.HTTPClient}
}

// Probe inspects a source remote and optional target remote.
1 unmodified line

if err := req.Validate(); err != nil {
        return ProbeResult{}, err
    }
    result, err := syncer.Probe(ctx, buildProbeConfig(req))
    result, err := syncer.Probe(ctx, c.buildProbeConfig(req))
    if err != nil {
        return ProbeResult{}, err
    }
5 unmodified lines

if err := req.Validate(); err != nil {
        return PlanResult{}, err
    }
    result, err := syncer.Run(ctx, buildSyncConfig(req.Source, req.SourceAuth, req.Target, req.TargetAuth, req.Scope, req.Policy, req.CollectStats, true))
    result, err := syncer.Run(ctx, c.buildSyncConfig(req.Source, req.SourceAuth, req.Target, req.TargetAuth, req.Scope, req.Policy, req.CollectStats, true))
    if err != nil {
        return PlanResult{}, err
    }
5 unmodified lines

if err := req.Validate(); err != nil {
        return SyncResult{}, err
    }
    result, err := syncer.Run(ctx, buildSyncConfig(req.Source, req.SourceAuth, req.Target, req.TargetAuth, req.Scope, req.Policy, req.CollectStats, false))
    result, err := syncer.Run(ctx, c.buildSyncConfig(req.Source, req.SourceAuth, req.Target, req.TargetAuth, req.Scope, req.Policy, req.CollectStats, false))
    if err != nil {
        return SyncResult{}, err
    }
13 unmodified lines

return cfg
}

func buildSyncConfig(source Endpoint, sourceAuth EndpointAuth, target Endpoint, targetAuth EndpointAuth, scope RefScope, policy SyncPolicy, collectStats, dryRun bool) syncer.Config {
func (c *Client) buildProbeConfig(req ProbeRequest) syncer.Config {
    cfg := buildProbeConfig(req)
    cfg.HTTPClient = c.httpClient
    return cfg
}

func (c *Client) buildSyncConfig(source Endpoint, sourceAuth EndpointAuth, target Endpoint, targetAuth EndpointAuth, scope RefScope, policy SyncPolicy, collectStats, dryRun bool) syncer.Config {
    return syncer.Config{
        Source:                 syncer.Endpoint{URL: source.URL, Username: sourceAuth.Username, Token: sourceAuth.Token, BearerToken: sourceAuth.BearerToken, SkipTLSVerify: sourceAuth.SkipTLSVerify},
        Target:                 syncer.Endpoint{URL: target.URL, Username: targetAuth.Username, Token: targetAuth.Token, BearerToken: targetAuth.BearerToken, SkipTLSVerify: targetAuth.SkipTLSVerify},
        HTTPClient:             c.httpClient,
        Branches:               append([]string(nil), scope.Branches...),
        Mappings:               append([]RefMapping(nil), scope.Mappings...),
        IncludeTags:            policy.IncludeTags,
    }