Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ cargo test --lib tree::tests

# Run with specific features
cargo test --features "git sql"

# Test merge functionality specifically
cargo test test_versioned_kv_store_merge --lib -- --nocapture
```

### Code Quality
Expand Down Expand Up @@ -106,6 +109,7 @@ python -m pytest python/tests/
python python/tests/test_prollytree.py
python python/tests/test_sql.py
python python/tests/test_agent.py
python python/tests/test_merge.py # Merge functionality tests

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

# Branch operations
./target/debug/git-prolly checkout -b feature-branch
./target/debug/git-prolly checkout main

# List all keys
./target/debug/git-prolly list
./target/debug/git-prolly list --values # Include values
Expand Down Expand Up @@ -216,6 +224,16 @@ The codebase implements a layered architecture where each layer builds on the pr
- LRU cache available for frequently accessed nodes
- Python bindings handle memory safely through PyO3

### Merge Operations & Conflict Resolution
- **Three-way merge**: Uses common base commit to intelligently merge branches
- **Key-value level merging**: Operates on actual data rather than tree structure for reliability
- **Conflict resolution strategies**:
- `IgnoreConflictsResolver`: Keeps destination branch values (default for `merge_ignore_conflicts`)
- `TakeSourceResolver`: Always prefers source branch values
- `TakeDestinationResolver`: Always keeps current branch values
- **Python API**: Full merge support with `store.merge(branch, ConflictResolution.TakeSource)` and `store.try_merge(branch)` for conflict detection
- **Implementation**: `src/git/versioned_store.rs` contains merge logic, `src/diff.rs` defines conflict resolvers

### Concurrency
- Thread-safe variants available for multi-threaded access
- Agent memory system uses Tokio for async operations
Expand Down Expand Up @@ -247,6 +265,9 @@ The codebase implements a layered architecture where each layer builds on the pr
- **Documentation**: Auto-generated Sphinx docs at https://prollytree.readthedocs.io/

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

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.
19 changes: 19 additions & 0 deletions python/docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ StorageBackend
:undoc-members:
:show-inheritance:

Merge Operations
----------------

MergeConflict
~~~~~~~~~~~~~

.. autoclass:: prollytree.MergeConflict
:members:
:undoc-members:
:show-inheritance:

ConflictResolution
~~~~~~~~~~~~~~~~~~

.. autoclass:: prollytree.ConflictResolution
:members:
:undoc-members:
:show-inheritance:

SQL Support
-----------

Expand Down
111 changes: 111 additions & 0 deletions python/docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,114 @@ Performance Examples

return tree

Branch Merging and Conflict Resolution
---------------------------------------

.. code-block:: python

from prollytree import VersionedKvStore, ConflictResolution, MergeConflict
import tempfile
import os
import subprocess

def example_merge_operations():
"""Comprehensive example of branch merging with conflict resolution"""

# Create temporary directory for the example
with tempfile.TemporaryDirectory() as tmpdir:
# Initialize git repository
subprocess.run(['git', 'init'], cwd=tmpdir, check=True, capture_output=True)
subprocess.run(['git', 'config', 'user.name', 'Example'], cwd=tmpdir, check=True, capture_output=True)
subprocess.run(['git', 'config', 'user.email', 'example@test.com'], cwd=tmpdir, check=True, capture_output=True)

# Create data subdirectory
data_dir = os.path.join(tmpdir, 'data')
os.makedirs(data_dir)

store = VersionedKvStore(data_dir)

print("=== Basic Merge Without Conflicts ===")

# Initial data
store.insert(b"users:alice", b"Alice Smith")
store.insert(b"users:bob", b"Bob Jones")
store.insert(b"config:theme", b"light")
store.commit("Initial data")

# Create feature branch
store.create_branch("add-user-feature")

# Changes on feature branch
store.insert(b"users:charlie", b"Charlie Brown")
store.update(b"config:theme", b"dark")
store.commit("Add Charlie and dark theme")

# Switch back to main and make different changes
store.checkout("main")
store.insert(b"users:diana", b"Diana Prince")
store.commit("Add Diana")

# Merge feature branch
merge_commit = store.merge("add-user-feature", ConflictResolution.TakeSource)
print(f"Merge successful: {merge_commit[:8]}")

# Show final state
print("Final users:")
for key in [b"users:alice", b"users:bob", b"users:charlie", b"users:diana"]:
value = store.get(key)
if value:
print(f" {key.decode()}: {value.decode()}")

print(f"Theme: {store.get(b'config:theme').decode()}")

print("\\n=== Conflict Detection ===")

# Create another scenario with conflicts
store.create_branch("conflicting-feature")
store.update(b"config:theme", b"blue")
store.commit("Change theme to blue")

store.checkout("main")
store.update(b"config:theme", b"red")
store.commit("Change theme to red")

# Check for conflicts without applying
success, conflicts = store.try_merge("conflicting-feature")
if not success:
print(f"Detected {len(conflicts)} conflicts:")
for conflict in conflicts:
print(f" Key: {conflict.key.decode()}")
print(f" Source: {conflict.source_value.decode()}")
print(f" Destination: {conflict.destination_value.decode()}")

print("\\n=== Conflict Resolution Strategies ===")

# Demonstrate different resolution strategies
strategies = [
("IgnoreAll", ConflictResolution.IgnoreAll),
("TakeSource", ConflictResolution.TakeSource),
("TakeDestination", ConflictResolution.TakeDestination)
]

for name, strategy in strategies:
# Create test branch for each strategy
branch_name = f"test-{name.lower()}"
store.checkout("main")
store.create_branch(branch_name)

store.update(b"config:theme", b"feature-theme")
store.commit(f"Feature theme on {branch_name}")

store.checkout("main")

# Apply merge with strategy
merge_commit = store.merge(branch_name, strategy)
final_theme = store.get(b"config:theme").decode()
print(f"{name:15} -> Theme: {final_theme}")

# Reset main for next test
store.checkout("main")

Running Examples
----------------

Expand All @@ -327,5 +435,8 @@ Running Examples
print("\\n=== AI Agent Memory ===")
example_ai_agent_memory()

print("\\n=== Branch Merging ===")
example_merge_operations()

print("\\n=== Performance ===")
example_batch_operations()
48 changes: 48 additions & 0 deletions python/docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,54 @@ ProllyTree provides Git-like versioned storage:
for commit in commits:
print(f"{commit['id'][:8]} - {commit['message']}")

Branch Operations & Merging
----------------------------

ProllyTree supports Git-like branching and merging with conflict resolution:

.. code-block:: python

from prollytree import VersionedKvStore, ConflictResolution

store = VersionedKvStore("/path/to/store")

# Set up initial data
store.insert(b"config:theme", b"light")
store.insert(b"config:lang", b"en")
store.commit("Initial configuration")

# Create and switch to feature branch
store.create_branch("feature-dark-mode")

# Make changes on feature branch
store.update(b"config:theme", b"dark")
store.insert(b"config:animations", b"enabled")
store.commit("Add dark mode and animations")

# Switch back to main branch
store.checkout("main")

# Make different changes on main
store.update(b"config:lang", b"fr")
store.commit("Change language to French")

# Merge feature branch with conflict resolution
try:
# Attempt merge, taking source values on conflicts
merge_commit = store.merge("feature-dark-mode", ConflictResolution.TakeSource)
print(f"Merge successful: {merge_commit[:8]}")
except Exception as e:
print(f"Merge failed: {e}")

# Check for conflicts without applying merge
success, conflicts = store.try_merge("feature-dark-mode")
if not success:
print(f"Found {len(conflicts)} conflicts:")
for conflict in conflicts:
print(f" Key: {conflict.key}")
print(f" Source: {conflict.source_value}")
print(f" Destination: {conflict.destination_value}")

SQL Queries
-----------

Expand Down
Loading