collab-runtime 0.9.0__tar.gz → 0.9.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {collab_runtime-0.9.0/collab_runtime.egg-info → collab_runtime-0.9.2}/PKG-INFO +3 -2
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard_server.py +2 -1
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/live_locks_watcher.py +4 -68
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/lock_client.py +13 -39
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/overlap.py +23 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/safe_subprocess.py +1 -1
- {collab_runtime-0.9.0 → collab_runtime-0.9.2/collab_runtime.egg-info}/PKG-INFO +3 -2
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/requires.txt +1 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/pyproject.toml +4 -3
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/LICENSE +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/README.md +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/__init__.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/__main__.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/agent_hooks.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/agent_identity.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard/dashboard-charts.js +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard/dashboard-filters.js +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard/dashboard-format.js +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard/index.html +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/errors.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/githooks.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/commit-msg +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/post-checkout +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/post-commit +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/post-merge +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/pre-commit +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/pre-push +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/logging_config.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/main.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/platform_probe.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/pr_overlap.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/subprocess_bridge.py +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/SOURCES.txt +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/dependency_links.txt +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/entry_points.txt +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/top_level.txt +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/docs/pypi/README.md +0 -0
- {collab_runtime-0.9.0 → collab_runtime-0.9.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: collab-runtime
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Collaborative file locking runtime
|
|
5
5
|
Author-email: KirilMT <kiril.mt95@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Topic :: Software Development :: Version Control
|
|
16
|
-
Requires-Python:
|
|
16
|
+
Requires-Python: <3.14,>=3.10
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: python-dotenv>=1.2.2
|
|
@@ -21,6 +21,7 @@ Requires-Dist: supabase>=2.30.0
|
|
|
21
21
|
Requires-Dist: httpx>=0.28.1
|
|
22
22
|
Requires-Dist: psutil>=5.9.8
|
|
23
23
|
Requires-Dist: plyer>=2.1.0
|
|
24
|
+
Requires-Dist: tomli>=2.0.0
|
|
24
25
|
Dynamic: license-file
|
|
25
26
|
|
|
26
27
|
# collab-runtime — Collaborative File Locking
|
|
@@ -17,12 +17,13 @@ import re
|
|
|
17
17
|
import tempfile
|
|
18
18
|
import threading
|
|
19
19
|
import time
|
|
20
|
-
import tomllib
|
|
21
20
|
import urllib.parse
|
|
22
21
|
import zipfile
|
|
23
22
|
from pathlib import Path
|
|
24
23
|
from typing import Any, Callable, Iterable, Optional, Sequence, Tuple
|
|
25
24
|
|
|
25
|
+
import tomli as tomllib
|
|
26
|
+
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
27
28
|
|
|
28
29
|
DASHBOARD_TEMP_PREFIX = ".collab-dashboard-"
|
|
@@ -781,26 +781,8 @@ def _process_releases(client, released: set[str]) -> None:
|
|
|
781
781
|
|
|
782
782
|
Extracted so tests can simulate exceptions when removing locks from the local-owned
|
|
783
783
|
set without running the entire main loop.
|
|
784
|
-
|
|
785
|
-
Enforces a minimum hold time to avoid rapid acquire/release cycles when git status
|
|
786
|
-
is transient.
|
|
787
784
|
"""
|
|
788
785
|
for fp in released:
|
|
789
|
-
# Enforce minimum lock hold time: skip files whose lock was acquired
|
|
790
|
-
# too recently to avoid thrashing when git status fluctuates.
|
|
791
|
-
acquired = _lock_acquired_at.get(fp)
|
|
792
|
-
if acquired is not None:
|
|
793
|
-
age = (datetime.now() - acquired).total_seconds()
|
|
794
|
-
if age < _min_auto_lock_hold_seconds():
|
|
795
|
-
logger.debug(
|
|
796
|
-
"⏳ [KEPT] %s — lock is only %ds old "
|
|
797
|
-
"(< %ds minimum); deferring auto-release",
|
|
798
|
-
fp,
|
|
799
|
-
int(age),
|
|
800
|
-
_min_auto_lock_hold_seconds(),
|
|
801
|
-
)
|
|
802
|
-
continue
|
|
803
|
-
|
|
804
786
|
# Was this file in conflict?
|
|
805
787
|
if fp in _active_conflicts:
|
|
806
788
|
_active_conflicts.discard(fp)
|
|
@@ -1205,25 +1187,10 @@ def _reconcile_on_startup(client) -> None:
|
|
|
1205
1187
|
msg = f"[RESUMED] {fp} - lock re-adopted from this machine"
|
|
1206
1188
|
logger.info(_color(msg, Fore.GREEN) if _HAS_COLORAMA else msg)
|
|
1207
1189
|
else:
|
|
1208
|
-
# File is clean —
|
|
1209
|
-
#
|
|
1210
|
-
#
|
|
1211
|
-
#
|
|
1212
|
-
acquired = _lock_acquired_at.get(fp)
|
|
1213
|
-
if acquired is not None:
|
|
1214
|
-
age = (datetime.now() - acquired).total_seconds()
|
|
1215
|
-
if age < _min_auto_lock_hold_seconds():
|
|
1216
|
-
logger.debug(
|
|
1217
|
-
"⏳ [KEPT] %s — lock is only %ds old "
|
|
1218
|
-
"(< %ds minimum); skipping auto-release",
|
|
1219
|
-
fp,
|
|
1220
|
-
int(age),
|
|
1221
|
-
_min_auto_lock_hold_seconds(),
|
|
1222
|
-
)
|
|
1223
|
-
# Keep the lock in _local_owned_locks so the main loop
|
|
1224
|
-
# continues to track it for eventual release.
|
|
1225
|
-
_local_owned_locks.add(fp)
|
|
1226
|
-
continue
|
|
1190
|
+
# File is clean — release immediately.
|
|
1191
|
+
# Unlike agent locks, the developer's own watcher has authoritative
|
|
1192
|
+
# knowledge of the working tree. Delaying here would leave
|
|
1193
|
+
# post-merge / post-pull files locked (#150, #151).
|
|
1227
1194
|
|
|
1228
1195
|
# File is clean - stale lock, release it
|
|
1229
1196
|
try:
|
|
@@ -1522,22 +1489,6 @@ def _graceful_shutdown() -> None:
|
|
|
1522
1489
|
msg = f"[KEPT] {fp} - still has local edits, lock preserved"
|
|
1523
1490
|
logger.debug(_color(msg, Fore.GREEN) if _HAS_COLORAMA else msg)
|
|
1524
1491
|
else:
|
|
1525
|
-
# Enforce minimum hold time even during shutdown:
|
|
1526
|
-
# keep locks that were acquired too recently.
|
|
1527
|
-
acquired = _lock_acquired_at.get(fp)
|
|
1528
|
-
if acquired is not None:
|
|
1529
|
-
age = (datetime.now() - acquired).total_seconds()
|
|
1530
|
-
if age < _min_auto_lock_hold_seconds():
|
|
1531
|
-
n_kept += 1
|
|
1532
|
-
logger.debug(
|
|
1533
|
-
"⏳ [KEPT] %s — lock is only %ds old "
|
|
1534
|
-
"(< %ds minimum); preserving",
|
|
1535
|
-
fp,
|
|
1536
|
-
int(age),
|
|
1537
|
-
_min_auto_lock_hold_seconds(),
|
|
1538
|
-
)
|
|
1539
|
-
continue
|
|
1540
|
-
|
|
1541
1492
|
try:
|
|
1542
1493
|
_scope_agent(
|
|
1543
1494
|
client.table("file_locks")
|
|
@@ -1571,21 +1522,6 @@ def _graceful_shutdown() -> None:
|
|
|
1571
1522
|
]
|
|
1572
1523
|
for fp in db_locks:
|
|
1573
1524
|
if fp and fp not in still_dirty:
|
|
1574
|
-
# Enforce minimum hold time
|
|
1575
|
-
acquired = _lock_acquired_at.get(fp)
|
|
1576
|
-
if acquired is not None:
|
|
1577
|
-
age = (datetime.now() - acquired).total_seconds()
|
|
1578
|
-
if age < _min_auto_lock_hold_seconds():
|
|
1579
|
-
n_kept += 1
|
|
1580
|
-
logger.debug(
|
|
1581
|
-
"⏳ [KEPT] %s — lock is only %ds old "
|
|
1582
|
-
"(< %ds minimum); preserving",
|
|
1583
|
-
fp,
|
|
1584
|
-
int(age),
|
|
1585
|
-
_min_auto_lock_hold_seconds(),
|
|
1586
|
-
)
|
|
1587
|
-
continue
|
|
1588
|
-
|
|
1589
1525
|
_scope_agent(
|
|
1590
1526
|
client.table("file_locks")
|
|
1591
1527
|
.delete()
|
|
@@ -3633,46 +3633,20 @@ class LockClient:
|
|
|
3633
3633
|
if any([n_released, n_newly_locked, n_readopted, n_refreshed, n_multi]):
|
|
3634
3634
|
logger.debug("Starting lock reconciliation...")
|
|
3635
3635
|
|
|
3636
|
-
# Process stale locks —
|
|
3637
|
-
#
|
|
3638
|
-
# the
|
|
3636
|
+
# Process stale locks — release immediately because git status
|
|
3637
|
+
# confirms the file is clean. Unlike agent locks (dev_other_stale),
|
|
3638
|
+
# the developer's own watcher has authoritative knowledge of the
|
|
3639
|
+
# working tree. Delaying here (e.g. via a minimum hold time) would
|
|
3640
|
+
# leave post-merge / post-pull files locked for minutes (#150, #151).
|
|
3639
3641
|
if stale:
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
.astimezone()
|
|
3649
|
-
.replace(tzinfo=None)
|
|
3650
|
-
)
|
|
3651
|
-
age = (_safe_now() - acq_dt).total_seconds()
|
|
3652
|
-
if age < _min_auto_lock_hold_seconds():
|
|
3653
|
-
kept_young.add(fp)
|
|
3654
|
-
logger.debug(
|
|
3655
|
-
"⏳ [KEPT] %s — lock is only %ds old "
|
|
3656
|
-
"(< %ds minimum); skipping auto-release",
|
|
3657
|
-
fp,
|
|
3658
|
-
int(age),
|
|
3659
|
-
_min_auto_lock_hold_seconds(),
|
|
3660
|
-
)
|
|
3661
|
-
continue
|
|
3662
|
-
except (ValueError, TypeError):
|
|
3663
|
-
pass
|
|
3664
|
-
truly_stale.add(fp)
|
|
3665
|
-
|
|
3666
|
-
if truly_stale:
|
|
3667
|
-
for fp in sorted(truly_stale):
|
|
3668
|
-
logger.info(
|
|
3669
|
-
"🔓 [STALE-RELEASED] %s — locked but file is now clean,"
|
|
3670
|
-
" releasing",
|
|
3671
|
-
fp,
|
|
3672
|
-
)
|
|
3673
|
-
self.release_multiple(list(truly_stale))
|
|
3674
|
-
# Only count actually-released stale locks in the summary
|
|
3675
|
-
n_released = len(truly_stale)
|
|
3642
|
+
for fp in sorted(stale):
|
|
3643
|
+
logger.info(
|
|
3644
|
+
"🔓 [STALE-RELEASED] %s — locked but file is now clean,"
|
|
3645
|
+
" releasing",
|
|
3646
|
+
fp,
|
|
3647
|
+
)
|
|
3648
|
+
self.release_multiple(list(stale))
|
|
3649
|
+
n_released = len(stale)
|
|
3676
3650
|
|
|
3677
3651
|
# Process RESUMED locks: use direct table update (preserves acquired_at)
|
|
3678
3652
|
# This prevents the timer from resetting when switching IDEs
|
|
@@ -308,6 +308,11 @@ def merge_tree_conflicts(
|
|
|
308
308
|
conflicted.add(line.strip())
|
|
309
309
|
# rc==1 with nothing parseable -> inconclusive; fall back to file-level.
|
|
310
310
|
return frozenset(conflicted) if conflicted else None
|
|
311
|
+
# rc != 0,1: the merge-tree command itself failed (e.g. rc=128 "refusing to
|
|
312
|
+
# merge unrelated histories" in a shallow clone, or unsupported git version).
|
|
313
|
+
logger.debug(
|
|
314
|
+
"merge-tree exited rc=%d for %s..%s; inconclusive", rc, head_ref, other_ref
|
|
315
|
+
)
|
|
311
316
|
return None
|
|
312
317
|
|
|
313
318
|
|
|
@@ -376,6 +381,24 @@ def fetch_pr_ref(
|
|
|
376
381
|
"Fetched SHA %s != expected %s for PR #%d", fetched_sha, head_sha, pr_number
|
|
377
382
|
)
|
|
378
383
|
# Still usable (the ref exists), but log the discrepancy.
|
|
384
|
+
|
|
385
|
+
# If the checkout is shallow (e.g. actions/checkout@v4 default fetch-depth: 1),
|
|
386
|
+
# unshallow now so that git merge-tree can find the merge base. Without this,
|
|
387
|
+
# merge-tree exits rc=128 ("refusing to merge unrelated histories") and
|
|
388
|
+
# merge_tree_conflicts returns None (inconclusive).
|
|
389
|
+
rc_shallow, shallow_out = capture(["rev-parse", "--is-shallow-repository"])
|
|
390
|
+
if rc_shallow == 0 and shallow_out.strip() == "true":
|
|
391
|
+
logger.debug("Shallow clone detected; unshallowing from %s", remote)
|
|
392
|
+
rc_unshallow, _ = capture(["fetch", "--unshallow", remote])
|
|
393
|
+
if rc_unshallow != 0:
|
|
394
|
+
# Best-effort fallback: deepen history so merge-tree has a
|
|
395
|
+
# reasonable chance of finding the merge base.
|
|
396
|
+
logger.debug(
|
|
397
|
+
"--unshallow failed (rc=%d); trying --deepen=50 as fallback",
|
|
398
|
+
rc_unshallow,
|
|
399
|
+
)
|
|
400
|
+
capture(["fetch", "--deepen=50", remote])
|
|
401
|
+
|
|
379
402
|
return local_ref
|
|
380
403
|
|
|
381
404
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: collab-runtime
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Collaborative file locking runtime
|
|
5
5
|
Author-email: KirilMT <kiril.mt95@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Topic :: Software Development :: Version Control
|
|
16
|
-
Requires-Python:
|
|
16
|
+
Requires-Python: <3.14,>=3.10
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: python-dotenv>=1.2.2
|
|
@@ -21,6 +21,7 @@ Requires-Dist: supabase>=2.30.0
|
|
|
21
21
|
Requires-Dist: httpx>=0.28.1
|
|
22
22
|
Requires-Dist: psutil>=5.9.8
|
|
23
23
|
Requires-Dist: plyer>=2.1.0
|
|
24
|
+
Requires-Dist: tomli>=2.0.0
|
|
24
25
|
Dynamic: license-file
|
|
25
26
|
|
|
26
27
|
# collab-runtime — Collaborative File Locking
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "collab-runtime"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.2"
|
|
8
8
|
description = "Collaborative file locking runtime"
|
|
9
9
|
readme = "docs/pypi/README.md"
|
|
10
10
|
license = "MIT"
|
|
11
|
-
requires-python = ">=3.10"
|
|
11
|
+
requires-python = ">=3.10,<3.14"
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "KirilMT", email = "kiril.mt95@gmail.com" }
|
|
14
14
|
]
|
|
@@ -28,7 +28,8 @@ dependencies = [
|
|
|
28
28
|
"supabase>=2.30.0",
|
|
29
29
|
"httpx>=0.28.1",
|
|
30
30
|
"psutil>=5.9.8",
|
|
31
|
-
"plyer>=2.1.0"
|
|
31
|
+
"plyer>=2.1.0",
|
|
32
|
+
"tomli>=2.0.0",
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
[project.scripts]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|