pyPhotoAlbum/MERGE_FEATURE.md
Duncan Tourolle 0d698a83b4
Some checks failed
Python CI / test (push) Successful in 55s
Lint / lint (push) Successful in 1m31s
Tests / test (3.10) (push) Failing after 44s
Tests / test (3.11) (push) Failing after 42s
Tests / test (3.9) (push) Failing after 42s
large change to allow project merging
2025-11-23 00:33:42 +01:00

542 lines
14 KiB
Markdown

# Project Merge & Conflict Resolution Feature
## Overview
pyPhotoAlbum v3.0 introduces comprehensive merge conflict resolution support, enabling multiple users to edit the same album and merge their changes intelligently. The system uses UUIDs, timestamps, and a project ID to track changes and resolve conflicts.
## Table of Contents
- [Key Features](#key-features)
- [How It Works](#how-it-works)
- [File Format Changes (v3.0)](#file-format-changes-v30)
- [User Guide](#user-guide)
- [Developer Guide](#developer-guide)
- [Testing](#testing)
- [Migration from v2.0](#migration-from-v20)
---
## Key Features
### 1. **Project ID-Based Merge Detection**
- Each project has a unique `project_id` (UUID)
- **Same project_id** → Merge with conflict resolution
- **Different project_id** → Concatenate (combine all pages)
### 2. **UUID-Based Element Tracking**
- Every page and element has a stable UUID
- Elements can be tracked even when page numbers or z-order changes
- Enables reliable conflict detection across versions
### 3. **Timestamp-Based Conflict Resolution**
- All changes tracked with `created` and `last_modified` timestamps (ISO 8601 UTC)
- Automatic "Latest Wins" strategy available
- Manual conflict resolution through visual dialog
### 4. **Soft Delete Support**
- Deleted items marked with `deleted` flag and `deleted_at` timestamp
- Prevents resurrection conflicts
- Tombstone pattern ensures deleted items stay deleted
### 5. **Visual Merge Dialog**
- Side-by-side comparison of conflicting changes
- Page previews and element details
- Multiple resolution strategies:
- **Latest Wins**: Most recent change wins (automatic)
- **Always Use Yours**: Keep all local changes
- **Always Use Theirs**: Accept all remote changes
- **Manual**: Choose per-conflict
---
## How It Works
### Merge Workflow
```
1. User clicks "Merge Projects" in File ribbon tab
2. Select .ppz file to merge
3. System compares project_ids
├─→ Same ID: Detect conflicts → Show merge dialog
└─→ Different ID: Ask to concatenate
4. User resolves conflicts (if any)
5. Merged project becomes current project
6. User saves merged project
```
### Conflict Detection
The system detects three types of conflicts:
#### 1. **Page-Level Conflicts**
- Page modified in both versions
- Page deleted in one, modified in other
- Page properties changed (size, type, etc.)
#### 2. **Element-Level Conflicts**
- Element modified in both versions (position, size, rotation, content)
- Element deleted in one, modified in other
- Element properties changed differently
#### 3. **Project-Level Conflicts**
- Settings changed in both (page size, DPI, cover settings, etc.)
### Automatic Conflict Resolution
**Non-conflicting changes** are automatically merged:
- Page 1 modified in version A, Page 2 modified in version B → Keep both
- New pages added at different positions → Merge both sets
- Different elements modified → Keep all modifications
**Conflicting changes** require resolution:
- Same element modified in both versions
- Element/page deleted in one but modified in other
---
## File Format Changes (v3.0)
### What's New in v3.0
#### Project Level
```json
{
"data_version": "3.0",
"project_id": "550e8400-e29b-41d4-a716-446655440000",
"created": "2025-01-22T10:30:00.123456+00:00",
"last_modified": "2025-01-22T14:45:12.789012+00:00",
...
}
```
#### Page Level
```json
{
"page_number": 1,
"uuid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"created": "2025-01-22T10:30:00.123456+00:00",
"last_modified": "2025-01-22T11:15:30.456789+00:00",
"deleted": false,
"deleted_at": null,
...
}
```
#### Element Level
```json
{
"type": "image",
"uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"created": "2025-01-22T10:30:00.123456+00:00",
"last_modified": "2025-01-22T13:20:45.123456+00:00",
"deleted": false,
"deleted_at": null,
"position": [10, 10],
"size": [100, 100],
...
}
```
### Backwards Compatibility
- **v3.0 can read v2.0 and v1.0 files** with automatic migration
- **v2.0/v1.0 cannot read v3.0 files** (breaking change)
- Migration automatically generates UUIDs and timestamps for old files
---
## User Guide
### How to Merge Two Album Versions
1. **Open your current album** in pyPhotoAlbum
2. **Click "Merge Projects"** in the File tab of the ribbon
3. **Select the other album file** (.ppz) to merge
4. **System analyzes the projects:**
- If they're the same album (same project_id):
- Shows conflicts requiring resolution
- Auto-merges non-conflicting changes
- If they're different albums:
- Asks if you want to combine all pages
5. **Resolve conflicts** (if merging same album):
- View side-by-side comparison
- Choose "Use Your Version" or "Use Other Version" for each conflict
- Or click "Auto-Resolve All" with a strategy:
- **Latest Wins**: Keeps most recently modified version
- **Always Use Yours**: Keeps all your changes
- **Always Use Theirs**: Accepts all their changes
6. **Click "Apply Merge"** to complete the merge
7. **Save the merged album** when ready
### Best Practices
1. **Save before merging** - The system will prompt you, but it's good practice
2. **Use cloud sync carefully** - If using Dropbox/Google Drive:
- Each person should have their own working copy
- Merge explicitly rather than relying on cloud sync conflicts
3. **Communicate with collaborators** - Agree on who edits which pages to minimize conflicts
4. **Review the merge** - Check the merged result before saving
5. **Keep backups** - The autosave system creates checkpoints, but manual backups are recommended
### Common Scenarios
#### Scenario 1: You and a Friend Edit Different Pages
- **Result**: Auto-merge ✅
- No conflicts, both sets of changes preserved
#### Scenario 2: You Both Edit the Same Image Position
- **Result**: Conflict resolution needed ⚠️
- You choose which position to keep
#### Scenario 3: You Delete an Image, They Move It
- **Result**: Conflict resolution needed ⚠️
- You choose: keep it deleted or use their moved version
#### Scenario 4: Combining Two Different Albums
- **Result**: Concatenation
- All pages from both albums combined into one
---
## Developer Guide
### Architecture
```
pyPhotoAlbum/
├── models.py # BaseLayoutElement with UUID/timestamp support
├── project.py # Project and Page with UUID/timestamp support
├── version_manager.py # v3.0 migration logic
├── project_serializer.py # Save/load with v3.0 support
├── merge_manager.py # Core merge conflict detection & resolution
├── merge_dialog.py # Qt UI for visual conflict resolution
└── mixins/operations/
└── merge_ops.py # Ribbon integration & workflow
```
### Key Classes
#### MergeManager
```python
from pyPhotoAlbum.merge_manager import MergeManager, MergeStrategy
manager = MergeManager()
# Check if projects should be merged or concatenated
should_merge = manager.should_merge_projects(project_a_data, project_b_data)
# Detect conflicts
conflicts = manager.detect_conflicts(our_data, their_data)
# Auto-resolve
resolutions = manager.auto_resolve_conflicts(MergeStrategy.LATEST_WINS)
# Apply merge
merged_data = manager.apply_resolutions(our_data, their_data, resolutions)
```
#### Data Model Updates
```python
from pyPhotoAlbum.models import ImageData
from pyPhotoAlbum.project import Page, Project
# All elements now have:
element = ImageData(...)
element.uuid # Auto-generated UUID
element.created # ISO 8601 timestamp
element.last_modified # ISO 8601 timestamp
element.deleted # Boolean flag
element.deleted_at # Timestamp when deleted
# Mark as modified
element.mark_modified() # Updates last_modified
# Mark as deleted
element.mark_deleted() # Sets deleted=True, deleted_at=now
# Same for pages and projects
page.mark_modified()
project.mark_modified()
```
### Adding Merge Support to Custom Elements
If you create custom element types, ensure they:
1. **Inherit from BaseLayoutElement**
```python
class MyCustomElement(BaseLayoutElement):
def __init__(self, **kwargs):
super().__init__(**kwargs) # Initializes UUID and timestamps
# Your custom fields here
```
2. **Call `_deserialize_base_fields()` first in deserialize**
```python
def deserialize(self, data: Dict[str, Any]):
self._deserialize_base_fields(data) # Load UUID/timestamps
# Load your custom fields
```
3. **Include base fields in serialize**
```python
def serialize(self) -> Dict[str, Any]:
data = {
"type": "mycustom",
# Your custom fields
}
data.update(self._serialize_base_fields()) # Add UUID/timestamps
return data
```
4. **Call `mark_modified()` when changed**
```python
def set_my_property(self, value):
self.my_property = value
self.mark_modified() # Update timestamp
```
### Migration System
To add a new migration (e.g., v3.0 to v4.0):
```python
# In version_manager.py
@DataMigration.register_migration("3.0", "4.0")
def migrate_3_0_to_4_0(data: Dict[str, Any]) -> Dict[str, Any]:
"""
Migrate from version 3.0 to 4.0.
Main changes:
- Add new fields
- Update structures
"""
# Perform migration
data['new_field'] = default_value
# Update version
data['data_version'] = "4.0"
return data
```
### Testing
Run the provided test scripts:
```bash
# Test v2.0 → v3.0 migration
python test_migration.py
# Test merge functionality
python test_merge.py
```
Expected output: All tests should pass with ✅
---
## Testing
### Manual Testing Checklist
#### Test 1: Basic Migration
- [ ] Open a v2.0 project
- [ ] Verify it loads without errors
- [ ] Check console for "Migration 2.0 → 3.0" message
- [ ] Save the project
- [ ] Verify saved version is 3.0
#### Test 2: Same Project Merge
- [ ] Create a project, save it
- [ ] Open the file twice in different instances
- [ ] Modify same element in both
- [ ] Merge them
- [ ] Verify conflict dialog appears
- [ ] Resolve conflict
- [ ] Verify merged result
#### Test 3: Different Project Concatenation
- [ ] Create two different projects
- [ ] Try to merge them
- [ ] Verify concatenation option appears
- [ ] Verify combined project has all pages
#### Test 4: Auto-Merge Non-Conflicting
- [ ] Create project with 2 pages
- [ ] Version A: Edit page 1
- [ ] Version B: Edit page 2
- [ ] Merge
- [ ] Verify auto-merge without conflicts
- [ ] Verify both edits preserved
### Automated Testing
Run the test scripts:
```bash
cd /home/dtourolle/Development/pyPhotoAlbum
# Migration test
./test_migration.py
# Merge test
./test_merge.py
```
---
## Migration from v2.0
### Automatic Migration
When you open a v2.0 project in v3.0, it will automatically:
1. Generate a unique `project_id`
2. Generate `uuid` for all pages and elements
3. Set `created` and `last_modified` to current time
4. Add `deleted` and `deleted_at` fields (all set to False/None)
5. Update `data_version` to "3.0"
### Migration Output Example
```
Migration 2.0 → 3.0: Adding UUIDs, timestamps, and project_id
Generated project_id: 550e8400-e29b-41d4-a716-446655440000
Migrated 5 pages to v3.0
Migration completed successfully
```
### After Migration
- **Save the project** to persist the migration
- The migrated file can **only be opened in v3.0+**
- Keep a backup of v2.0 file if you need v2.0 compatibility
### Rollback
If you need to rollback to v2.0:
1. Don't save after opening in v3.0
2. Close without saving
3. Open original v2.0 file in v2.0
---
## Troubleshooting
### Merge Dialog Won't Appear
**Problem**: Clicking "Merge Projects" does nothing
**Solutions**:
- Check both projects are v3.0 (or were migrated)
- Verify projects have the same `project_id`
- Check console for error messages
### Can't Resolve Conflicts
**Problem**: "Apply Merge" button is grayed out
**Solutions**:
- Make a resolution choice for each conflict
- Or click "Auto-Resolve All" first
### Changes Not Preserved
**Problem**: After merge, some changes are missing
**Solutions**:
- Check which resolution strategy you used
- "Latest Wins" prefers most recent modifications
- Review each conflict manually if needed
### Project Won't Load
**Problem**: "Incompatible file version" error
**Solutions**:
- This is a v2.0 or v1.0 file
- Migration should happen automatically
- If not, check version_manager.py for errors
---
## FAQ
### Q: Can I merge more than two projects at once?
**A:** Not directly. Merge two at a time, then merge the result with a third.
### Q: What happens to undo history after merge?
**A:** Undo history is session-specific and not preserved during merge. Save before merging.
### Q: Can I see what changed before merging?
**A:** The merge dialog shows changed elements with timestamps. Future versions may add detailed diff view.
### Q: Is merge atomic?
**A:** No. If you cancel during conflict resolution, no changes are made. Once you click "Apply Merge", the changes are applied to the current project.
### Q: Can I merge projects from different versions?
**A:** Yes! v2.0 and v1.0 projects are automatically migrated to v3.0 before merging.
### Q: What if two people add the same image?
**A:** If the image has the same filename and is added to different pages, both instances are kept. If added to the same location on the same page, it becomes a conflict.
### Q: Can I programmatically merge projects?
**A:** Yes! See the Developer Guide section for `MergeManager` API usage.
---
## Future Enhancements
Potential improvements for future versions:
1. **Three-way merge** - Use base version for better conflict resolution
2. **Merge history tracking** - Log all merges performed
3. **Partial merge** - Merge only specific pages
4. **Cloud collaboration** - Real-time collaborative editing
5. **Merge preview** - Show full diff before applying
6. **Asset conflict handling** - Better handling of duplicate assets
7. **Conflict visualization** - Visual overlay showing changes
---
## Version History
### v3.0 (2025-01-22)
- ✨ Initial merge conflict resolution feature
- ✨ UUID and timestamp tracking
- ✨ Project ID-based merge detection
- ✨ Visual merge dialog
- ✨ Automatic migration from v2.0
- ✨ Soft delete support
---
## Credits
Merge system designed and implemented with the following principles:
- **UUID stability** - Elements tracked across versions
- **Timestamp precision** - ISO 8601 UTC for reliable ordering
- **Backwards compatibility** - Seamless migration from v2.0
- **User-friendly** - Visual conflict resolution
- **Developer-friendly** - Clean API, well-documented
For questions or issues, please file a bug report in the project repository.