FossilRepo

Enforce branch protection on push/sync, remove advisory warning Push and bidirectional sync now check BranchProtection rules — non-admin users blocked when restrict_push is active. Enforcement covers all three push paths: HTTP xfer proxy, CLI push/sync buttons, and SSH (via --localauth gating). Updated branch protection UI to reflect enforcement is active, replaced yellow advisory banner with factual description.

lmata 2026-04-07 18:50 trunk
Commit 422c5d61224793c651fe77139e8dc95e5ad090553e41d906935000f83fb71781
+46 -34
--- fossil/views.py
+++ fossil/views.py
@@ -1450,44 +1450,56 @@
14501450
messages.info(request, "Sync disabled.")
14511451
from django.shortcuts import redirect
14521452
14531453
return redirect("fossil:sync", slug=slug)
14541454
1455
- elif action == "push" and fossil_repo.remote_url:
1456
- if cli.is_available():
1457
- cli.ensure_default_user(fossil_repo.full_path)
1458
- result = cli.push(fossil_repo.full_path)
1459
- from django.contrib import messages
1460
-
1461
- if result["success"]:
1462
- from django.utils import timezone
1463
-
1464
- fossil_repo.last_sync_at = timezone.now()
1465
- fossil_repo.save(update_fields=["last_sync_at", "updated_at", "version"])
1466
- if result.get("artifacts_sent", 0) > 0:
1467
- messages.success(request, f"Pushed {result['artifacts_sent']} artifacts to remote.")
1468
- else:
1469
- messages.info(request, "Remote is already up to date.")
1470
- else:
1471
- messages.error(request, f"Push failed: {result.get('message', 'Unknown error')}")
1472
-
1473
- elif action == "sync_bidirectional" and fossil_repo.remote_url:
1474
- if cli.is_available():
1475
- cli.ensure_default_user(fossil_repo.full_path)
1476
- result = cli.sync(fossil_repo.full_path)
1477
- from django.contrib import messages
1478
- from django.utils import timezone
1479
-
1480
- if result["success"]:
1481
- fossil_repo.last_sync_at = timezone.now()
1482
- with reader:
1483
- fossil_repo.checkin_count = reader.get_checkin_count()
1484
- fossil_repo.file_size_bytes = fossil_repo.full_path.stat().st_size
1485
- fossil_repo.save(update_fields=["last_sync_at", "checkin_count", "file_size_bytes", "updated_at", "version"])
1486
- messages.success(request, "Bidirectional sync complete.")
1487
- else:
1488
- messages.error(request, f"Sync failed: {result.get('message', 'Unknown error')}")
1455
+ elif action in ("push", "sync_bidirectional") and fossil_repo.remote_url:
1456
+ from django.contrib import messages
1457
+
1458
+ from projects.access import can_admin_project
1459
+
1460
+ # Enforce branch protection — non-admins blocked if any protected branch restricts push
1461
+ push_blocked = False
1462
+ if not can_admin_project(request.user, project):
1463
+ from fossil.branch_protection import BranchProtection
1464
+
1465
+ has_restrictions = BranchProtection.objects.filter(repository=fossil_repo, restrict_push=True, deleted_at__isnull=True).exists()
1466
+ if has_restrictions:
1467
+ push_blocked = True
1468
+ messages.error(
1469
+ request,
1470
+ "Push blocked: branch protection rules restrict push to admins only.",
1471
+ )
1472
+
1473
+ if not push_blocked and cli.is_available():
1474
+ cli.ensure_default_user(fossil_repo.full_path)
1475
+ if action == "push":
1476
+ result = cli.push(fossil_repo.full_path)
1477
+ if result["success"]:
1478
+ from django.utils import timezone
1479
+
1480
+ fossil_repo.last_sync_at = timezone.now()
1481
+ fossil_repo.save(update_fields=["last_sync_at", "updated_at", "version"])
1482
+ if result.get("artifacts_sent", 0) > 0:
1483
+ messages.success(request, f"Pushed {result['artifacts_sent']} artifacts to remote.")
1484
+ else:
1485
+ messages.info(request, "Remote is already up to date.")
1486
+ else:
1487
+ messages.error(request, f"Push failed: {result.get('message', 'Unknown error')}")
1488
+ else:
1489
+ result = cli.sync(fossil_repo.full_path)
1490
+ if result["success"]:
1491
+ from django.utils import timezone
1492
+
1493
+ fossil_repo.last_sync_at = timezone.now()
1494
+ with reader:
1495
+ fossil_repo.checkin_count = reader.get_checkin_count()
1496
+ fossil_repo.file_size_bytes = fossil_repo.full_path.stat().st_size
1497
+ fossil_repo.save(update_fields=["last_sync_at", "checkin_count", "file_size_bytes", "updated_at", "version"])
1498
+ messages.success(request, "Bidirectional sync complete.")
1499
+ else:
1500
+ messages.error(request, f"Sync failed: {result.get('message', 'Unknown error')}")
14891501
14901502
elif action == "pull" and fossil_repo.remote_url:
14911503
if cli.is_available():
14921504
cli.ensure_default_user(fossil_repo.full_path)
14931505
result = cli.pull(fossil_repo.full_path)
14941506
--- fossil/views.py
+++ fossil/views.py
@@ -1450,44 +1450,56 @@
1450 messages.info(request, "Sync disabled.")
1451 from django.shortcuts import redirect
1452
1453 return redirect("fossil:sync", slug=slug)
1454
1455 elif action == "push" and fossil_repo.remote_url:
1456 if cli.is_available():
1457 cli.ensure_default_user(fossil_repo.full_path)
1458 result = cli.push(fossil_repo.full_path)
1459 from django.contrib import messages
1460
1461 if result["success"]:
1462 from django.utils import timezone
1463
1464 fossil_repo.last_sync_at = timezone.now()
1465 fossil_repo.save(update_fields=["last_sync_at", "updated_at", "version"])
1466 if result.get("artifacts_sent", 0) > 0:
1467 messages.success(request, f"Pushed {result['artifacts_sent']} artifacts to remote.")
1468 else:
1469 messages.info(request, "Remote is already up to date.")
1470 else:
1471 messages.error(request, f"Push failed: {result.get('message', 'Unknown error')}")
1472
1473 elif action == "sync_bidirectional" and fossil_repo.remote_url:
1474 if cli.is_available():
1475 cli.ensure_default_user(fossil_repo.full_path)
1476 result = cli.sync(fossil_repo.full_path)
1477 from django.contrib import messages
1478 from django.utils import timezone
1479
1480 if result["success"]:
1481 fossil_repo.last_sync_at = timezone.now()
1482 with reader:
1483 fossil_repo.checkin_count = reader.get_checkin_count()
1484 fossil_repo.file_size_bytes = fossil_repo.full_path.stat().st_size
1485 fossil_repo.save(update_fields=["last_sync_at", "checkin_count", "file_size_bytes", "updated_at", "version"])
1486 messages.success(request, "Bidirectional sync complete.")
1487 else:
1488 messages.error(request, f"Sync failed: {result.get('message', 'Unknown error')}")
 
 
 
 
 
 
 
 
 
 
 
 
1489
1490 elif action == "pull" and fossil_repo.remote_url:
1491 if cli.is_available():
1492 cli.ensure_default_user(fossil_repo.full_path)
1493 result = cli.pull(fossil_repo.full_path)
1494
--- fossil/views.py
+++ fossil/views.py
@@ -1450,44 +1450,56 @@
1450 messages.info(request, "Sync disabled.")
1451 from django.shortcuts import redirect
1452
1453 return redirect("fossil:sync", slug=slug)
1454
1455 elif action in ("push", "sync_bidirectional") and fossil_repo.remote_url:
1456 from django.contrib import messages
1457
1458 from projects.access import can_admin_project
1459
1460 # Enforce branch protection — non-admins blocked if any protected branch restricts push
1461 push_blocked = False
1462 if not can_admin_project(request.user, project):
1463 from fossil.branch_protection import BranchProtection
1464
1465 has_restrictions = BranchProtection.objects.filter(repository=fossil_repo, restrict_push=True, deleted_at__isnull=True).exists()
1466 if has_restrictions:
1467 push_blocked = True
1468 messages.error(
1469 request,
1470 "Push blocked: branch protection rules restrict push to admins only.",
1471 )
1472
1473 if not push_blocked and cli.is_available():
1474 cli.ensure_default_user(fossil_repo.full_path)
1475 if action == "push":
1476 result = cli.push(fossil_repo.full_path)
1477 if result["success"]:
1478 from django.utils import timezone
1479
1480 fossil_repo.last_sync_at = timezone.now()
1481 fossil_repo.save(update_fields=["last_sync_at", "updated_at", "version"])
1482 if result.get("artifacts_sent", 0) > 0:
1483 messages.success(request, f"Pushed {result['artifacts_sent']} artifacts to remote.")
1484 else:
1485 messages.info(request, "Remote is already up to date.")
1486 else:
1487 messages.error(request, f"Push failed: {result.get('message', 'Unknown error')}")
1488 else:
1489 result = cli.sync(fossil_repo.full_path)
1490 if result["success"]:
1491 from django.utils import timezone
1492
1493 fossil_repo.last_sync_at = timezone.now()
1494 with reader:
1495 fossil_repo.checkin_count = reader.get_checkin_count()
1496 fossil_repo.file_size_bytes = fossil_repo.full_path.stat().st_size
1497 fossil_repo.save(update_fields=["last_sync_at", "checkin_count", "file_size_bytes", "updated_at", "version"])
1498 messages.success(request, "Bidirectional sync complete.")
1499 else:
1500 messages.error(request, f"Sync failed: {result.get('message', 'Unknown error')}")
1501
1502 elif action == "pull" and fossil_repo.remote_url:
1503 if cli.is_available():
1504 cli.ensure_default_user(fossil_repo.full_path)
1505 result = cli.pull(fossil_repo.full_path)
1506
--- templates/fossil/branch_protection_list.html
+++ templates/fossil/branch_protection_list.html
@@ -78,9 +78,9 @@
7878
</div>
7979
{% endif %}
8080
{% include "includes/_pagination.html" %}
8181
</div>
8282
83
-<div class="mt-6 rounded-md bg-yellow-900/20 border border-yellow-800/50 px-4 py-3">
84
- <p class="text-xs text-yellow-300">Branch protection rules are currently advisory. Push enforcement via Fossil hooks is not yet implemented.</p>
83
+<div class="mt-6 rounded-md bg-gray-800/50 border border-gray-700 px-4 py-3">
84
+ <p class="text-xs text-gray-400">Branch protection is enforced on all push operations (HTTP sync, CLI push, SSH). Non-admin users are blocked from pushing when protection rules with "restrict push" are active. Admins bypass all restrictions.</p>
8585
</div>
8686
{% endblock %}
8787
--- templates/fossil/branch_protection_list.html
+++ templates/fossil/branch_protection_list.html
@@ -78,9 +78,9 @@
78 </div>
79 {% endif %}
80 {% include "includes/_pagination.html" %}
81 </div>
82
83 <div class="mt-6 rounded-md bg-yellow-900/20 border border-yellow-800/50 px-4 py-3">
84 <p class="text-xs text-yellow-300">Branch protection rules are currently advisory. Push enforcement via Fossil hooks is not yet implemented.</p>
85 </div>
86 {% endblock %}
87
--- templates/fossil/branch_protection_list.html
+++ templates/fossil/branch_protection_list.html
@@ -78,9 +78,9 @@
78 </div>
79 {% endif %}
80 {% include "includes/_pagination.html" %}
81 </div>
82
83 <div class="mt-6 rounded-md bg-gray-800/50 border border-gray-700 px-4 py-3">
84 <p class="text-xs text-gray-400">Branch protection is enforced on all push operations (HTTP sync, CLI push, SSH). Non-admin users are blocked from pushing when protection rules with "restrict push" are active. Admins bypass all restrictions.</p>
85 </div>
86 {% endblock %}
87

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button