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

14 KiB

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

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

{
  "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

{
  "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

{
  "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

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

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
class MyCustomElement(BaseLayoutElement):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)  # Initializes UUID and timestamps
        # Your custom fields here
  1. Call _deserialize_base_fields() first in deserialize
def deserialize(self, data: Dict[str, Any]):
    self._deserialize_base_fields(data)  # Load UUID/timestamps
    # Load your custom fields
  1. Include base fields in serialize
def serialize(self) -> Dict[str, Any]:
    data = {
        "type": "mycustom",
        # Your custom fields
    }
    data.update(self._serialize_base_fields())  # Add UUID/timestamps
    return data
  1. Call mark_modified() when changed
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):

# 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:

# 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:

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.