Skip to content

Commit 5200a2d

Browse files
committed
expose python api
1 parent 6dadfb0 commit 5200a2d

5 files changed

Lines changed: 655 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ cargo test --lib tree::tests
6565

6666
# Run with specific features
6767
cargo test --features "git sql"
68+
69+
# Test merge functionality specifically
70+
cargo test test_versioned_kv_store_merge --lib -- --nocapture
6871
```
6972

7073
### Code Quality
@@ -106,6 +109,7 @@ python -m pytest python/tests/
106109
python python/tests/test_prollytree.py
107110
python python/tests/test_sql.py
108111
python python/tests/test_agent.py
112+
python python/tests/test_merge.py # Merge functionality tests
109113

110114
# Run Python examples
111115
cd python/examples && ./run_examples.sh
@@ -126,6 +130,10 @@ cd python/examples && ./run_examples.sh langgraph_chronological.py
126130
# Commit changes
127131
./target/debug/git-prolly commit -m "Initial data"
128132

133+
# Branch operations
134+
./target/debug/git-prolly checkout -b feature-branch
135+
./target/debug/git-prolly checkout main
136+
129137
# List all keys
130138
./target/debug/git-prolly list
131139
./target/debug/git-prolly list --values # Include values
@@ -216,6 +224,16 @@ The codebase implements a layered architecture where each layer builds on the pr
216224
- LRU cache available for frequently accessed nodes
217225
- Python bindings handle memory safely through PyO3
218226

227+
### Merge Operations & Conflict Resolution
228+
- **Three-way merge**: Uses common base commit to intelligently merge branches
229+
- **Key-value level merging**: Operates on actual data rather than tree structure for reliability
230+
- **Conflict resolution strategies**:
231+
- `IgnoreConflictsResolver`: Keeps destination branch values (default for `merge_ignore_conflicts`)
232+
- `TakeSourceResolver`: Always prefers source branch values
233+
- `TakeDestinationResolver`: Always keeps current branch values
234+
- **Python API**: Full merge support with `store.merge(branch, ConflictResolution.TakeSource)` and `store.try_merge(branch)` for conflict detection
235+
- **Implementation**: `src/git/versioned_store.rs` contains merge logic, `src/diff.rs` defines conflict resolvers
236+
219237
### Concurrency
220238
- Thread-safe variants available for multi-threaded access
221239
- Agent memory system uses Tokio for async operations
@@ -247,6 +265,9 @@ The codebase implements a layered architecture where each layer builds on the pr
247265
- **Documentation**: Auto-generated Sphinx docs at https://prollytree.readthedocs.io/
248266

249267
### Recent Additions
268+
- **Branch Merging**: Three-way merge functionality with configurable conflict resolution strategies
269+
- **Conflict Resolution**: Support for IgnoreAll, TakeSource, and TakeDestination merge strategies
270+
- **Python Merge API**: Complete Python bindings for merge operations with MergeConflict detection
250271
- **LangGraph Integration**: Examples showing AI agent workflows with ProllyTree memory
251272
- **SQL API**: Complete SQL interface exposed to Python via GlueSQL
252273
- **Historical Commit Access**: Track and retrieve commit history for specific keys
@@ -270,5 +291,19 @@ NEVER create files unless they're absolutely necessary for achieving your goal.
270291
ALWAYS prefer editing an existing file to creating a new one.
271292
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
272293
NEVER perform `git push` or `git commit` operations without explicit instructions from the User.
294+
ALWAYS add Apache 2.0 license headers to new files (Rust, Python, etc.). Format:
295+
```
296+
# Licensed under the Apache License, Version 2.0 (the "License");
297+
# you may not use this file except in compliance with the License.
298+
# You may obtain a copy of the License at
299+
#
300+
# http://www.apache.org/licenses/LICENSE-2.0
301+
#
302+
# Unless required by applicable law or agreed to in writing, software
303+
# distributed under the License is distributed on an "AS IS" BASIS,
304+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
305+
# See the License for the specific language governing permissions and
306+
# limitations under the License.
307+
```
273308

274309
Note: This project has comprehensive documentation auto-generation via Sphinx at https://prollytree.readthedocs.io/ - prefer directing users there rather than creating new documentation files.

python/examples/merge_example.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
#!/usr/bin/env python3
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Example: Branch merging with conflict resolution in VersionedKvStore
17+
18+
This example demonstrates how to use the merge functionality in ProllyTree's
19+
VersionedKvStore with different conflict resolution strategies.
20+
"""
21+
22+
import tempfile
23+
import subprocess
24+
import os
25+
import sys
26+
import shutil
27+
28+
# Add the parent directory to the path so we can import prollytree
29+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
30+
31+
from prollytree import VersionedKvStore, ConflictResolution, MergeConflict
32+
33+
34+
def setup_example_repo():
35+
"""Set up a temporary git repository for the example"""
36+
tmpdir = tempfile.mkdtemp(prefix="prollytree_merge_example_")
37+
print(f"📁 Created temporary directory: {tmpdir}")
38+
39+
# Initialize git repository
40+
subprocess.run(['git', 'init'], cwd=tmpdir, check=True, capture_output=True)
41+
subprocess.run(['git', 'config', 'user.name', 'Example User'], cwd=tmpdir, check=True, capture_output=True)
42+
subprocess.run(['git', 'config', 'user.email', 'user@example.com'], cwd=tmpdir, check=True, capture_output=True)
43+
44+
# Create data subdirectory
45+
data_dir = os.path.join(tmpdir, 'data')
46+
os.makedirs(data_dir, exist_ok=True)
47+
48+
return tmpdir, data_dir
49+
50+
51+
def demo_basic_merge():
52+
"""Demonstrate basic merge without conflicts"""
53+
print("\n🔀 Demo: Basic merge without conflicts")
54+
print("=" * 50)
55+
56+
tmpdir, data_dir = setup_example_repo()
57+
58+
try:
59+
# Initialize the store
60+
store = VersionedKvStore(data_dir)
61+
62+
# Create initial data on main branch
63+
print("📝 Setting up initial data on main branch...")
64+
store.insert(b"users:alice", b"Alice Smith")
65+
store.insert(b"users:bob", b"Bob Jones")
66+
store.insert(b"config:theme", b"light")
67+
store.commit("Initial user data")
68+
69+
# Create and switch to feature branch
70+
print("🌿 Creating feature branch...")
71+
store.create_branch("add-user-charlie")
72+
73+
# Add new user and update config on feature branch
74+
print("✍️ Making changes on feature branch...")
75+
store.insert(b"users:charlie", b"Charlie Brown")
76+
store.update(b"config:theme", b"dark") # This will create a conflict later
77+
store.commit("Add Charlie and switch to dark theme")
78+
79+
# Switch back to main and make different changes
80+
print("🔄 Switching back to main branch...")
81+
store.checkout("main")
82+
83+
print("✍️ Making changes on main branch...")
84+
store.insert(b"users:diana", b"Diana Prince")
85+
store.commit("Add Diana")
86+
87+
# Show status before merge
88+
print("\n📊 Status before merge:")
89+
print(f"Current branch: {store.current_branch()}")
90+
print("Users on main:", {k.decode(): v.decode() for k, v in
91+
[(k, store.get(k)) for k in [b"users:alice", b"users:bob", b"users:diana"]]
92+
if v})
93+
print(f"Theme: {store.get(b'config:theme').decode()}")
94+
95+
# Perform merge
96+
print("\n🔀 Merging feature branch into main...")
97+
merge_commit = store.merge("add-user-charlie", ConflictResolution.TakeSource)
98+
print(f"✅ Merge successful! Commit: {merge_commit[:8]}")
99+
100+
# Show final state
101+
print("\n📊 Final state after merge:")
102+
all_keys = store.list_keys()
103+
for key in sorted(all_keys):
104+
value = store.get(key)
105+
print(f" {key.decode()}: {value.decode()}")
106+
107+
finally:
108+
shutil.rmtree(tmpdir)
109+
print(f"🧹 Cleaned up {tmpdir}")
110+
111+
112+
def demo_conflict_resolution():
113+
"""Demonstrate different conflict resolution strategies"""
114+
print("\n⚔️ Demo: Conflict resolution strategies")
115+
print("=" * 50)
116+
117+
for strategy_name, strategy in [
118+
("IgnoreAll", ConflictResolution.IgnoreAll),
119+
("TakeSource", ConflictResolution.TakeSource),
120+
("TakeDestination", ConflictResolution.TakeDestination)
121+
]:
122+
print(f"\n🛡️ Testing {strategy_name} strategy...")
123+
124+
tmpdir, data_dir = setup_example_repo()
125+
126+
try:
127+
store = VersionedKvStore(data_dir)
128+
129+
# Set up conflict scenario
130+
store.insert(b"shared_key", b"initial_value")
131+
store.commit("Initial commit")
132+
133+
# Feature branch changes
134+
store.create_branch("feature")
135+
store.update(b"shared_key", b"feature_value")
136+
store.commit("Feature change")
137+
138+
# Main branch changes
139+
store.checkout("main")
140+
store.update(b"shared_key", b"main_value")
141+
store.commit("Main change")
142+
143+
# Apply merge with strategy
144+
merge_commit = store.merge("feature", strategy)
145+
final_value = store.get(b"shared_key").decode()
146+
147+
print(f" Result with {strategy_name}: '{final_value}'")
148+
149+
finally:
150+
shutil.rmtree(tmpdir)
151+
152+
153+
def demo_conflict_detection():
154+
"""Demonstrate conflict detection with try_merge"""
155+
print("\n🔍 Demo: Conflict detection with try_merge")
156+
print("=" * 50)
157+
158+
tmpdir, data_dir = setup_example_repo()
159+
160+
try:
161+
store = VersionedKvStore(data_dir)
162+
163+
# Create conflict scenario
164+
store.insert(b"config:database_url", b"sqlite:///prod.db")
165+
store.insert(b"config:debug", b"false")
166+
store.commit("Production config")
167+
168+
# Feature branch: development config
169+
store.create_branch("dev-config")
170+
store.update(b"config:database_url", b"sqlite:///dev.db")
171+
store.update(b"config:debug", b"true")
172+
store.insert(b"config:dev_tools", b"enabled")
173+
store.commit("Development configuration")
174+
175+
# Main branch: staging config
176+
store.checkout("main")
177+
store.update(b"config:database_url", b"postgresql://staging-db")
178+
store.insert(b"config:cache", b"redis://cache-server")
179+
store.commit("Staging configuration")
180+
181+
# Try merge to detect conflicts
182+
print("🔍 Checking for merge conflicts...")
183+
success, conflicts = store.try_merge("dev-config")
184+
185+
if success:
186+
print("✅ No conflicts detected - merge would succeed")
187+
else:
188+
print(f"⚠️ Conflicts detected! Found {len(conflicts)} conflict(s):")
189+
for i, conflict in enumerate(conflicts, 1):
190+
print(f"\n Conflict {i}: {conflict.key.decode()}")
191+
if conflict.base_value:
192+
print(f" Base: '{conflict.base_value.decode()}'")
193+
if conflict.source_value:
194+
print(f" Source: '{conflict.source_value.decode()}'")
195+
if conflict.destination_value:
196+
print(f" Destination: '{conflict.destination_value.decode()}'")
197+
198+
print("\n💡 State remains unchanged after try_merge:")
199+
print(f" database_url: {store.get(b'config:database_url').decode()}")
200+
print(f" debug: {store.get(b'config:debug').decode()}")
201+
202+
finally:
203+
shutil.rmtree(tmpdir)
204+
print(f"🧹 Cleaned up {tmpdir}")
205+
206+
207+
def main():
208+
"""Run all merge examples"""
209+
print("🌳 ProllyTree Merge Examples")
210+
print("=" * 50)
211+
212+
print("This example demonstrates branch merging with conflict resolution")
213+
print("in ProllyTree's VersionedKvStore.")
214+
215+
try:
216+
demo_basic_merge()
217+
demo_conflict_resolution()
218+
demo_conflict_detection()
219+
220+
print("\n🎉 All examples completed successfully!")
221+
print("\nKey takeaways:")
222+
print("• Use store.merge(branch, strategy) to merge branches")
223+
print("• ConflictResolution.IgnoreAll keeps destination values")
224+
print("• ConflictResolution.TakeSource prefers source branch values")
225+
print("• ConflictResolution.TakeDestination keeps current branch values")
226+
print("• Use store.try_merge(branch) to detect conflicts without applying changes")
227+
228+
except KeyboardInterrupt:
229+
print("\n⏸️ Example interrupted by user")
230+
sys.exit(1)
231+
except Exception as e:
232+
print(f"\n❌ Example failed: {e}")
233+
import traceback
234+
traceback.print_exc()
235+
sys.exit(1)
236+
237+
238+
if __name__ == "__main__":
239+
main()

python/prollytree/__init__.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,42 @@
1717
to provide efficient data access with verifiable integrity.
1818
"""
1919

20-
from .prollytree import ProllyTree, TreeConfig, AgentMemorySystem, MemoryType, VersionedKvStore, StorageBackend
20+
from .prollytree import (
21+
ProllyTree,
22+
TreeConfig,
23+
AgentMemorySystem,
24+
MemoryType,
25+
VersionedKvStore,
26+
StorageBackend,
27+
MergeConflict,
28+
ConflictResolution
29+
)
2130

2231
# Try to import SQL functionality if available
2332
try:
2433
from .prollytree import ProllySQLStore
25-
__all__ = ["ProllyTree", "TreeConfig", "AgentMemorySystem", "MemoryType", "VersionedKvStore", "StorageBackend", "ProllySQLStore"]
34+
__all__ = [
35+
"ProllyTree",
36+
"TreeConfig",
37+
"AgentMemorySystem",
38+
"MemoryType",
39+
"VersionedKvStore",
40+
"StorageBackend",
41+
"MergeConflict",
42+
"ConflictResolution",
43+
"ProllySQLStore"
44+
]
2645
except ImportError:
2746
# SQL feature not available
28-
__all__ = ["ProllyTree", "TreeConfig", "AgentMemorySystem", "MemoryType", "VersionedKvStore", "StorageBackend"]
47+
__all__ = [
48+
"ProllyTree",
49+
"TreeConfig",
50+
"AgentMemorySystem",
51+
"MemoryType",
52+
"VersionedKvStore",
53+
"StorageBackend",
54+
"MergeConflict",
55+
"ConflictResolution"
56+
]
2957

3058
__version__ = "0.2.1"

0 commit comments

Comments
 (0)