kvgit 0.3.6__tar.gz → 0.3.7__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.
- {kvgit-0.3.6 → kvgit-0.3.7}/PKG-INFO +33 -1
- {kvgit-0.3.6 → kvgit-0.3.7}/README.md +32 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/staged.py +56 -1
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/versioned/base.py +69 -5
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/versioned/kv.py +15 -10
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/versioned/protocol.py +25 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit.egg-info/PKG-INFO +33 -1
- {kvgit-0.3.6 → kvgit-0.3.7}/pyproject.toml +1 -1
- {kvgit-0.3.6 → kvgit-0.3.7}/LICENSE +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/__init__.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/codecs/__init__.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/codecs/_hash.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/codecs/base.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/codecs/numpy.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/codecs/pandas.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/codecs/pickler.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/content_types.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/encoding.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/errors.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/hamt.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/kv/__init__.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/kv/base.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/kv/composite.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/kv/disk.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/kv/indexeddb.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/kv/memory.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/merges.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/namespaced.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/py.typed +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/store.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/versioned/__init__.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/versioned/helpers.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/versioned/keyset.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit/versioned/merge.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit.egg-info/SOURCES.txt +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit.egg-info/dependency_links.txt +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit.egg-info/requires.txt +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/kvgit.egg-info/top_level.txt +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/setup.cfg +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/tests/test_content_types.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/tests/test_delete_branches.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/tests/test_hamt.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/tests/test_merges.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/tests/test_namespaced.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/tests/test_staged.py +0 -0
- {kvgit-0.3.6 → kvgit-0.3.7}/tests/test_store_factory.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kvgit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Versioned key-value store with git-like commit, branch, and merge semantics.
|
|
5
5
|
Author: ashenfad
|
|
6
6
|
License: MIT
|
|
@@ -94,6 +94,38 @@ main.tag("v1")
|
|
|
94
94
|
print(main.peek("score", tag="v1")) # 0
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
## Merging
|
|
98
|
+
|
|
99
|
+
`Staged.merge()` merges another head -- usually another branch's HEAD --
|
|
100
|
+
into the current branch: lowest common ancestor, three-way resolve, and
|
|
101
|
+
a two-parent merge commit guarded on your own head:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
dev["score"] = 500
|
|
105
|
+
dev.commit()
|
|
106
|
+
|
|
107
|
+
result = main.merge(dev.current_commit) # True when merged
|
|
108
|
+
print(main["score"]) # 500 (fast-forward: main hadn't diverged)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Overlapping changes need a merge function per key, or a `default_merge`
|
|
112
|
+
fallback. `kvgit.merges.text` resolves line-oriented text with git-style
|
|
113
|
+
`<<<<<<<` markers (see `make_text_merge` for custom labels); anything it
|
|
114
|
+
cannot mark -- binary, non-UTF-8, oversized -- raises `CantMark`, filed
|
|
115
|
+
as an ordinary conflict:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from kvgit.merges import text
|
|
119
|
+
|
|
120
|
+
result = main.merge(dev.current_commit, default_merge=text)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A `post_check(key, merged_bytes)` predicate runs over every
|
|
124
|
+
merge-produced value; returning `False` files that key as conflicted.
|
|
125
|
+
`on_conflict="abandon"` leaves the branch untouched instead of raising.
|
|
126
|
+
Merging refuses with `ValueError` when the staging buffer holds
|
|
127
|
+
uncommitted changes -- commit or reset first.
|
|
128
|
+
|
|
97
129
|
## Chunked codecs (numpy / pandas)
|
|
98
130
|
|
|
99
131
|
Large numpy arrays and pandas DataFrames -- and any sliced views of them -- can be stored once and shared across keys, commits, and branches:
|
|
@@ -45,6 +45,38 @@ main.tag("v1")
|
|
|
45
45
|
print(main.peek("score", tag="v1")) # 0
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
## Merging
|
|
49
|
+
|
|
50
|
+
`Staged.merge()` merges another head -- usually another branch's HEAD --
|
|
51
|
+
into the current branch: lowest common ancestor, three-way resolve, and
|
|
52
|
+
a two-parent merge commit guarded on your own head:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
dev["score"] = 500
|
|
56
|
+
dev.commit()
|
|
57
|
+
|
|
58
|
+
result = main.merge(dev.current_commit) # True when merged
|
|
59
|
+
print(main["score"]) # 500 (fast-forward: main hadn't diverged)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Overlapping changes need a merge function per key, or a `default_merge`
|
|
63
|
+
fallback. `kvgit.merges.text` resolves line-oriented text with git-style
|
|
64
|
+
`<<<<<<<` markers (see `make_text_merge` for custom labels); anything it
|
|
65
|
+
cannot mark -- binary, non-UTF-8, oversized -- raises `CantMark`, filed
|
|
66
|
+
as an ordinary conflict:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from kvgit.merges import text
|
|
70
|
+
|
|
71
|
+
result = main.merge(dev.current_commit, default_merge=text)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A `post_check(key, merged_bytes)` predicate runs over every
|
|
75
|
+
merge-produced value; returning `False` files that key as conflicted.
|
|
76
|
+
`on_conflict="abandon"` leaves the branch untouched instead of raising.
|
|
77
|
+
Merging refuses with `ValueError` when the staging buffer holds
|
|
78
|
+
uncommitted changes -- commit or reset first.
|
|
79
|
+
|
|
48
80
|
## Chunked codecs (numpy / pandas)
|
|
49
81
|
|
|
50
82
|
Large numpy arrays and pandas DataFrames -- and any sliced views of them -- can be stored once and shared across keys, commits, and branches:
|
|
@@ -8,7 +8,13 @@ from typing import Any
|
|
|
8
8
|
from .codecs._hash import hash_bytes
|
|
9
9
|
from .content_types import MergeFn
|
|
10
10
|
from .versioned.kv import CHUNK_PREFIX, VersionedKV
|
|
11
|
-
from .versioned.protocol import
|
|
11
|
+
from .versioned.protocol import (
|
|
12
|
+
BytesMergeFn,
|
|
13
|
+
MergeResult,
|
|
14
|
+
PostCheck,
|
|
15
|
+
TagInfo,
|
|
16
|
+
Versioned,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
|
|
14
20
|
class _ChunkSink:
|
|
@@ -355,6 +361,55 @@ class Staged(MutableMapping[str, Any]):
|
|
|
355
361
|
self._cache.clear()
|
|
356
362
|
return result
|
|
357
363
|
|
|
364
|
+
def merge(
|
|
365
|
+
self,
|
|
366
|
+
their_head: str,
|
|
367
|
+
*,
|
|
368
|
+
on_conflict: str = "raise",
|
|
369
|
+
merge_fns: dict[str, MergeFn] | None = None,
|
|
370
|
+
default_merge: MergeFn | None = None,
|
|
371
|
+
post_check: PostCheck | None = None,
|
|
372
|
+
info: dict | None = None,
|
|
373
|
+
) -> MergeResult:
|
|
374
|
+
"""Merge another head (usually another branch's HEAD) into this branch.
|
|
375
|
+
|
|
376
|
+
Lowest common ancestor + three-way resolve + a two-parent merge
|
|
377
|
+
commit, CAS-guarded on our own head. Refuses with ``ValueError``
|
|
378
|
+
when the staging buffer holds uncommitted changes — commit or
|
|
379
|
+
reset first, so the merge reads committed heads on both sides.
|
|
380
|
+
|
|
381
|
+
``merge_fns`` / ``default_merge`` take decoded values (wrapped
|
|
382
|
+
like :meth:`commit`); ``post_check`` takes merged bytes.
|
|
383
|
+
"""
|
|
384
|
+
if self._updates or self._removals:
|
|
385
|
+
raise ValueError("cannot merge with staged changes; commit or reset first")
|
|
386
|
+
# Same assembly as commit(): registered fns first, per-call
|
|
387
|
+
# overrides on top — a configured Staged merges without repeats.
|
|
388
|
+
effective_fns = dict(self._merge_fns)
|
|
389
|
+
if merge_fns:
|
|
390
|
+
effective_fns.update(merge_fns)
|
|
391
|
+
effective_default = default_merge or self._default_merge
|
|
392
|
+
bytes_merge_fns = (
|
|
393
|
+
{key: self._wrap_merge_fn(fn) for key, fn in effective_fns.items()}
|
|
394
|
+
if effective_fns
|
|
395
|
+
else None
|
|
396
|
+
)
|
|
397
|
+
bytes_default = (
|
|
398
|
+
self._wrap_merge_fn(effective_default) if effective_default else None
|
|
399
|
+
)
|
|
400
|
+
result = self._versioned.merge_heads(
|
|
401
|
+
their_head,
|
|
402
|
+
on_conflict=on_conflict,
|
|
403
|
+
merge_fns=bytes_merge_fns,
|
|
404
|
+
default_merge=bytes_default,
|
|
405
|
+
post_check=post_check,
|
|
406
|
+
info=info,
|
|
407
|
+
)
|
|
408
|
+
if result.merged:
|
|
409
|
+
# HEAD moved: cached values may be stale after a merge.
|
|
410
|
+
self._cache.clear()
|
|
411
|
+
return result
|
|
412
|
+
|
|
358
413
|
def reset(self) -> None:
|
|
359
414
|
"""Discard all staged changes."""
|
|
360
415
|
self._updates.clear()
|
|
@@ -6,7 +6,7 @@ from collections.abc import Iterable
|
|
|
6
6
|
from ..errors import ConcurrencyError, MergeConflict
|
|
7
7
|
from .helpers import diff_keysets, walk_history
|
|
8
8
|
from .merge import MergeResolution, resolve_merge
|
|
9
|
-
from .protocol import BytesMergeFn, DiffResult, MergeResult
|
|
9
|
+
from .protocol import BytesMergeFn, DiffResult, MergeResult, PostCheck
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class VersionedBase(ABC):
|
|
@@ -223,10 +223,20 @@ class VersionedBase(ABC):
|
|
|
223
223
|
on_conflict: str,
|
|
224
224
|
merge_fns: dict[str, BytesMergeFn] | None,
|
|
225
225
|
default_merge: BytesMergeFn | None,
|
|
226
|
+
post_check: PostCheck | None = None,
|
|
226
227
|
info: dict | None,
|
|
227
228
|
saved_state: tuple | None = None,
|
|
229
|
+
cas_from: str | None = None,
|
|
230
|
+
parents: tuple[str, str] | None = None,
|
|
228
231
|
) -> MergeResult:
|
|
229
|
-
"""Perform a three-way merge between our branch and their HEAD.
|
|
232
|
+
"""Perform a three-way merge between our branch and their HEAD.
|
|
233
|
+
|
|
234
|
+
``their_head`` is any commit in the store — the moved HEAD of our
|
|
235
|
+
own branch (the concurrent-write path) or another branch's HEAD
|
|
236
|
+
(cross-branch merge). ``cas_from`` names the commit the merge
|
|
237
|
+
commits on top of (defaults to ``their_head``, the concurrent
|
|
238
|
+
case); cross-branch callers pass their own head.
|
|
239
|
+
"""
|
|
230
240
|
lca = self._find_lca(self._current_commit, their_head)
|
|
231
241
|
if lca is None:
|
|
232
242
|
if saved_state is not None:
|
|
@@ -275,6 +285,14 @@ class VersionedBase(ABC):
|
|
|
275
285
|
merge_fns=effective_fns,
|
|
276
286
|
default_merge=effective_default,
|
|
277
287
|
)
|
|
288
|
+
if post_check is not None:
|
|
289
|
+
refused = {
|
|
290
|
+
key
|
|
291
|
+
for key, value in resolution.merged_values.items()
|
|
292
|
+
if not post_check(key, value)
|
|
293
|
+
}
|
|
294
|
+
if refused:
|
|
295
|
+
raise MergeConflict(refused)
|
|
278
296
|
except MergeConflict:
|
|
279
297
|
if saved_state is not None:
|
|
280
298
|
self._restore_state(saved_state)
|
|
@@ -291,14 +309,19 @@ class VersionedBase(ABC):
|
|
|
291
309
|
raise
|
|
292
310
|
|
|
293
311
|
auto_merged = resolution.auto_merged_keys
|
|
294
|
-
parents
|
|
312
|
+
if parents is None:
|
|
313
|
+
# Concurrent-write default: keep following the moved HEAD, as
|
|
314
|
+
# before. Cross-branch callers pass (our_head, their_head) so
|
|
315
|
+
# linear history stays on the merging branch (git convention).
|
|
316
|
+
parents = (their_head, self._current_commit)
|
|
295
317
|
|
|
296
318
|
self._create_merge_commit(resolution, parents, info)
|
|
297
319
|
merge_hash = self._current_commit
|
|
298
320
|
merged_keyset = self._commit_keys
|
|
299
321
|
|
|
300
|
-
# CAS HEAD from their_head
|
|
301
|
-
|
|
322
|
+
# CAS HEAD onto the merge commit: from their_head in the
|
|
323
|
+
# concurrent-write path, from our own head cross-branch.
|
|
324
|
+
if self._cas_head(cas_from or their_head, merge_hash):
|
|
302
325
|
self._base_commit = merge_hash
|
|
303
326
|
result = MergeResult(
|
|
304
327
|
merged=True,
|
|
@@ -330,6 +353,47 @@ class VersionedBase(ABC):
|
|
|
330
353
|
"HEAD changed during three-way merge. Refresh and retry."
|
|
331
354
|
)
|
|
332
355
|
|
|
356
|
+
def merge_heads(
|
|
357
|
+
self,
|
|
358
|
+
their_head: str,
|
|
359
|
+
*,
|
|
360
|
+
on_conflict: str = "raise",
|
|
361
|
+
merge_fns: dict[str, BytesMergeFn] | None = None,
|
|
362
|
+
default_merge: BytesMergeFn | None = None,
|
|
363
|
+
post_check: PostCheck | None = None,
|
|
364
|
+
info: dict | None = None,
|
|
365
|
+
) -> MergeResult:
|
|
366
|
+
"""Merge another head into this branch.
|
|
367
|
+
|
|
368
|
+
``their_head`` is any commit in the store — usually another
|
|
369
|
+
branch's HEAD. Finds the lowest common ancestor with our head,
|
|
370
|
+
three-way resolves, and creates a two-parent merge commit,
|
|
371
|
+
CAS-guarded on our own head (a race raises ``ConcurrencyError``
|
|
372
|
+
and changes nothing). No common ancestor raises
|
|
373
|
+
``ConcurrencyError`` likewise without changing anything; retrying
|
|
374
|
+
cannot help that case, unlike a race.
|
|
375
|
+
|
|
376
|
+
``post_check`` runs over each merge-function-produced value;
|
|
377
|
+
a False files that key as conflicted (handled per
|
|
378
|
+
``on_conflict`` like any other conflict).
|
|
379
|
+
"""
|
|
380
|
+
if on_conflict not in ("raise", "abandon"):
|
|
381
|
+
raise ValueError(
|
|
382
|
+
f"on_conflict must be 'raise' or 'abandon', got {on_conflict!r}"
|
|
383
|
+
)
|
|
384
|
+
our_head = self._current_commit
|
|
385
|
+
return self._three_way_merge(
|
|
386
|
+
their_head,
|
|
387
|
+
on_conflict=on_conflict,
|
|
388
|
+
merge_fns=merge_fns,
|
|
389
|
+
default_merge=default_merge,
|
|
390
|
+
post_check=post_check,
|
|
391
|
+
info=info,
|
|
392
|
+
saved_state=self._snapshot_state(),
|
|
393
|
+
cas_from=our_head,
|
|
394
|
+
parents=(our_head, their_head),
|
|
395
|
+
)
|
|
396
|
+
|
|
333
397
|
# -- Abstract methods (implemented by subclasses) --
|
|
334
398
|
|
|
335
399
|
@property
|
|
@@ -1182,14 +1182,19 @@ class VersionedKV(VersionedBase):
|
|
|
1182
1182
|
diffs[vk] = value
|
|
1183
1183
|
|
|
1184
1184
|
# Build merged meta from the parents' meta. ``self._meta`` is
|
|
1185
|
-
# already our parent's meta (in memory).
|
|
1186
|
-
# we
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1185
|
+
# already our parent's meta (in memory). Any other parent's meta
|
|
1186
|
+
# we walk via the HAMT — position-independent, since merge
|
|
1187
|
+
# commits record ours-first: union every parent so their-side
|
|
1188
|
+
# keys resolve however the tuple is ordered. Ties keep ours
|
|
1189
|
+
# (checked first below), matching the old parents[0]-is-theirs
|
|
1190
|
+
# behavior exactly.
|
|
1191
|
+
parent_meta: dict[str, MetaEntry] = {}
|
|
1192
|
+
for parent in parents:
|
|
1193
|
+
parent_root = _load_root(self.store, parent)
|
|
1194
|
+
if parent_root is None:
|
|
1195
|
+
continue
|
|
1196
|
+
for key, entry in Keyset(self.store, root=parent_root).items():
|
|
1197
|
+
parent_meta.setdefault(key, entry.meta)
|
|
1193
1198
|
|
|
1194
1199
|
merged_meta: dict[str, MetaEntry] = {}
|
|
1195
1200
|
for key in merged_keyset:
|
|
@@ -1200,8 +1205,8 @@ class VersionedKV(VersionedBase):
|
|
|
1200
1205
|
)
|
|
1201
1206
|
elif key in self._meta:
|
|
1202
1207
|
merged_meta[key] = self._meta[key]
|
|
1203
|
-
elif key in
|
|
1204
|
-
merged_meta[key] =
|
|
1208
|
+
elif key in parent_meta:
|
|
1209
|
+
merged_meta[key] = parent_meta[key]
|
|
1205
1210
|
|
|
1206
1211
|
# Apply the merge result on top of our parent's HAMT. We compute
|
|
1207
1212
|
# the minimal updates and removals so structural sharing kicks in
|
|
@@ -10,6 +10,15 @@ BytesMergeFn = Callable[[bytes | None, bytes | None, bytes | None], bytes]
|
|
|
10
10
|
Any argument can be None (key absent or removed on that side).
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
PostCheck = Callable[[str, bytes], bool]
|
|
14
|
+
"""Post-merge predicate: (key, merged_bytes) -> accept?
|
|
15
|
+
|
|
16
|
+
Runs over values a merge function produced. Returning False files the
|
|
17
|
+
key as conflicted, as if no merge function had resolved it. kvgit never
|
|
18
|
+
inspects the bytes itself — callers that know what their values mean
|
|
19
|
+
(marker scans, schema checks) decide.
|
|
20
|
+
"""
|
|
21
|
+
|
|
13
22
|
|
|
14
23
|
@dataclass(frozen=True)
|
|
15
24
|
class DiffResult:
|
|
@@ -110,6 +119,22 @@ class Versioned(Protocol):
|
|
|
110
119
|
chunk_refs: dict[str, list[str]] | None = None,
|
|
111
120
|
) -> MergeResult: ...
|
|
112
121
|
|
|
122
|
+
def merge_heads(
|
|
123
|
+
self,
|
|
124
|
+
their_head: str,
|
|
125
|
+
*,
|
|
126
|
+
on_conflict: str = "raise",
|
|
127
|
+
merge_fns: dict[str, BytesMergeFn] | None = None,
|
|
128
|
+
default_merge: BytesMergeFn | None = None,
|
|
129
|
+
post_check: PostCheck | None = None,
|
|
130
|
+
info: dict | None = None,
|
|
131
|
+
) -> MergeResult:
|
|
132
|
+
"""Merge another head (any commit in the store, usually another
|
|
133
|
+
branch's HEAD) into this branch: LCA + three-way resolve + a
|
|
134
|
+
two-parent merge commit, CAS-guarded on our own head.
|
|
135
|
+
"""
|
|
136
|
+
...
|
|
137
|
+
|
|
113
138
|
def refresh(self) -> None: ...
|
|
114
139
|
|
|
115
140
|
def checkout(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kvgit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Versioned key-value store with git-like commit, branch, and merge semantics.
|
|
5
5
|
Author: ashenfad
|
|
6
6
|
License: MIT
|
|
@@ -94,6 +94,38 @@ main.tag("v1")
|
|
|
94
94
|
print(main.peek("score", tag="v1")) # 0
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
## Merging
|
|
98
|
+
|
|
99
|
+
`Staged.merge()` merges another head -- usually another branch's HEAD --
|
|
100
|
+
into the current branch: lowest common ancestor, three-way resolve, and
|
|
101
|
+
a two-parent merge commit guarded on your own head:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
dev["score"] = 500
|
|
105
|
+
dev.commit()
|
|
106
|
+
|
|
107
|
+
result = main.merge(dev.current_commit) # True when merged
|
|
108
|
+
print(main["score"]) # 500 (fast-forward: main hadn't diverged)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Overlapping changes need a merge function per key, or a `default_merge`
|
|
112
|
+
fallback. `kvgit.merges.text` resolves line-oriented text with git-style
|
|
113
|
+
`<<<<<<<` markers (see `make_text_merge` for custom labels); anything it
|
|
114
|
+
cannot mark -- binary, non-UTF-8, oversized -- raises `CantMark`, filed
|
|
115
|
+
as an ordinary conflict:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from kvgit.merges import text
|
|
119
|
+
|
|
120
|
+
result = main.merge(dev.current_commit, default_merge=text)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A `post_check(key, merged_bytes)` predicate runs over every
|
|
124
|
+
merge-produced value; returning `False` files that key as conflicted.
|
|
125
|
+
`on_conflict="abandon"` leaves the branch untouched instead of raising.
|
|
126
|
+
Merging refuses with `ValueError` when the staging buffer holds
|
|
127
|
+
uncommitted changes -- commit or reset first.
|
|
128
|
+
|
|
97
129
|
## Chunked codecs (numpy / pandas)
|
|
98
130
|
|
|
99
131
|
Large numpy arrays and pandas DataFrames -- and any sliced views of them -- can be stored once and shared across keys, commits, and branches:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kvgit"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.7"
|
|
8
8
|
description = "Versioned key-value store with git-like commit, branch, and merge semantics."
|
|
9
9
|
authors = [{ name = "ashenfad" }]
|
|
10
10
|
requires-python = ">=3.10"
|
|
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
|
|
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
|