FossilRepo

Add push and bidirectional sync to Fossil remote CLI: push() and sync() methods wrapping fossil push/sync commands. View: push and sync_bidirectional actions on the sync page alongside existing pull. Template: Pull/Push/Sync buttons with distinct icons.

lmata 2026-04-07 18:48 trunk
Commit 23ff229f2427bf49a1ce80804999aa73c662a3dfc97842c02d51fc2372af311e
--- fossil/cli.py
+++ fossil/cli.py
@@ -165,10 +165,38 @@
165165
finally:
166166
import shutil
167167
168168
shutil.rmtree(tmpdir, ignore_errors=True)
169169
return lines
170
+
171
+ def push(self, repo_path: Path, remote_url: str = "") -> dict:
172
+ """Push to the remote. Returns {success, artifacts_sent, message}."""
173
+ import re
174
+
175
+ cmd = [self.binary, "push", "-R", str(repo_path)]
176
+ if remote_url:
177
+ cmd.append(remote_url)
178
+ try:
179
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, env=self._env)
180
+ artifacts = 0
181
+ m = re.search(r"sent:\s*(\d+)", result.stdout)
182
+ if m:
183
+ artifacts = int(m.group(1))
184
+ return {"success": result.returncode == 0, "artifacts_sent": artifacts, "message": result.stdout.strip()}
185
+ except (FileNotFoundError, subprocess.TimeoutExpired) as e:
186
+ return {"success": False, "artifacts_sent": 0, "message": str(e)}
187
+
188
+ def sync(self, repo_path: Path, remote_url: str = "") -> dict:
189
+ """Bidirectional sync with remote. Returns {success, message}."""
190
+ cmd = [self.binary, "sync", "-R", str(repo_path)]
191
+ if remote_url:
192
+ cmd.append(remote_url)
193
+ try:
194
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, env=self._env)
195
+ return {"success": result.returncode == 0, "message": result.stdout.strip()}
196
+ except (FileNotFoundError, subprocess.TimeoutExpired) as e:
197
+ return {"success": False, "message": str(e)}
170198
171199
def pull(self, repo_path: Path) -> dict:
172200
"""Pull updates from the remote. Returns {success, artifacts_received, message}."""
173201
try:
174202
result = subprocess.run(
175203
--- fossil/cli.py
+++ fossil/cli.py
@@ -165,10 +165,38 @@
165 finally:
166 import shutil
167
168 shutil.rmtree(tmpdir, ignore_errors=True)
169 return lines
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
171 def pull(self, repo_path: Path) -> dict:
172 """Pull updates from the remote. Returns {success, artifacts_received, message}."""
173 try:
174 result = subprocess.run(
175
--- fossil/cli.py
+++ fossil/cli.py
@@ -165,10 +165,38 @@
165 finally:
166 import shutil
167
168 shutil.rmtree(tmpdir, ignore_errors=True)
169 return lines
170
171 def push(self, repo_path: Path, remote_url: str = "") -> dict:
172 """Push to the remote. Returns {success, artifacts_sent, message}."""
173 import re
174
175 cmd = [self.binary, "push", "-R", str(repo_path)]
176 if remote_url:
177 cmd.append(remote_url)
178 try:
179 result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, env=self._env)
180 artifacts = 0
181 m = re.search(r"sent:\s*(\d+)", result.stdout)
182 if m:
183 artifacts = int(m.group(1))
184 return {"success": result.returncode == 0, "artifacts_sent": artifacts, "message": result.stdout.strip()}
185 except (FileNotFoundError, subprocess.TimeoutExpired) as e:
186 return {"success": False, "artifacts_sent": 0, "message": str(e)}
187
188 def sync(self, repo_path: Path, remote_url: str = "") -> dict:
189 """Bidirectional sync with remote. Returns {success, message}."""
190 cmd = [self.binary, "sync", "-R", str(repo_path)]
191 if remote_url:
192 cmd.append(remote_url)
193 try:
194 result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, env=self._env)
195 return {"success": result.returncode == 0, "message": result.stdout.strip()}
196 except (FileNotFoundError, subprocess.TimeoutExpired) as e:
197 return {"success": False, "message": str(e)}
198
199 def pull(self, repo_path: Path) -> dict:
200 """Pull updates from the remote. Returns {success, artifacts_received, message}."""
201 try:
202 result = subprocess.run(
203
--- fossil/views.py
+++ fossil/views.py
@@ -1450,10 +1450,45 @@
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')}")
1489
+
14551490
elif action == "pull" and fossil_repo.remote_url:
14561491
if cli.is_available():
14571492
cli.ensure_default_user(fossil_repo.full_path)
14581493
result = cli.pull(fossil_repo.full_path)
14591494
if result["success"]:
14601495
--- fossil/views.py
+++ fossil/views.py
@@ -1450,10 +1450,45 @@
1450 messages.info(request, "Sync disabled.")
1451 from django.shortcuts import redirect
1452
1453 return redirect("fossil:sync", slug=slug)
1454
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1455 elif action == "pull" and fossil_repo.remote_url:
1456 if cli.is_available():
1457 cli.ensure_default_user(fossil_repo.full_path)
1458 result = cli.pull(fossil_repo.full_path)
1459 if result["success"]:
1460
--- fossil/views.py
+++ fossil/views.py
@@ -1450,10 +1450,45 @@
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 if result["success"]:
1495
--- templates/fossil/sync.html
+++ templates/fossil/sync.html
@@ -31,26 +31,46 @@
3131
<dt class="text-sm text-gray-400">Repository size</dt>
3232
<dd class="text-sm text-gray-200">{{ fossil_repo.file_size_bytes|filesizeformat }}</dd>
3333
</div>
3434
</dl>
3535
36
- <div class="mt-5 flex items-center gap-3">
36
+ <div class="mt-5 flex items-center gap-3 flex-wrap">
3737
<form method="post">
3838
{% csrf_token %}
3939
<input type="hidden" name="action" value="pull">
4040
<button type="submit" class="inline-flex items-center gap-2 rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
4141
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
42
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
43
+ </svg>
44
+ Pull
45
+ </button>
46
+ </form>
47
+ <form method="post">
48
+ {% csrf_token %}
49
+ <input type="hidden" name="action" value="push">
50
+ <button type="submit" class="inline-flex items-center gap-2 rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-200 shadow-sm hover:bg-gray-600 ring-1 ring-gray-600">
51
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
52
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
53
+ </svg>
54
+ Push
55
+ </button>
56
+ </form>
57
+ <form method="post">
58
+ {% csrf_token %}
59
+ <input type="hidden" name="action" value="sync_bidirectional">
60
+ <button type="submit" class="inline-flex items-center gap-2 rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-200 shadow-sm hover:bg-gray-600 ring-1 ring-gray-600">
61
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
4262
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182" />
4363
</svg>
44
- Pull from Upstream
64
+ Sync
4565
</button>
4666
</form>
4767
<form method="post">
4868
{% csrf_token %}
4969
<input type="hidden" name="action" value="disable">
50
- <button type="submit" class="rounded-md bg-gray-700 px-3 py-2 text-sm text-gray-400 hover:text-white ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
51
- Disable Sync
70
+ <button type="submit" class="rounded-md bg-gray-800 px-3 py-2 text-sm text-gray-500 hover:text-gray-300 ring-1 ring-inset ring-gray-700 hover:bg-gray-700">
71
+ Disable
5272
</button>
5373
</form>
5474
</div>
5575
</div>
5676
5777
--- templates/fossil/sync.html
+++ templates/fossil/sync.html
@@ -31,26 +31,46 @@
31 <dt class="text-sm text-gray-400">Repository size</dt>
32 <dd class="text-sm text-gray-200">{{ fossil_repo.file_size_bytes|filesizeformat }}</dd>
33 </div>
34 </dl>
35
36 <div class="mt-5 flex items-center gap-3">
37 <form method="post">
38 {% csrf_token %}
39 <input type="hidden" name="action" value="pull">
40 <button type="submit" class="inline-flex items-center gap-2 rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
41 <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42 <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182" />
43 </svg>
44 Pull from Upstream
45 </button>
46 </form>
47 <form method="post">
48 {% csrf_token %}
49 <input type="hidden" name="action" value="disable">
50 <button type="submit" class="rounded-md bg-gray-700 px-3 py-2 text-sm text-gray-400 hover:text-white ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
51 Disable Sync
52 </button>
53 </form>
54 </div>
55 </div>
56
57
--- templates/fossil/sync.html
+++ templates/fossil/sync.html
@@ -31,26 +31,46 @@
31 <dt class="text-sm text-gray-400">Repository size</dt>
32 <dd class="text-sm text-gray-200">{{ fossil_repo.file_size_bytes|filesizeformat }}</dd>
33 </div>
34 </dl>
35
36 <div class="mt-5 flex items-center gap-3 flex-wrap">
37 <form method="post">
38 {% csrf_token %}
39 <input type="hidden" name="action" value="pull">
40 <button type="submit" class="inline-flex items-center gap-2 rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
41 <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
42 <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
43 </svg>
44 Pull
45 </button>
46 </form>
47 <form method="post">
48 {% csrf_token %}
49 <input type="hidden" name="action" value="push">
50 <button type="submit" class="inline-flex items-center gap-2 rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-200 shadow-sm hover:bg-gray-600 ring-1 ring-gray-600">
51 <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
52 <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
53 </svg>
54 Push
55 </button>
56 </form>
57 <form method="post">
58 {% csrf_token %}
59 <input type="hidden" name="action" value="sync_bidirectional">
60 <button type="submit" class="inline-flex items-center gap-2 rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-200 shadow-sm hover:bg-gray-600 ring-1 ring-gray-600">
61 <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
62 <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182" />
63 </svg>
64 Sync
65 </button>
66 </form>
67 <form method="post">
68 {% csrf_token %}
69 <input type="hidden" name="action" value="disable">
70 <button type="submit" class="rounded-md bg-gray-800 px-3 py-2 text-sm text-gray-500 hover:text-gray-300 ring-1 ring-inset ring-gray-700 hover:bg-gray-700">
71 Disable
72 </button>
73 </form>
74 </div>
75 </div>
76
77

Keyboard Shortcuts

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