# Merge pull request #33 from entireio/soph/fixes

`3819253`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·

Soph·2mo ago·5 files·+365 added/-22 removed

Sync to targets with overlapping reachability

## Changes

5

- internal

- gitproto

- Msmarthttp.go+25/-1

- planner

- Mplanner.go+14

- Mplanner_test.go+188/-11

- Mrelay.go+30/-10

- syncer

- Mintegration_test.go+108

```
15 unmodified lines

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
7 unmodified lines

44
45
46
32
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

15 unmodified lines

const maxHTTPErrorBody = 64 * 1024

// diagnosticHeaders carry trace/correlation IDs that operators of upstream
// services use to look up the failing request server-side. Surfaced in
// httpError so a 500 with an opaque body (e.g. "Internal Server Error") still
// gives the user something actionable to share when reporting the failure.
var diagnosticHeaders = []string{
	"Cf-Ray",
	"X-Request-Id",
	"Request-Id",
	"X-Trace-Id",
	"X-Amz-Request-Id",
	"X-Github-Request-Id",
	"Server",
	"Content-Type",
}

// httpError checks an HTTP response status and returns an error for non-2xx responses.
func httpError(res *http.Response) error {
	if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusMultipleChoices {
7 unmodified lines

if len(data) > maxHTTPErrorBody {
			data = append(data[:maxHTTPErrorBody], []byte("...")...)
		}
		reason = string(data)
		reason = strings.TrimSpace(string(data))
	}
}
var diag []string
for _, h := range diagnosticHeaders {
	if v := res.Header.Get(h); v != "" {
		diag = append(diag, h+"="+v)
	}
}
if len(diag) > 0 {
	return fmt.Errorf("http %d: %s [%s] %s", res.StatusCode, res.Request.URL.Redacted(), strings.Join(diag, ", "), reason)
}
return fmt.Errorf("http %d: %s %s", res.StatusCode, res.Request.URL.Redacted(), reason)
}
```

Minternal/gitproto/smarthttp.go+25/-1

```
406 unmodified lines

407
408
409
410
411
412
413
414
415
416
417
418
419
420
42 unmodified lines

463
464
465
466
467
468
469
470
471
472
473
474

406 unmodified lines

}

// ObjectsToPush computes the set of objects that need to be sent to the target.
//
// A fetch with target refs as haves prunes the source pack server-side, so
// objects reachable from a have are intentionally absent from the local
// store. Both top-level wants and transitive references encountered during
// the walk may be missing for this reason — they're treated as implicitly
// have'd by the target and excluded from the push pack. The target's
// receive-pack accepts ref updates referencing such objects because it
// already has them under one of its existing refs.
func ObjectsToPush(store storer.EncodedObjectStorer, wants []plumbing.Hash, targetRefs map[plumbing.ReferenceName]plumbing.Hash) ([]plumbing.Hash, error) {
	haveSet := make(map[plumbing.Hash]struct{})
	for _, h := range targetRefs {
42 unmodified lines

obj, err := store.EncodedObject(plumbing.AnyObject, hash)
	if err != nil {
		if errors.Is(err, plumbing.ErrObjectNotFound) {
			// Reachable from a target-ref have via the source server's
			// pack-prune; the target already has it, so it stays out
			// of the push.
			return nil
		}
		return fmt.Errorf("load object %s: %w", hash, err)
	}
}
```

Minternal/planner/planner.go+14

```
1037 unmodified lines

1038
1039
1040
1041
1041
1042
1043
1044
1045
1046
1047
1048
5 unmodified lines

1054
1055
1056
1053
1054
1057
1058
1059
1056
1060
1061
1062
1063
1064
1061
1065
1066
1063
1064
1065
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1074
1100
1101
1076
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
73 unmodified lines

1210
1211
1212
1156
1213
1214
1215
1216
77 unmodified lines

1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419

1037 unmodified lines

}
}

func TestCanIncrementalRelayRejectsNoThin(t *testing.T) {
func TestCanIncrementalRelayToleratesNoThin(t *testing.T) {
	// Incremental relay tolerates "no-thin" targets because gitproto.FetchPack
	// never requests the thin-pack capability, so the relayed pack is always
	// self-contained and safe for no-thin receive-pack servers — same logic
	// as SupportsReplicateRelay.
	plans := []BranchPlan{{
		Branch:     "main",
		SourceRef:  "refs/heads/main",
5 unmodified lines
	}}

ok, reason := CanIncrementalRelay(false, false, false, plans, RelayTargetPolicy{CapabilitiesKnown: true, NoThin: true})
	if ok {
		t.Fatal("expected CanIncrementalRelay=false when target advertises no-thin")
	}
	if !ok {
		t.Fatalf("expected CanIncrementalRelay=true for no-thin target, got reason=%s", reason)
	}
	if reason != "incremental-target-no-thin" {
		if reason != reasonIncrementalEligible {
			t.Fatalf("unexpected reason: %s", reason)
		}
	}
}

func TestCanIncrementalRelayRejectsBranchCreate(t *testing.T) {
func TestCanIncrementalRelayAcceptsBranchCreate(t *testing.T) {
	plans := []BranchPlan{{
		Branch:     "main",
		SourceRef:  "refs/heads/main",
		TargetRef:  "refs/heads/main",
		Branch:     "feature",
		SourceRef:  "refs/heads/feature",
		TargetRef:  "refs/heads/feature",
		SourceHash: plumbing.NewHash("1111111111111111111111111111111111111111"),
		TargetHash: plumbing.ZeroHash,
		Kind:       RefKindBranch,
		Action:     ActionCreate,
	}}

ok, reason := CanIncrementalRelay(false, false, false, plans, RelayTargetPolicy{CapabilitiesKnown: true})
	if !ok {
			t.Fatalf("expected CanIncrementalRelay=true for branch create, got reason=%s", reason)
	}
	if reason != reasonIncrementalEligible {
		t.Fatalf("unexpected reason: %s", reason)
	}
}

func TestCanIncrementalRelayRejectsBranchCreateWithNonZeroTarget(t *testing.T) {
	// A "Create" plan with a non-zero TargetHash is incoherent — surface it
	// rather than silently relay against the wrong have.
	plans := []BranchPlan{{
		Branch:     "feature",
		SourceRef:  "refs/heads/feature",
		TargetRef:  "refs/heads/feature",
		SourceHash: plumbing.NewHash("1111111111111111111111111111111111111111"),
		TargetHash: plumbing.NewHash("2222222222222222222222222222222222222222"),
		Kind:       RefKindBranch,
		Action:     ActionCreate,
	}}

ok, reason := CanIncrementalRelay(false, false, false, plans, RelayTargetPolicy{CapabilitiesKnown: true})
	if ok {
		t.Fatal("expected CanIncrementalRelay=false for branch create")
		t.Fatal("expected CanIncrementalRelay=false for create plan with non-zero target hash")
	}
	if reason != "incremental-branch-action-not-update" {
		if reason != "incremental-branch-create-target-not-empty" {
			t.Fatalf("unexpected reason: %s", reason)
		}
	}
}

func TestCanIncrementalRelayMixedCreateAndUpdate(t *testing.T) {
	plans := []BranchPlan{{
		Branch:     "main",
		SourceRef:  "refs/heads/main",
		TargetRef:  "refs/heads/main",
		SourceHash: plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
		TargetHash: plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
		Kind:       RefKindBranch,
		Action:     ActionUpdate,
	},
	{
		Branch:     "feature",
		SourceRef:  "refs/heads/feature",
		TargetRef:  "refs/heads/feature",
		SourceHash: plumbing.NewHash("cccccccccccccccccccccccccccccccccccccccc"),
		TargetHash: plumbing.ZeroHash,
		Kind:       RefKindBranch,
		Action:     ActionCreate,
	}}

ok, reason := CanIncrementalRelay(false, false, false, plans, RelayTargetPolicy{CapabilitiesKnown: true})
	if !ok {
		t.Fatalf("expected CanIncrementalRelay=true for mixed create+update, got reason=%s", reason)
	}
	if reason != reasonIncrementalEligible {
		t.Fatalf("unexpected reason: %s", reason)
	}
}
73 unmodified lines

}}
