Add initial public gitsync package · Entire
Add initial public gitsync package
3e236b4→main·
Soph·3mo ago·3 files·+448 added/-0 removed
Sessions
5fe00ff669ffView transcript
Changes
3
pkg/gitsync
Aclient.go+119
Aclient_test.go+55
Atypes.go+274
package gitsync
import (
"context"
"fmt"
"github.com/soph/git-sync/internal/syncer"
)
// Options configures a Client. It is intentionally small in the first public cut.
type Options struct{}
// Client provides the public orchestration API for git-sync.
type Client struct{}
// New constructs a new Client.
func New(opts Options) *Client {
_ = opts
return &Client{}
}
// Probe inspects a source remote and optional target remote.
func (c *Client) Probe(ctx context.Context, req ProbeRequest) (ProbeResult, error) {
if err := req.Validate(); err != nil {
return ProbeResult{}, err
}
result, err := syncer.Probe(ctx, buildProbeConfig(req))
if err != nil {
return ProbeResult{}, err
}
return fromSyncerProbeResult(result), nil
}
// Plan computes ref actions without pushing.
func (c *Client) Plan(ctx context.Context, req PlanRequest) (PlanResult, error) {
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))
if err != nil {
return PlanResult{}, err
}
return fromSyncerResult(result), nil
}
// Sync executes a sync between two remotes.
func (c *Client) Sync(ctx context.Context, req SyncRequest) (SyncResult, error) {
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))
if err != nil {
return SyncResult{}, err
}
return fromSyncerResult(result), nil
}
func buildProbeConfig(req ProbeRequest) syncer.Config {
cfg := syncer.Config{
Source: syncer.Endpoint{URL: req.Source.URL, Username: req.SourceAuth.Username, Token: req.SourceAuth.Token, BearerToken: req.SourceAuth.BearerToken, SkipTLSVerify: req.SourceAuth.SkipTLSVerify},
IncludeTags: req.IncludeTags,
ShowStats: req.CollectStats,
ProtocolMode: string(req.Protocol),
}
if req.Target != nil {
cfg.Target = syncer.Endpoint{URL: req.Target.URL, Username: req.TargetAuth.Username, Token: req.TargetAuth.Token, BearerToken: req.TargetAuth.BearerToken, SkipTLSVerify: req.TargetAuth.SkipTLSVerify}
}
return cfg
}
func 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},
Branches: append([]string(nil), scope.Branches...),
Mappings: append([]RefMapping(nil), scope.Mappings...),
IncludeTags: policy.IncludeTags,
DryRun: dryRun,
ShowStats: collectStats,
Force: policy.Force,
Prune: policy.Prune,
ProtocolMode: protocolString(policy.Protocol),
MaterializedMaxObjects: syncer.DefaultMaterializedMaxObjects,
}
}
func protocolString(mode ProtocolMode) string {
if mode == "" {
return string(ProtocolAuto)
}
return string(mode)
}
func (r SyncRequest) Validate() error {
if r.Source.URL == "" {
return fmt.Errorf("source URL is required")
}
if r.Target.URL == "" {
return fmt.Errorf("target URL is required")
}
return nil
}
func (r PlanRequest) Validate() error {
if r.Source.URL == "" {
return fmt.Errorf("source URL is required")
}
if r.Target.URL == "" {
return fmt.Errorf("target URL is required")
}
return nil
}
func (r ProbeRequest) Validate() error {
if r.Source.URL == "" {
return fmt.Errorf("source URL is required")
}
return nil
}
Apkg/gitsync/client.go+119
package gitsync
import (
"testing"
"github.com/go-git/go-git/v6/plumbing"
)
func TestBuildSyncConfigUsesDefaultProtocolAndMaterializedLimit(t *testing.T) {
cfg := buildSyncConfig(
Endpoint{URL: "https://source.example/repo.git"},
EndpointAuth{Token: "src"},
Endpoint{URL: "https://target.example/repo.git"},
EndpointAuth{Token: "dst"},
RefScope{Branches: []string{"main"}},
SyncPolicy{},
true,
false,
)
if cfg.ProtocolMode != string(ProtocolAuto) {
t.Fatalf("protocol mode = %q, want %q", cfg.ProtocolMode, ProtocolAuto)
}
if cfg.MaterializedMaxObjects <= 0 {
t.Fatalf("materialized max objects = %d, want positive default", cfg.MaterializedMaxObjects)
}
if !cfg.ShowStats {
t.Fatalf("show stats = false, want true")
}
if cfg.DryRun {
t.Fatalf("dry run = true, want false")
}
if cfg.Source.Token != "src" || cfg.Target.Token != "dst" {
t.Fatalf("unexpected token mapping: %+v %+v", cfg.Source, cfg.Target)
}
}
func TestValidateRequests(t *testing.T) {
if err := (ProbeRequest{}).Validate(); err == nil {
t.Fatalf("expected probe validation error")
}
if err := (PlanRequest{}).Validate(); err == nil {
t.Fatalf("expected plan validation error")
}
if err := (SyncRequest{}).Validate(); err == nil {
t.Fatalf("expected sync validation error")
}
}
func TestFromSyncerResultZeroHashesAreEmptyStrings(t *testing.T) {
got := hashString(plumbing.ZeroHash)
if got != "" {
t.Fatalf("hashString(zero) = %q, want empty string", got)
}
}
Apkg/gitsync/client_test.go+55
package gitsync
import (
"github.com/go-git/go-git/v6/plumbing"
"github.com/soph/git-sync/internal/planner"
"github.com/soph/git-sync/internal/syncer"
"github.com/soph/git-sync/internal/validation"
)
// ProtocolMode controls source-side protocol negotiation.
type ProtocolMode string
const (
ProtocolAuto ProtocolMode = validation.ProtocolAuto
ProtocolV1 ProtocolMode = validation.ProtocolV1
ProtocolV2 ProtocolMode = validation.ProtocolV2
)
// Endpoint identifies a remote Git endpoint.
type Endpoint struct {
URL string
}
// EndpointAuth carries explicit per-request auth and TLS settings.
type EndpointAuth struct {
Username string
Token string
BearerToken string
SkipTLSVerify bool
}
// RefMapping is an explicit source-to-target ref mapping.
type RefMapping = validation.RefMapping
// RefScope constrains which refs a request manages.
type RefScope struct {
Branches []string
Mappings []RefMapping
}
// SyncPolicy controls high-level sync behavior.
type SyncPolicy struct {
IncludeTags bool
Force bool
Prune bool
Protocol ProtocolMode
}
// ProbeRequest inspects source refs and optional target capabilities.
type ProbeRequest struct {
Source Endpoint
SourceAuth EndpointAuth
Target *Endpoint
TargetAuth EndpointAuth
IncludeTags bool
Protocol ProtocolMode
CollectStats bool
}
// PlanRequest computes ref actions without pushing.
type PlanRequest struct {
Source Endpoint
SourceAuth EndpointAuth
Target Endpoint
TargetAuth EndpointAuth
Scope RefScope
Policy SyncPolicy
CollectStats bool
}
// SyncRequest executes a sync between two remotes.
type SyncRequest struct {
Source Endpoint
SourceAuth EndpointAuth
Target Endpoint
TargetAuth EndpointAuth
Scope RefScope
Policy SyncPolicy
CollectStats bool
}
// RefKind distinguishes branch refs from tag refs.
type RefKind string
const (
RefKindBranch RefKind = RefKind(planner.RefKindBranch)
RefKindTag RefKind = RefKind(planner.RefKindTag)
)
// Action describes the planned or executed operation on a ref.
type Action string
const (
ActionCreate Action = Action(planner.ActionCreate)
ActionUpdate Action = Action(planner.ActionUpdate)
ActionDelete Action = Action(planner.ActionDelete)
ActionSkip Action = Action(planner.ActionSkip)
ActionBlock Action = Action(planner.ActionBlock)
)
// RefPlan describes the outcome for a single ref.
type RefPlan struct {
Branch string `json:"branch"`
SourceRef string `json:"source_ref"`
TargetRef string `json:"target_ref"`
SourceHash string `json:"source_hash"`
TargetHash string `json:"target_hash"`
Kind RefKind `json:"kind"`
Action Action `json:"action"`
Reason string `json:"reason"`
}
// RefInfo identifies a named ref.
type RefInfo struct {
Name string `json:"name"`
Hash string `json:"hash"`
}
// ServiceStats tracks transfer statistics for a single service.
type ServiceStats struct {
Name string `json:"name"`
Requests int `json:"requests"`
RequestBytes int64 `json:"request_bytes"`
ResponseBytes int64 `json:"response_bytes"`
Wants int `json:"wants"`
Haves int `json:"haves"`
Commands int `json:"commands"`
}
// Stats summarizes transfer metrics.
type Stats struct {
Enabled bool `json:"enabled"`
Items map[string]*ServiceStats `json:"items"`
}
// Measurement summarizes elapsed time and Go heap usage.
type Measurement struct {
Enabled bool `json:"enabled"`
ElapsedMillis int64 `json:"elapsed_millis"`
PeakAllocBytes uint64 `json:"peak_alloc_bytes"`
PeakHeapInuseBytes uint64 `json:"peak_heap_inuse_bytes"`
TotalAllocBytes uint64 `json:"total_alloc_bytes"`
GCCount uint32 `json:"gc_count"`
}
// ProbeResult holds structured probe output suitable for workers.
type ProbeResult struct {
SourceURL string `json:"source_url"`
TargetURL string `json:"target_url,omitempty"`
RequestedMode string `json:"requested_mode"`
Protocol string `json:"protocol"`
RefPrefixes []string `json:"ref_prefixes"`
Capabilities []string `json:"source_capabilities"`
TargetCaps []string `json:"target_capabilities,omitempty"`
Refs []RefInfo `json:"refs"`
Stats Stats `json:"stats"`
Measurement Measurement `json:"measurement"`
}
// SyncResult holds structured sync output suitable for workers.
type SyncResult struct {
Plans []RefPlan `json:"plans"`
Pushed int `json:"pushed"`
Skipped int `json:"skipped"`
Blocked int `json:"blocked"`
Deleted int `json:"deleted"`
DryRun bool `json:"dry_run"`
Relay bool `json:"relay"`
RelayMode string `json:"relay_mode"`
RelayReason string `json:"relay_reason"`
Batching bool `json:"batching"`
BatchCount int `json:"batch_count"`
PlannedBatchCount int `json:"planned_batch_count"`
TempRefs []string `json:"temp_refs"`
BootstrapSuggested bool `json:"bootstrap_suggested"`
Stats Stats `json:"stats"`
Measurement Measurement `json:"measurement"`
Protocol string `json:"protocol"`
}
// PlanResult is the dry-run form of SyncResult.
type PlanResult = SyncResult
func fromSyncerProbeResult(result syncer.ProbeResult) ProbeResult {
out := ProbeResult{
SourceURL: result.SourceURL,
TargetURL: result.TargetURL,
RequestedMode: result.RequestedMode,
Protocol: result.Protocol,
RefPrefixes: append([]string(nil), result.RefPrefixes...),
Capabilities: append([]string(nil), result.Capabilities...),
TargetCaps: append([]string(nil), result.TargetCaps...),
Refs: make([]RefInfo, 0, len(result.Refs)),
Stats: fromSyncerStats(result.Stats),
Measurement: fromSyncerMeasurement(result.Measurement),
}
for _, ref := range result.Refs {
out.Refs = append(out.Refs, RefInfo{Name: ref.Name, Hash: ref.Hash.String()})
}
return out
}
func fromSyncerResult(result syncer.Result) SyncResult {
out := SyncResult{
Plans: make([]RefPlan, 0, len(result.Plans)),
Pushed: result.Pushed,
Skipped: result.Skipped,
Blocked: result.Blocked,
Deleted: result.Deleted,
DryRun: result.DryRun,
Relay: result.Relay,
RelayMode: result.RelayMode,
RelayReason: result.RelayReason,
Batching: result.Batching,
BatchCount: result.BatchCount,
PlannedBatchCount: result.PlannedBatchCount,
TempRefs: append([]string(nil), result.TempRefs...),
BootstrapSuggested: result.BootstrapSuggested,
Stats: fromSyncerStats(result.Stats),
Measurement: fromSyncerMeasurement(result.Measurement),
Protocol: result.Protocol,
}
for _, plan := range result.Plans {
out.Plans = append(out.Plans, RefPlan{
Branch: plan.Branch,
SourceRef: plan.SourceRef.String(),
TargetRef: plan.TargetRef.String(),
SourceHash: hashString(plan.SourceHash),
TargetHash: hashString(plan.TargetHash),
Kind: RefKind(plan.Kind),
Action: Action(plan.Action),
Reason: plan.Reason,
})
}
return out
}
func fromSyncerStats(stats syncer.Stats) Stats {
out := Stats{Enabled: stats.Enabled, Items: make(map[string]*ServiceStats, len(stats.Items))}
for key, item := range stats.Items {
copyItem := *item
out.Items[key] = &ServiceStats{
Name: copyItem.Name,
Requests: copyItem.Requests,
RequestBytes: copyItem.RequestBytes,
ResponseBytes: copyItem.ResponseBytes,
Wants: copyItem.Wants,
Haves: copyItem.Haves,
Commands: copyItem.Commands,
}
}
return out
}
func fromSyncerMeasurement(m syncer.Measurement) Measurement {
return Measurement{
Enabled: m.Enabled,
ElapsedMillis: m.ElapsedMillis,
PeakAllocBytes: m.PeakAllocBytes,
PeakHeapInuseBytes: m.PeakHeapInuseBytes,
TotalAllocBytes: m.TotalAllocBytes,
GCCount: m.GCCount,
}
}
func hashString(hash plumbing.Hash) string {
if hash.IsZero() {
return ""
}
return hash.String()
}
Apkg/gitsync/types.go+274