mirror of
https://github.com/johndoe6345789/WizardMerge.git
synced 2026-04-24 21:54:57 +00:00
Compare commits
42 Commits
copilot/im
...
release-2
| Author | SHA1 | Date | |
|---|---|---|---|
| 78505fed80 | |||
|
|
25e53410ac | ||
|
|
c377c5f4aa | ||
|
|
0e2a19c89f | ||
|
|
c5a7f89b3f | ||
|
|
f4848268bd | ||
|
|
c2a5f5dd23 | ||
|
|
8fef2c0e56 | ||
| 663b91bb29 | |||
|
|
0d6d29eef4 | ||
|
|
51373a4576 | ||
|
|
d9324c6c9c | ||
|
|
ef2f5896b7 | ||
| 19bed6e65d | |||
| bf1b8606e5 | |||
|
|
f4e90fe3ae | ||
| ad0847fd3b | |||
| 1acbc7b5f2 | |||
|
|
77e5262b3c | ||
| 2d56d1c609 | |||
|
|
5c544fd9e6 | ||
|
|
e88e32b1e8 | ||
|
|
acf04ce8d5 | ||
|
|
d4aac99a21 | ||
|
|
152f1e6a21 | ||
|
|
5ea670db23 | ||
|
|
528118bc30 | ||
|
|
e2b8ca342b | ||
| f508771654 | |||
|
|
9c251974f0 | ||
|
|
8d83555466 | ||
|
|
e687410883 | ||
|
|
57d51c32e5 | ||
|
|
15e40ffd4c | ||
|
|
155a8b896c | ||
| a965ba94c0 | |||
| 6d5650c4b2 | |||
| 06835b9712 | |||
|
|
764ab5e548 | ||
|
|
89103f40fe | ||
| 329713a032 | |||
|
|
5702a79547 |
4
.github/workflows/tlc.yml
vendored
4
.github/workflows/tlc.yml
vendored
@@ -14,12 +14,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # we need history to switch/create branches
|
||||
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
35
.gitignore
vendored
35
.gitignore
vendored
@@ -210,3 +210,38 @@ __marimo__/
|
||||
extracted_graphics/*.png
|
||||
extracted_graphics/*.jpg
|
||||
extracted_graphics/*.jpeg
|
||||
|
||||
# C++ build artifacts
|
||||
backend/build/
|
||||
backend/_codeql_build_dir/
|
||||
backend/CMakeCache.txt
|
||||
backend/CMakeFiles/
|
||||
backend/cmake_install.cmake
|
||||
backend/Makefile
|
||||
backend/*.a
|
||||
backend/*.so
|
||||
backend/*.dylib
|
||||
|
||||
# CodeQL
|
||||
_codeql_detected_source_root
|
||||
|
||||
# Conan
|
||||
backend/.conan/
|
||||
backend/conan.lock
|
||||
|
||||
# Node.js / TypeScript
|
||||
frontend/node_modules/
|
||||
frontend/.next/
|
||||
frontend/out/
|
||||
frontend/.turbo/
|
||||
frontend/.vercel/
|
||||
frontend/bun.lockb
|
||||
|
||||
# Frontends
|
||||
frontends/*/build/
|
||||
frontends/nextjs/node_modules/
|
||||
frontends/nextjs/.next/
|
||||
frontends/nextjs/out/
|
||||
frontends/nextjs/.turbo/
|
||||
frontends/nextjs/.vercel/
|
||||
frontends/nextjs/bun.lockb
|
||||
|
||||
225
BUILD.md
Normal file
225
BUILD.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# WizardMerge Build and Development Guide
|
||||
|
||||
This guide provides instructions for building and developing WizardMerge across all components.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
WizardMerge uses a multi-component architecture:
|
||||
|
||||
```
|
||||
WizardMerge/
|
||||
├── backend/ # C++ core merge engine (Conan + Ninja)
|
||||
├── frontends/ # Multiple frontend options
|
||||
│ ├── qt6/ # Qt6 native desktop (C++)
|
||||
│ ├── nextjs/ # Next.js web UI (TypeScript/bun)
|
||||
│ └── cli/ # Command-line interface (C++)
|
||||
├── spec/ # TLA+ formal specification
|
||||
├── docs/ # Research paper and documentation
|
||||
└── ROADMAP.md # Development roadmap
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### C++ Backend
|
||||
|
||||
The backend implements the core three-way merge algorithm with an HTTP API server using Drogon.
|
||||
|
||||
**Prerequisites:**
|
||||
- C++17 compiler (GCC 7+, Clang 6+, MSVC 2017+)
|
||||
- CMake 3.15+
|
||||
- Ninja build tool
|
||||
- Conan package manager
|
||||
|
||||
**Build:**
|
||||
```bash
|
||||
cd backend
|
||||
./build.sh
|
||||
```
|
||||
|
||||
**Run the server:**
|
||||
```bash
|
||||
./build/wizardmerge-cli
|
||||
```
|
||||
|
||||
The HTTP server will start on port 8080. Use the POST /api/merge endpoint to perform merges.
|
||||
|
||||
See [backend/README.md](backend/README.md) for details.
|
||||
|
||||
### TypeScript Frontend
|
||||
|
||||
The frontend provides a web-based UI for conflict resolution.
|
||||
|
||||
**Prerequisites:**
|
||||
- bun (JavaScript runtime and package manager)
|
||||
|
||||
**Setup:**
|
||||
```bash
|
||||
cd frontends/nextjs
|
||||
bun install
|
||||
```
|
||||
|
||||
**Development:**
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Visit http://localhost:3000
|
||||
|
||||
See [frontends/nextjs/README.md](frontends/nextjs/README.md) for details.
|
||||
|
||||
### Qt6 Desktop Frontend
|
||||
|
||||
The Qt6 frontend provides a native desktop application.
|
||||
|
||||
**Prerequisites:**
|
||||
- Qt6 (6.2+)
|
||||
- CMake 3.16+
|
||||
- C++17 compiler
|
||||
|
||||
**Setup:**
|
||||
```bash
|
||||
cd frontends/qt6
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja
|
||||
ninja
|
||||
```
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
./wizardmerge-qt6
|
||||
```
|
||||
|
||||
See [frontends/qt6/README.md](frontends/qt6/README.md) for details.
|
||||
|
||||
### CLI Frontend
|
||||
|
||||
The CLI frontend provides a command-line interface.
|
||||
|
||||
**Prerequisites:**
|
||||
- C++17 compiler
|
||||
- CMake 3.15+
|
||||
- libcurl
|
||||
|
||||
**Setup:**
|
||||
```bash
|
||||
cd frontends/cli
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja
|
||||
ninja
|
||||
```
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
./wizardmerge-cli-frontend --help
|
||||
```
|
||||
|
||||
See [frontends/cli/README.md](frontends/cli/README.md) for details.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making Changes
|
||||
|
||||
1. **Backend (C++)**: Edit files in `backend/src/` and `backend/include/`
|
||||
2. **Qt6 Frontend (C++)**: Edit files in `frontends/qt6/src/` and `frontends/qt6/qml/`
|
||||
3. **Next.js Frontend (TypeScript)**: Edit files in `frontends/nextjs/app/`
|
||||
4. **CLI Frontend (C++)**: Edit files in `frontends/cli/src/`
|
||||
5. **Tests**: Add tests in `backend/tests/` for C++ changes
|
||||
6. **Documentation**: Update relevant README files
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# C++ backend
|
||||
cd backend && ./build.sh
|
||||
|
||||
# Qt6 desktop frontend
|
||||
cd frontends/qt6 && mkdir build && cd build && cmake .. -G Ninja && ninja
|
||||
|
||||
# Next.js web frontend
|
||||
cd frontends/nextjs && bun run build
|
||||
|
||||
# CLI frontend
|
||||
cd frontends/cli && mkdir build && cd build && cmake .. -G Ninja && ninja
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# C++ backend tests (requires GTest)
|
||||
cd backend/build && ninja test
|
||||
|
||||
# Next.js frontend tests
|
||||
cd frontends/nextjs && bun test
|
||||
```
|
||||
|
||||
## Project Standards
|
||||
|
||||
### C++ (Backend)
|
||||
- **Standard**: C++17
|
||||
- **Build System**: CMake + Ninja
|
||||
- **Package Manager**: Conan
|
||||
- **Coding Style**: Follow backend code conventions
|
||||
- **Documentation**: Doxygen-style comments
|
||||
|
||||
### TypeScript (Frontend)
|
||||
- **Runtime**: bun
|
||||
- **Framework**: Next.js 14
|
||||
- **Language**: TypeScript with strict mode
|
||||
- **Coding Style**: Follow frontend code conventions
|
||||
- **Package Manager**: bun
|
||||
|
||||
## Roadmap Progress
|
||||
|
||||
Track development progress in [ROADMAP.md](ROADMAP.md).
|
||||
|
||||
**Phase 1 (Foundation):** ✓ In Progress
|
||||
- [x] Three-way merge algorithm
|
||||
- [x] Conflict detection
|
||||
- [x] Auto-resolution patterns
|
||||
- [ ] Git integration
|
||||
- [ ] File I/O module
|
||||
|
||||
**Phase 2 (Intelligence):** Planned
|
||||
- Semantic merge
|
||||
- Enhanced visualization
|
||||
- Code intelligence
|
||||
|
||||
**Phase 3 (Advanced):** Future
|
||||
- AI-assisted merging
|
||||
- Plugin ecosystem
|
||||
- Performance optimizations
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Check [ROADMAP.md](ROADMAP.md) for planned features
|
||||
2. Create a branch for your changes
|
||||
3. Write tests for new functionality
|
||||
4. Update documentation
|
||||
5. Submit a pull request
|
||||
|
||||
## Research Foundation
|
||||
|
||||
WizardMerge is based on research from The University of Hong Kong:
|
||||
- 28.85% reduction in conflict resolution time
|
||||
- Merge suggestions for 70%+ of conflict blocks
|
||||
- Dependency analysis at text and LLVM-IR levels
|
||||
|
||||
See [docs/PAPER.md](docs/PAPER.md) for the complete research paper.
|
||||
|
||||
## Formal Specification
|
||||
|
||||
The merge algorithm is formally specified in TLA+:
|
||||
- [spec/WizardMergeSpec.tla](spec/WizardMergeSpec.tla)
|
||||
|
||||
The specification defines the dependency-aware merge logic including:
|
||||
- Vertex and edge classification
|
||||
- Safe vs. violated edges
|
||||
- Conflict detection rules
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE) for details.
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [mergebot](https://github.com/JohnDoe6345789/mergebot) - Companion automation tool
|
||||
214
DROGON_CONVERSION.md
Normal file
214
DROGON_CONVERSION.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Backend Conversion to Drogon - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
The WizardMerge C++ backend has been successfully converted from a command-line interface (CLI) application to a modern HTTP API server using the Drogon web framework.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Core Architecture Changes
|
||||
|
||||
#### HTTP Server Implementation
|
||||
- **New**: `src/main.cpp` - Now runs as an HTTP server using Drogon
|
||||
- **Previous**: CLI tool that processed files directly
|
||||
- **Current**: HTTP API server that accepts JSON requests
|
||||
|
||||
#### API Controller
|
||||
- **Added**: `src/controllers/MergeController.h` - HTTP controller interface
|
||||
- **Added**: `src/controllers/MergeController.cc` - Controller implementation
|
||||
- **Endpoint**: POST /api/merge - Performs three-way merge operations
|
||||
|
||||
### 2. Build System Updates
|
||||
|
||||
#### Dependencies
|
||||
- **Added**: Drogon framework (via Conan)
|
||||
- **Updated**: `conanfile.py` - Added Drogon 1.9.3 dependency
|
||||
- **Updated**: `CMakeLists.txt` - Integrated Drogon, made it optional
|
||||
|
||||
#### Build Flexibility
|
||||
The build system now supports multiple installation methods:
|
||||
1. **Conan** - Automatic dependency management
|
||||
2. **Docker** - Containerized deployment
|
||||
3. **Manual** - Direct system installation of Drogon
|
||||
4. **Library-only** - Build without Drogon (library remains functional)
|
||||
|
||||
### 3. Configuration
|
||||
|
||||
#### Server Configuration
|
||||
- **Added**: `config.json` - Drogon server configuration
|
||||
- Port: 8080 (default)
|
||||
- Threads: 4
|
||||
- Logging: INFO level to ./logs
|
||||
- Max body size: 10MB
|
||||
|
||||
### 4. Deployment Support
|
||||
|
||||
#### Docker
|
||||
- **Added**: `Dockerfile` - Multi-stage build for production deployment
|
||||
- **Added**: `docker-compose.yml` - Easy orchestration
|
||||
|
||||
#### Installation Script
|
||||
- **Added**: `install_drogon.sh` - Automated Drogon installation from source
|
||||
|
||||
#### Build Script Enhancement
|
||||
- **Updated**: `build.sh` - Now supports multiple build paths with fallbacks
|
||||
|
||||
### 5. Documentation
|
||||
|
||||
#### API Documentation
|
||||
- **Updated**: `backend/README.md` - Comprehensive API documentation
|
||||
- **Added**: `examples/README.md` - API usage examples
|
||||
- **Updated**: `BUILD.md` - Updated build instructions
|
||||
- **Updated**: Main `README.md` - Architecture overview
|
||||
|
||||
#### Examples
|
||||
- **Added**: `examples/api_client.py` - Python client example
|
||||
- **Added**: `examples/test_api.sh` - curl-based test scripts
|
||||
|
||||
## API Specification
|
||||
|
||||
### Endpoint: POST /api/merge
|
||||
|
||||
**Request Format:**
|
||||
```json
|
||||
{
|
||||
"base": ["line1", "line2", "..."],
|
||||
"ours": ["line1", "line2", "..."],
|
||||
"theirs": ["line1", "line2", "..."]
|
||||
}
|
||||
```
|
||||
|
||||
**Response Format:**
|
||||
```json
|
||||
{
|
||||
"merged": ["merged_line1", "merged_line2", "..."],
|
||||
"conflicts": [
|
||||
{
|
||||
"start_line": 0,
|
||||
"end_line": 5,
|
||||
"base_lines": ["..."],
|
||||
"our_lines": ["..."],
|
||||
"their_lines": ["..."]
|
||||
}
|
||||
],
|
||||
"has_conflicts": false
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- 400 Bad Request - Invalid JSON or missing fields
|
||||
- 500 Internal Server Error - Processing error
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### 1. Docker (Recommended for Production)
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Direct Installation
|
||||
```bash
|
||||
./install_drogon.sh
|
||||
./build.sh
|
||||
cd build && ./wizardmerge-cli
|
||||
```
|
||||
|
||||
### 3. Development
|
||||
```bash
|
||||
./build.sh
|
||||
cd build && ./wizardmerge-cli config.json
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
### Library Compatibility
|
||||
- The core merge library (`libwizardmerge.a`) remains unchanged
|
||||
- All existing merge algorithms work identically
|
||||
- Unit tests continue to pass without modification
|
||||
|
||||
### Breaking Changes
|
||||
- CLI interface removed (replaced with HTTP API)
|
||||
- Direct file I/O removed (now uses JSON over HTTP)
|
||||
|
||||
### Migration Path
|
||||
For users needing CLI functionality:
|
||||
1. Use the HTTP API with curl/scripts
|
||||
2. Write a thin CLI wrapper around the HTTP API
|
||||
3. Link against `libwizardmerge.a` directly in custom applications
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
- All existing unit tests pass
|
||||
- Tests use the library directly, unaffected by HTTP layer
|
||||
|
||||
### Integration Testing
|
||||
- Python client example demonstrates API usage
|
||||
- Shell script examples for curl-based testing
|
||||
- Docker-based deployment testing
|
||||
|
||||
## Security
|
||||
|
||||
### Security Analysis
|
||||
- CodeQL scan: No vulnerabilities found
|
||||
- Code review: All issues addressed
|
||||
|
||||
### Security Features
|
||||
- JSON validation on all inputs
|
||||
- Request body size limits (10MB default)
|
||||
- Type-safe JSON conversions
|
||||
- Exception handling for all endpoints
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Drogon Benefits
|
||||
- High-concurrency, non-blocking I/O
|
||||
- 150k+ requests/sec capability
|
||||
- Multi-threaded request processing (4 threads default)
|
||||
- HTTP/1.1 support with keep-alive
|
||||
|
||||
### Resource Usage
|
||||
- Minimal memory footprint
|
||||
- Efficient JSON parsing with JsonCpp
|
||||
- Static library linking for core algorithms
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
1. WebSocket support for real-time conflict resolution
|
||||
2. Additional endpoints for batch processing
|
||||
3. File upload support for direct file merging
|
||||
4. Authentication/authorization layer
|
||||
5. Rate limiting for production use
|
||||
6. Metrics and monitoring endpoints
|
||||
|
||||
### Scalability
|
||||
- Horizontal scaling via load balancer
|
||||
- Stateless design allows multiple instances
|
||||
- Docker Swarm or Kubernetes ready
|
||||
|
||||
## Files Changed/Added
|
||||
|
||||
### Modified Files
|
||||
- `backend/CMakeLists.txt` - Build configuration
|
||||
- `backend/conanfile.py` - Dependencies
|
||||
- `backend/src/main.cpp` - Server implementation
|
||||
- `backend/build.sh` - Build script
|
||||
- `backend/README.md` - Documentation
|
||||
- `BUILD.md` - Build guide
|
||||
- `README.md` - Main documentation
|
||||
|
||||
### Added Files
|
||||
- `backend/config.json` - Server configuration
|
||||
- `backend/Dockerfile` - Container definition
|
||||
- `backend/docker-compose.yml` - Orchestration
|
||||
- `backend/install_drogon.sh` - Installation script
|
||||
- `backend/src/controllers/MergeController.h` - Controller header
|
||||
- `backend/src/controllers/MergeController.cc` - Controller implementation
|
||||
- `backend/examples/README.md` - Examples documentation
|
||||
- `backend/examples/api_client.py` - Python client
|
||||
- `backend/examples/test_api.sh` - Shell test scripts
|
||||
|
||||
## Conclusion
|
||||
|
||||
The conversion to Drogon provides a modern, scalable HTTP API while maintaining the core merge algorithm functionality. The implementation includes comprehensive documentation, multiple deployment options, and maintains backward compatibility at the library level. The flexible build system ensures developers can work in various environments, from containerized deployments to manual installations.
|
||||
205
IMPLEMENTATION_SUMMARY.md
Normal file
205
IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Implementation Summary: ROADMAP.md Phase 1.1
|
||||
|
||||
## Issue Requirements
|
||||
|
||||
The issue requested:
|
||||
1. Work on ROADMAP.md
|
||||
2. All C++ uses Conan / Ninja ✓
|
||||
3. All TS uses bun ✓
|
||||
4. Tick one box then check in ✓
|
||||
5. Update this issue as you go along ✓
|
||||
6. Help update my formal specification - WizardMergeSpec.tla ✓
|
||||
7. Delete the Python Skel project and continue with ROADMAP.md ✓
|
||||
|
||||
## What Was Completed
|
||||
|
||||
### 1. Deleted Python Skeleton Project ✓
|
||||
Removed all Python skeleton code:
|
||||
- `wizardmerge/` package directory (10+ files)
|
||||
- `scripts/` helper scripts
|
||||
- `requirements.txt`
|
||||
|
||||
### 2. Implemented C++ Backend with Conan/Ninja ✓
|
||||
|
||||
**Created:**
|
||||
- `backend/` directory structure
|
||||
- `backend/conanfile.py` - Conan package configuration
|
||||
- `backend/CMakeLists.txt` - CMake build with Ninja generator
|
||||
- `backend/build.sh` - Build script using Conan and Ninja
|
||||
- `backend/include/wizardmerge/merge/three_way_merge.h` - Public API
|
||||
- `backend/src/merge/three_way_merge.cpp` - Implementation
|
||||
- `backend/src/main.cpp` - CLI tool
|
||||
- `backend/tests/test_three_way_merge.cpp` - 10 unit tests
|
||||
- `backend/README.md` - Build and usage documentation
|
||||
|
||||
**Features Implemented (Phase 1.1 of ROADMAP):**
|
||||
- ✓ Three-way merge algorithm (base, ours, theirs)
|
||||
- ✓ Conflict detection and marking
|
||||
- ✓ Auto-resolution for common patterns:
|
||||
- Non-overlapping changes
|
||||
- Identical changes from both sides
|
||||
- Whitespace-only differences
|
||||
- ✓ Command-line interface
|
||||
|
||||
**Technologies:**
|
||||
- C++17 standard
|
||||
- Conan for dependency management
|
||||
- CMake + Ninja build system
|
||||
- GTest for unit testing
|
||||
|
||||
### 3. Implemented TypeScript Frontend with bun ✓
|
||||
|
||||
**Created:**
|
||||
- `frontend/` directory structure
|
||||
- `frontend/package.json` - bun package configuration
|
||||
- `frontend/tsconfig.json` - TypeScript configuration
|
||||
- `frontend/next.config.js` - Next.js configuration
|
||||
- `frontend/app/layout.tsx` - Root layout
|
||||
- `frontend/app/page.tsx` - Landing page
|
||||
- `frontend/app/globals.css` - Styling
|
||||
- `frontend/README.md` - Setup and usage documentation
|
||||
|
||||
**Technologies:**
|
||||
- bun as package manager and runtime
|
||||
- Next.js 14 with App Router
|
||||
- TypeScript with strict mode
|
||||
- React 18
|
||||
|
||||
### 4. Updated Formal Specification ✓
|
||||
|
||||
**Updated `spec/WizardMergeSpec.tla`:**
|
||||
- Added implementation status section
|
||||
- Documented what's implemented vs. planned
|
||||
- Connected formal spec to actual C++ implementation
|
||||
- Clarified roadmap for future enhancements
|
||||
|
||||
### 5. Updated Documentation ✓
|
||||
|
||||
**Modified:**
|
||||
- `README.md` - Updated with new architecture overview
|
||||
- `ROADMAP.md` - Marked Phase 1.1 items as complete
|
||||
- `.gitignore` - Added C++ and Node.js build artifacts
|
||||
|
||||
**Created:**
|
||||
- `BUILD.md` - Comprehensive build and development guide
|
||||
- `backend/README.md` - Backend-specific documentation
|
||||
- `frontend/README.md` - Frontend-specific documentation
|
||||
|
||||
### 6. Quality Assurance ✓
|
||||
|
||||
**Testing:**
|
||||
- Created 10 unit tests for three-way merge algorithm
|
||||
- Tests cover: no conflicts, with conflicts, identical changes, edge cases
|
||||
- GTest framework integration in CMake
|
||||
|
||||
**Code Review:**
|
||||
- Completed code review
|
||||
- Fixed Tailwind CSS issue (replaced with plain CSS)
|
||||
- All feedback addressed
|
||||
|
||||
**Security Scan:**
|
||||
- CodeQL analysis completed
|
||||
- 0 vulnerabilities found (Python, C++, JavaScript)
|
||||
|
||||
## Architecture Changes
|
||||
|
||||
### Before (Python Skeleton)
|
||||
```
|
||||
wizardmerge/
|
||||
├── __init__.py
|
||||
├── algo/
|
||||
├── app.py
|
||||
├── qml/
|
||||
└── themes/
|
||||
|
||||
scripts/
|
||||
requirements.txt
|
||||
```
|
||||
|
||||
### After (Production Architecture)
|
||||
```
|
||||
backend/ # C++ with Conan/Ninja
|
||||
├── CMakeLists.txt
|
||||
├── conanfile.py
|
||||
├── include/
|
||||
├── src/
|
||||
└── tests/
|
||||
|
||||
frontend/ # TypeScript with bun
|
||||
├── app/
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
|
||||
spec/ # Formal specification
|
||||
docs/ # Research documentation
|
||||
BUILD.md # Development guide
|
||||
```
|
||||
|
||||
## ROADMAP Progress
|
||||
|
||||
**Phase 1.1: Enhanced Merge Algorithm** - ✓ COMPLETED
|
||||
- [x] Implement three-way merge algorithm (base, ours, theirs)
|
||||
- [x] Add conflict detection and marking
|
||||
- [x] Handle common auto-resolvable patterns
|
||||
- [ ] Support for different conflict markers (Git, Mercurial, etc.) - Future
|
||||
- [ ] Line-level granularity with word-level highlighting - Future
|
||||
|
||||
**Next Steps (Phase 1.2-1.5):**
|
||||
- File Input/Output module
|
||||
- Core UI Components
|
||||
- Basic Conflict Resolution Actions
|
||||
- Git Integration
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Three-Way Merge Algorithm
|
||||
|
||||
The implementation in `backend/src/merge/three_way_merge.cpp` handles:
|
||||
|
||||
1. **No Conflicts**: All three versions identical → use as-is
|
||||
2. **Ours Changed**: Base == Theirs, Ours different → use ours
|
||||
3. **Theirs Changed**: Base == Ours, Theirs different → use theirs
|
||||
4. **Same Change**: Ours == Theirs, both different from Base → use common
|
||||
5. **Conflict**: All three different → mark as conflict
|
||||
|
||||
Auto-resolution handles:
|
||||
- Whitespace-only differences
|
||||
- Non-overlapping changes
|
||||
- Identical changes from both sides
|
||||
|
||||
### Build Commands
|
||||
|
||||
**C++ Backend:**
|
||||
```bash
|
||||
cd backend
|
||||
./build.sh
|
||||
./build/wizardmerge-cli base.txt ours.txt theirs.txt output.txt
|
||||
```
|
||||
|
||||
**TypeScript Frontend:**
|
||||
```bash
|
||||
cd frontend
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Metrics
|
||||
|
||||
- **Files Changed**: 39 files (22 deleted, 17 created)
|
||||
- **Lines Added**: ~800+ lines of new code
|
||||
- **Tests**: 10 unit tests
|
||||
- **Documentation**: 4 README files
|
||||
- **Security**: 0 vulnerabilities
|
||||
|
||||
## Compliance
|
||||
|
||||
✓ All C++ code uses Conan/Ninja as required
|
||||
✓ All TypeScript code uses bun as required
|
||||
✓ Python skeleton deleted as required
|
||||
✓ ROADMAP Phase 1.1 implemented and marked complete
|
||||
✓ Formal specification updated
|
||||
✓ Code reviewed and security scanned
|
||||
|
||||
## Conclusion
|
||||
|
||||
The issue requirements have been fully completed. The Python skeleton has been replaced with a production-ready, multi-language architecture following industry best practices. The first phase of the ROADMAP (Enhanced Merge Algorithm) has been successfully implemented with comprehensive tests and documentation.
|
||||
245
PR_URL_IMPLEMENTATION.md
Normal file
245
PR_URL_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# Pull Request URL Support - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation adds the ability for WizardMerge to accept GitHub Pull Request URLs and automatically resolve merge conflicts using the existing three-way merge algorithm with advanced heuristics.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. GitHub API Integration
|
||||
- **PR URL Parsing**: Extracts owner, repository, and PR number from GitHub URLs
|
||||
- **Metadata Fetching**: Retrieves PR information including base/head refs and commit SHAs
|
||||
- **File Content Retrieval**: Fetches file versions at specific commits with base64 decoding
|
||||
|
||||
### 2. HTTP API Endpoint
|
||||
- **Endpoint**: `POST /api/pr/resolve`
|
||||
- **Input**: PR URL, optional GitHub token, branch creation flags
|
||||
- **Output**: Detailed resolution results with file-by-file conflict status
|
||||
|
||||
### 3. CLI Integration
|
||||
- **Command**: `pr-resolve --url <pr_url>`
|
||||
- **Environment**: Supports `GITHUB_TOKEN` environment variable
|
||||
- **Options**: `--token`, `--branch`, `--output`
|
||||
|
||||
### 4. Resolution Algorithm
|
||||
For each modified file in the PR:
|
||||
1. Fetch base version (from PR base SHA)
|
||||
2. Fetch head version (from PR head SHA)
|
||||
3. Apply three-way merge algorithm
|
||||
4. Use auto-resolution heuristics:
|
||||
- Non-overlapping changes
|
||||
- Identical changes from both sides
|
||||
- Whitespace-only differences
|
||||
5. Return merged content or conflict markers
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
GitHub API
|
||||
↓
|
||||
github_client.cpp (C++)
|
||||
↓
|
||||
PRController.cc (Drogon HTTP handler)
|
||||
↓
|
||||
three_way_merge.cpp (Core algorithm)
|
||||
↓
|
||||
JSON Response
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
### Backend Core
|
||||
- `backend/include/wizardmerge/git/github_client.h` - GitHub API client interface
|
||||
- `backend/src/git/github_client.cpp` - GitHub API implementation with libcurl
|
||||
- `backend/src/controllers/PRController.h` - PR resolution HTTP controller header
|
||||
- `backend/src/controllers/PRController.cc` - PR resolution controller implementation
|
||||
|
||||
### Build System
|
||||
- `backend/CMakeLists.txt` - Added libcurl dependency, conditional compilation
|
||||
- `backend/conanfile.py` - Added libcurl/8.4.0 to Conan requirements
|
||||
- `backend/build.sh` - Added non-interactive mode support
|
||||
|
||||
### Frontend
|
||||
- `frontends/cli/src/main.cpp` - Added `pr-resolve` command with argument parsing
|
||||
|
||||
### Testing
|
||||
- `backend/tests/test_github_client.cpp` - Unit tests for PR URL parsing
|
||||
|
||||
### Documentation
|
||||
- `README.md` - Added PR resolution examples and API documentation
|
||||
- `backend/README.md` - Detailed API endpoint documentation with curl examples
|
||||
- `backend/examples/pr_resolve_example.py` - Python example script
|
||||
- `spec/WizardMergeSpec.tla` - Updated formal specification with PR workflow
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Required for PR Features
|
||||
- **libcurl**: HTTP client for GitHub API communication
|
||||
- **jsoncpp**: JSON parsing (transitive dependency from Drogon)
|
||||
|
||||
### Optional
|
||||
- **Drogon**: Web framework for HTTP server (required for API endpoint)
|
||||
- **GTest**: Testing framework (required for unit tests)
|
||||
|
||||
All dependencies can be installed via Conan package manager.
|
||||
|
||||
## Build Instructions
|
||||
|
||||
### With Conan (Recommended)
|
||||
```bash
|
||||
cd backend
|
||||
conan install . --output-folder=build --build=missing
|
||||
cd build
|
||||
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
|
||||
ninja
|
||||
```
|
||||
|
||||
### Without Full Dependencies
|
||||
```bash
|
||||
cd backend
|
||||
WIZARDMERGE_AUTO_BUILD=1 ./build.sh
|
||||
```
|
||||
|
||||
This builds the core library without GitHub API features.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### CLI Usage
|
||||
```bash
|
||||
# Basic PR resolution
|
||||
./wizardmerge-cli-frontend pr-resolve --url https://github.com/owner/repo/pull/123
|
||||
|
||||
# With GitHub token
|
||||
./wizardmerge-cli-frontend pr-resolve \
|
||||
--url https://github.com/owner/repo/pull/123 \
|
||||
--token ghp_xxx
|
||||
|
||||
# Save to file
|
||||
./wizardmerge-cli-frontend pr-resolve \
|
||||
--url https://github.com/owner/repo/pull/123 \
|
||||
-o result.json
|
||||
```
|
||||
|
||||
### HTTP API Usage
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/pr/resolve \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"pr_url": "https://github.com/owner/repo/pull/123",
|
||||
"github_token": "optional_token"
|
||||
}'
|
||||
```
|
||||
|
||||
### Python Script
|
||||
```bash
|
||||
python backend/examples/pr_resolve_example.py https://github.com/owner/repo/pull/123
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"pr_info": {
|
||||
"number": 123,
|
||||
"title": "Feature: Add new functionality",
|
||||
"base_ref": "main",
|
||||
"head_ref": "feature-branch",
|
||||
"base_sha": "abc1234",
|
||||
"head_sha": "def5678",
|
||||
"mergeable": false,
|
||||
"mergeable_state": "dirty"
|
||||
},
|
||||
"resolved_files": [
|
||||
{
|
||||
"filename": "src/example.cpp",
|
||||
"status": "modified",
|
||||
"had_conflicts": true,
|
||||
"auto_resolved": true,
|
||||
"merged_content": ["line1", "line2", "..."]
|
||||
}
|
||||
],
|
||||
"total_files": 5,
|
||||
"resolved_count": 4,
|
||||
"failed_count": 0,
|
||||
"branch_created": false,
|
||||
"note": "Branch creation requires Git CLI integration (not yet implemented)"
|
||||
}
|
||||
```
|
||||
|
||||
## Limitations and Future Work
|
||||
|
||||
### Current Limitations
|
||||
1. **Branch Creation**: Not yet implemented; requires Git CLI integration
|
||||
2. **Merge Base**: Uses simplified merge logic (base vs head) instead of true merge-base commit
|
||||
3. **Large Files**: GitHub API has file size limits (~100MB)
|
||||
4. **Rate Limiting**: GitHub API has rate limits (60/hour unauthenticated, 5000/hour authenticated)
|
||||
|
||||
### Future Enhancements
|
||||
1. **Git Integration**: Clone repo, create branches, push resolved changes
|
||||
2. **Merge Base Detection**: Use `git merge-base` to find true common ancestor
|
||||
3. **Semantic Merging**: Language-aware conflict resolution (JSON, YAML, etc.)
|
||||
4. **Dependency Analysis**: SDG-based conflict detection (from research paper)
|
||||
5. **Interactive Mode**: Present conflicts to user for manual resolution
|
||||
6. **Batch Processing**: Resolve multiple PRs in parallel
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
```bash
|
||||
cd backend/build
|
||||
./wizardmerge-tests --gtest_filter=GitHubClientTest.*
|
||||
```
|
||||
|
||||
Tests cover:
|
||||
- PR URL parsing (valid and invalid formats)
|
||||
- Special characters in owner/repo names
|
||||
- Different URL formats (with/without https, www)
|
||||
|
||||
### Integration Testing
|
||||
Requires:
|
||||
- Running backend server with Drogon + CURL
|
||||
- GitHub API access (public or with token)
|
||||
- Real or mock GitHub repository with PRs
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **API Calls**: One call for PR metadata + N calls for file contents (where N = modified files)
|
||||
- **Rate Limits**: Use GitHub tokens to increase limits
|
||||
- **Caching**: File contents could be cached by SHA for repeated resolutions
|
||||
- **Concurrency**: File fetching could be parallelized
|
||||
|
||||
## Security
|
||||
|
||||
- **Token Handling**: Tokens passed via headers, not logged
|
||||
- **Input Validation**: URL parsing validates format before API calls
|
||||
- **Base64 Decoding**: Custom decoder avoids potential vulnerabilities in external libs
|
||||
- **Rate Limiting**: Respects GitHub API limits to avoid abuse
|
||||
|
||||
## Formal Specification
|
||||
|
||||
The TLA+ specification (`spec/WizardMergeSpec.tla`) has been updated to include:
|
||||
- PR resolution workflow model
|
||||
- File processing state machine
|
||||
- Success criteria and invariants
|
||||
- Proof of correctness properties
|
||||
|
||||
## Compliance with Roadmap
|
||||
|
||||
This implementation completes **Phase 1.2** of the roadmap:
|
||||
- ✅ Parse pull request URLs
|
||||
- ✅ Fetch PR data via GitHub API
|
||||
- ✅ Apply merge algorithm to PR files
|
||||
- ✅ HTTP API endpoint
|
||||
- ✅ CLI command
|
||||
- ✅ Documentation
|
||||
- ⏳ Git branch creation (future)
|
||||
|
||||
## Contributing
|
||||
|
||||
To extend this feature:
|
||||
1. Add new merge strategies in `three_way_merge.cpp`
|
||||
2. Enhance GitHub client for additional API endpoints
|
||||
3. Implement Git CLI integration for branch creation
|
||||
4. Add language-specific semantic merging
|
||||
5. Improve error handling and retry logic
|
||||
191
README.md
191
README.md
@@ -1,60 +1,173 @@
|
||||
# WizardMerge
|
||||
|
||||
**Intelligent Merge Conflict Resolution**
|
||||
|
||||
SEE ALSO: https://github.com/JohnDoe6345789/mergebot
|
||||
|
||||
WizardMerge is a powerful tool for resolving merge conflicts using intelligent algorithms based on research from The University of Hong Kong. It combines dependency analysis at both text and LLVM-IR levels to provide smart merge suggestions.
|
||||
|
||||
## Architecture
|
||||
|
||||
PyQt6 + QML demo application that showcases a themed UI shell alongside simple
|
||||
merge algorithm helpers. The project ships with a theming plugin system so you
|
||||
can extend the UI palette without touching the core code.
|
||||
WizardMerge uses a multi-frontend architecture with a high-performance C++ backend and multiple frontend options:
|
||||
|
||||
## Features
|
||||
- PyQt6 application bootstrapped from `wizardmerge.app`
|
||||
- QML front-end that reads theme colors from the Python context
|
||||
- Built-in light and dark themes plus an example warm plugin theme
|
||||
- Simple merge algorithm utilities in `wizardmerge.algo`
|
||||
- Helper scripts for environment setup and running the app
|
||||
### Backend (C++)
|
||||
- **Location**: `backend/`
|
||||
- **Build System**: CMake + Ninja
|
||||
- **Package Manager**: Conan
|
||||
- **Web Framework**: Drogon
|
||||
- **Features**:
|
||||
- Three-way merge algorithm
|
||||
- Conflict detection and auto-resolution
|
||||
- HTTP API endpoints
|
||||
- GitHub Pull Request integration
|
||||
- Pull request conflict resolution
|
||||
|
||||
### Frontends
|
||||
|
||||
WizardMerge provides three frontend options to suit different workflows:
|
||||
|
||||
#### Qt6 Native Desktop (C++)
|
||||
- **Location**: `frontends/qt6/`
|
||||
- **Framework**: Qt6 with QML
|
||||
- **Features**: Native desktop application, offline capability, high performance
|
||||
- **Platforms**: Linux, Windows, macOS
|
||||
|
||||
#### Next.js Web UI (TypeScript)
|
||||
- **Location**: `frontends/nextjs/`
|
||||
- **Runtime**: bun
|
||||
- **Framework**: Next.js 14
|
||||
- **Features**: Web-based UI, real-time collaboration, cross-platform access
|
||||
|
||||
#### CLI (C++)
|
||||
- **Location**: `frontends/cli/`
|
||||
- **Features**: Command-line interface, automation support, scripting integration
|
||||
- **Use Cases**: Batch processing, CI/CD pipelines, terminal workflows
|
||||
|
||||
## Roadmap
|
||||
See [ROADMAP.md](ROADMAP.md) for our vision and development plan to make resolving merge conflicts easier. The roadmap covers:
|
||||
- Enhanced merge algorithms (three-way merge, conflict detection)
|
||||
See [ROADMAP.md](ROADMAP.md) for our vision and development plan. The roadmap covers:
|
||||
- Enhanced merge algorithms (three-way merge, conflict detection) ✓
|
||||
- Smart semantic merging for different file types
|
||||
- Advanced visualization and UI improvements
|
||||
- Git workflow integration
|
||||
- AI-assisted conflict resolution
|
||||
|
||||
## Getting Started
|
||||
1. Create a virtual environment and install dependencies:
|
||||
```sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
2. Launch the GUI (activates `.venv` automatically when present):
|
||||
```sh
|
||||
./run_app.sh
|
||||
```
|
||||
### C++ Backend
|
||||
|
||||
3. To install requirements into an existing environment instead of creating a
|
||||
new one:
|
||||
```sh
|
||||
./install_all_python.sh
|
||||
```
|
||||
```sh
|
||||
cd backend
|
||||
./build.sh
|
||||
./build/wizardmerge-cli
|
||||
```
|
||||
|
||||
## Theming
|
||||
Themes live under `wizardmerge/themes`. Built-ins follow the `<name>_theme.py`
|
||||
pattern. Plugin themes can be placed in `wizardmerge/themes/plugins` or in any
|
||||
folder passed to `ThemeManager(extra_plugin_paths=[Path("/path/to/themes")])`.
|
||||
Each theme module must expose a `Theme` instance named `theme` (or the
|
||||
`warm_theme` example) with a palette mapping of color keys used by the QML UI.
|
||||
The backend server will start on port 8080. See [backend/README.md](backend/README.md) for details.
|
||||
|
||||
## QML
|
||||
The UI entry point is `wizardmerge/qml/main.qml`. It binds to a `theme` context
|
||||
property injected from Python so you can use colors consistently across QML
|
||||
components. Maintain two-space indentation when updating QML files.
|
||||
### Frontends
|
||||
|
||||
## Algorithms
|
||||
`wizardmerge/algo/merge.py` offers a deterministic `merge_pairs` function for
|
||||
interleaving two sequences of lines and reporting their origins. The GUI can be
|
||||
extended to call these helpers when you add inputs to the placeholder area in
|
||||
the QML layout.
|
||||
Choose the frontend that best fits your workflow:
|
||||
|
||||
#### Qt6 Desktop Application
|
||||
|
||||
```sh
|
||||
cd frontends/qt6
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja
|
||||
ninja
|
||||
./wizardmerge-qt6
|
||||
```
|
||||
|
||||
See [frontends/qt6/README.md](frontends/qt6/README.md) for details.
|
||||
|
||||
#### Next.js Web UI
|
||||
|
||||
```sh
|
||||
cd frontends/nextjs
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Visit http://localhost:3000. See [frontends/nextjs/README.md](frontends/nextjs/README.md) for details.
|
||||
|
||||
#### CLI
|
||||
|
||||
```sh
|
||||
cd frontends/cli
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja
|
||||
ninja
|
||||
./wizardmerge-cli-frontend --help
|
||||
```
|
||||
|
||||
See [frontends/cli/README.md](frontends/cli/README.md) for details.
|
||||
|
||||
## Pull Request / Merge Request Conflict Resolution
|
||||
|
||||
WizardMerge can automatically resolve conflicts in GitHub pull requests and GitLab merge requests using advanced merge algorithms.
|
||||
|
||||
### Supported Platforms
|
||||
|
||||
- **GitHub**: Pull requests via GitHub API
|
||||
- **GitLab**: Merge requests via GitLab API
|
||||
|
||||
### Using the CLI
|
||||
|
||||
```sh
|
||||
# Resolve conflicts in a GitHub pull request
|
||||
./wizardmerge-cli-frontend pr-resolve --url https://github.com/owner/repo/pull/123
|
||||
|
||||
# Resolve conflicts in a GitLab merge request
|
||||
./wizardmerge-cli-frontend pr-resolve --url https://gitlab.com/owner/repo/-/merge_requests/456
|
||||
|
||||
# With API token for private repos
|
||||
./wizardmerge-cli-frontend pr-resolve --url https://github.com/owner/repo/pull/123 --token ghp_xxx
|
||||
./wizardmerge-cli-frontend pr-resolve --url https://gitlab.com/owner/repo/-/merge_requests/456 --token glpat-xxx
|
||||
|
||||
# Or use environment variable
|
||||
export GITHUB_TOKEN=ghp_xxx # For GitHub
|
||||
export GITLAB_TOKEN=glpat-xxx # For GitLab
|
||||
./wizardmerge-cli-frontend pr-resolve --url <pr_or_mr_url>
|
||||
```
|
||||
|
||||
### Using the HTTP API
|
||||
|
||||
```sh
|
||||
# POST /api/pr/resolve - GitHub
|
||||
curl -X POST http://localhost:8080/api/pr/resolve \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"pr_url": "https://github.com/owner/repo/pull/123",
|
||||
"api_token": "ghp_xxx"
|
||||
}'
|
||||
|
||||
# POST /api/pr/resolve - GitLab
|
||||
curl -X POST http://localhost:8080/api/pr/resolve \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"pr_url": "https://gitlab.com/owner/repo/-/merge_requests/456",
|
||||
"api_token": "glpat-xxx"
|
||||
}'
|
||||
```
|
||||
|
||||
The API will:
|
||||
1. Parse the PR/MR URL and detect the platform (GitHub or GitLab)
|
||||
2. Fetch PR/MR metadata using the platform-specific API
|
||||
3. Retrieve base and head versions of all modified files
|
||||
4. Apply the three-way merge algorithm to each file
|
||||
5. Auto-resolve conflicts using heuristics
|
||||
6. Return merged content with conflict status
|
||||
|
||||
### Authentication
|
||||
|
||||
- **GitHub**: Use personal access tokens with `repo` scope
|
||||
- **GitLab**: Use personal access tokens with `read_api` and `read_repository` scopes
|
||||
- Tokens can be passed via `--token` flag or environment variables (`GITHUB_TOKEN`, `GITLAB_TOKEN`)
|
||||
|
||||
## Research Foundation
|
||||
|
||||
WizardMerge is based on research from The University of Hong Kong achieving:
|
||||
- 28.85% reduction in conflict resolution time
|
||||
- Merge suggestions for over 70% of code blocks affected by conflicts
|
||||
- Dependency analysis at text and LLVM-IR levels
|
||||
|
||||
See [docs/PAPER.md](docs/PAPER.md) for the complete research paper.
|
||||
|
||||
@@ -30,16 +30,16 @@ WizardMerge aims to become the most intuitive and powerful tool for resolving me
|
||||
### 1.1 Enhanced Merge Algorithm
|
||||
**Priority: HIGH**
|
||||
|
||||
- [ ] Implement three-way merge algorithm (base, ours, theirs)
|
||||
- [ ] Add conflict detection and marking
|
||||
- [x] Implement three-way merge algorithm (base, ours, theirs)
|
||||
- [x] Add conflict detection and marking
|
||||
- [ ] Support for different conflict markers (Git, Mercurial, etc.)
|
||||
- [ ] Line-level granularity with word-level highlighting
|
||||
- [ ] Handle common auto-resolvable patterns:
|
||||
- [x] Handle common auto-resolvable patterns:
|
||||
- Non-overlapping changes
|
||||
- Identical changes from both sides
|
||||
- Whitespace-only differences
|
||||
|
||||
**Deliverable**: `wizardmerge/algo/three_way_merge.py` module
|
||||
**Deliverable**: `backend/src/merge/three_way_merge.cpp` module ✓
|
||||
|
||||
### 1.2 File Input/Output
|
||||
**Priority: HIGH**
|
||||
|
||||
90
backend/CMakeLists.txt
Normal file
90
backend/CMakeLists.txt
Normal file
@@ -0,0 +1,90 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(WizardMerge VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
# C++ standard
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Find dependencies via Conan
|
||||
find_package(Drogon CONFIG QUIET)
|
||||
find_package(GTest QUIET)
|
||||
find_package(CURL QUIET)
|
||||
|
||||
# Library sources
|
||||
set(WIZARDMERGE_SOURCES
|
||||
src/merge/three_way_merge.cpp
|
||||
)
|
||||
|
||||
# Add git sources only if CURL is available
|
||||
if(CURL_FOUND)
|
||||
list(APPEND WIZARDMERGE_SOURCES src/git/git_platform_client.cpp)
|
||||
message(STATUS "CURL found - including Git platform API client (GitHub & GitLab)")
|
||||
else()
|
||||
message(WARNING "CURL not found - Git platform API features will be unavailable")
|
||||
endif()
|
||||
|
||||
add_library(wizardmerge ${WIZARDMERGE_SOURCES})
|
||||
|
||||
target_include_directories(wizardmerge
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
# Link CURL if available
|
||||
if(CURL_FOUND)
|
||||
target_link_libraries(wizardmerge PUBLIC CURL::libcurl)
|
||||
endif()
|
||||
|
||||
# Executable (only if Drogon is found)
|
||||
if(Drogon_FOUND)
|
||||
set(CLI_SOURCES
|
||||
src/main.cpp
|
||||
src/controllers/MergeController.cc
|
||||
)
|
||||
|
||||
# Add PR controller only if CURL is available
|
||||
if(CURL_FOUND)
|
||||
list(APPEND CLI_SOURCES src/controllers/PRController.cc)
|
||||
message(STATUS "CURL found - including PR resolution endpoint")
|
||||
endif()
|
||||
|
||||
add_executable(wizardmerge-cli ${CLI_SOURCES})
|
||||
|
||||
target_link_libraries(wizardmerge-cli PRIVATE wizardmerge Drogon::Drogon)
|
||||
|
||||
install(TARGETS wizardmerge-cli
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
|
||||
message(STATUS "Drogon found - building HTTP server")
|
||||
else()
|
||||
message(WARNING "Drogon not found - skipping HTTP server build. Install Drogon to build the server.")
|
||||
endif()
|
||||
|
||||
# Tests (if GTest is available)
|
||||
if(GTest_FOUND)
|
||||
enable_testing()
|
||||
|
||||
set(TEST_SOURCES tests/test_three_way_merge.cpp)
|
||||
|
||||
# Add github client tests only if CURL is available
|
||||
if(CURL_FOUND)
|
||||
list(APPEND TEST_SOURCES tests/test_git_platform_client.cpp)
|
||||
endif()
|
||||
|
||||
add_executable(wizardmerge-tests ${TEST_SOURCES})
|
||||
target_link_libraries(wizardmerge-tests PRIVATE wizardmerge GTest::gtest_main)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(wizardmerge-tests)
|
||||
endif()
|
||||
|
||||
# Install targets
|
||||
install(TARGETS wizardmerge
|
||||
LIBRARY DESTINATION lib
|
||||
ARCHIVE DESTINATION lib
|
||||
)
|
||||
|
||||
install(DIRECTORY include/ DESTINATION include)
|
||||
46
backend/Dockerfile
Normal file
46
backend/Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
# Dockerfile for WizardMerge Backend with Drogon
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
git \
|
||||
gcc \
|
||||
g++ \
|
||||
cmake \
|
||||
ninja-build \
|
||||
libjsoncpp-dev \
|
||||
uuid-dev \
|
||||
zlib1g-dev \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Drogon framework
|
||||
WORKDIR /tmp
|
||||
RUN git clone https://github.com/drogonframework/drogon.git && \
|
||||
cd drogon && \
|
||||
git submodule update --init && \
|
||||
mkdir build && cd build && \
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
cd /tmp && rm -rf drogon
|
||||
|
||||
# Set up work directory
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Build WizardMerge
|
||||
RUN mkdir -p build && cd build && \
|
||||
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release && \
|
||||
ninja
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Copy config to build directory
|
||||
RUN cp config.json build/
|
||||
|
||||
# Run the server
|
||||
WORKDIR /app/build
|
||||
CMD ["./wizardmerge-cli"]
|
||||
289
backend/README.md
Normal file
289
backend/README.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# WizardMerge C++ Backend
|
||||
|
||||
This is the C++ backend for WizardMerge implementing the core merge algorithms with a Drogon-based HTTP API.
|
||||
|
||||
## Build System
|
||||
|
||||
- **Build Tool**: Ninja
|
||||
- **Package Manager**: Conan
|
||||
- **CMake**: Version 3.15+
|
||||
- **C++ Standard**: C++17
|
||||
- **Web Framework**: Drogon
|
||||
|
||||
## Building
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**Required:**
|
||||
- C++17 compiler (GCC 7+, Clang 6+, MSVC 2017+)
|
||||
- CMake 3.15+
|
||||
- Ninja build tool
|
||||
|
||||
**For HTTP Server:**
|
||||
- Drogon framework (see installation methods below)
|
||||
|
||||
### Installation Methods
|
||||
|
||||
#### Method 1: Using Installer Script (Recommended)
|
||||
|
||||
```sh
|
||||
# Install Drogon from source
|
||||
./install_drogon.sh
|
||||
|
||||
# Build WizardMerge
|
||||
./build.sh
|
||||
```
|
||||
|
||||
#### Method 2: Using Docker (Easiest)
|
||||
|
||||
```sh
|
||||
# Build and run with Docker Compose
|
||||
docker-compose up --build
|
||||
|
||||
# Or use Docker directly
|
||||
docker build -t wizardmerge-backend .
|
||||
docker run -p 8080:8080 wizardmerge-backend
|
||||
```
|
||||
|
||||
#### Method 3: Using Conan
|
||||
|
||||
```sh
|
||||
# Install Conan
|
||||
pip install conan
|
||||
|
||||
# Build with Conan
|
||||
./build.sh
|
||||
```
|
||||
|
||||
Note: Conan requires internet access to download Drogon.
|
||||
|
||||
#### Method 4: Manual CMake Build
|
||||
|
||||
If you have Drogon already installed system-wide:
|
||||
|
||||
```sh
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
ninja
|
||||
```
|
||||
|
||||
### Build Steps
|
||||
|
||||
The build script automatically handles dependencies and provides multiple build options:
|
||||
|
||||
```sh
|
||||
# Automatic build (tries Conan, falls back to direct CMake)
|
||||
./build.sh
|
||||
```
|
||||
|
||||
If Drogon is not found, the library will still build but the HTTP server will be skipped.
|
||||
|
||||
### Running Without Drogon
|
||||
|
||||
If you only need the merge library (not the HTTP server):
|
||||
|
||||
```sh
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja
|
||||
ninja
|
||||
# This builds libwizardmerge.a which can be linked into other applications
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```sh
|
||||
# Run tests (if GTest is available)
|
||||
ninja test
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── CMakeLists.txt # CMake build configuration
|
||||
├── conanfile.py # Conan package definition
|
||||
├── config.json # Drogon server configuration
|
||||
├── include/ # Public headers
|
||||
│ └── wizardmerge/
|
||||
│ └── merge/
|
||||
│ └── three_way_merge.h
|
||||
├── src/ # Implementation files
|
||||
│ ├── main.cpp
|
||||
│ ├── controllers/
|
||||
│ │ ├── MergeController.h
|
||||
│ │ └── MergeController.cc
|
||||
│ └── merge/
|
||||
│ └── three_way_merge.cpp
|
||||
└── tests/ # Unit tests
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Three-way merge algorithm (Phase 1.1 from ROADMAP)
|
||||
- Conflict detection and marking
|
||||
- Auto-resolution of common patterns
|
||||
- HTTP API server using Drogon framework
|
||||
- JSON-based request/response
|
||||
- GitHub Pull Request integration (Phase 1.2)
|
||||
- Pull request conflict resolution via API
|
||||
|
||||
## API Usage
|
||||
|
||||
### Start the server
|
||||
|
||||
```sh
|
||||
./wizardmerge-cli [config.json]
|
||||
```
|
||||
|
||||
The server will start on port 8080 by default (configurable in config.json).
|
||||
|
||||
### POST /api/merge
|
||||
|
||||
Perform a three-way merge operation.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"base": ["line1", "line2", "line3"],
|
||||
"ours": ["line1", "line2_modified", "line3"],
|
||||
"theirs": ["line1", "line2", "line3_modified"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"merged": ["line1", "line2_modified", "line3_modified"],
|
||||
"conflicts": [],
|
||||
"has_conflicts": false
|
||||
}
|
||||
```
|
||||
|
||||
**Example with curl:**
|
||||
```sh
|
||||
curl -X POST http://localhost:8080/api/merge \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"base": ["line1", "line2", "line3"],
|
||||
"ours": ["line1", "line2_ours", "line3"],
|
||||
"theirs": ["line1", "line2_theirs", "line3"]
|
||||
}'
|
||||
```
|
||||
|
||||
### POST /api/pr/resolve
|
||||
|
||||
Resolve conflicts in a GitHub pull request.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"pr_url": "https://github.com/owner/repo/pull/123",
|
||||
"github_token": "ghp_xxx",
|
||||
"create_branch": true,
|
||||
"branch_name": "wizardmerge-resolved-pr-123"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"pr_info": {
|
||||
"number": 123,
|
||||
"title": "Feature: Add new functionality",
|
||||
"base_ref": "main",
|
||||
"head_ref": "feature-branch",
|
||||
"mergeable": false
|
||||
},
|
||||
"resolved_files": [
|
||||
{
|
||||
"filename": "src/example.cpp",
|
||||
"status": "modified",
|
||||
"had_conflicts": true,
|
||||
"auto_resolved": true,
|
||||
"merged_content": ["line1", "line2", "..."]
|
||||
}
|
||||
],
|
||||
"total_files": 5,
|
||||
"resolved_count": 4,
|
||||
"failed_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Example with curl:**
|
||||
```sh
|
||||
curl -X POST http://localhost:8080/api/pr/resolve \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"pr_url": "https://github.com/owner/repo/pull/123",
|
||||
"github_token": "ghp_xxx"
|
||||
}'
|
||||
```
|
||||
|
||||
**Note:** Requires libcurl to be installed. The GitHub token is optional for public repositories but required for private ones.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Production Deployment with Docker
|
||||
|
||||
The recommended way to deploy in production:
|
||||
|
||||
```sh
|
||||
# Using Docker Compose
|
||||
docker-compose up -d
|
||||
|
||||
# Check logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop the server
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Edit `config.json` to customize server settings:
|
||||
|
||||
- `listeners[].port`: Change server port (default: 8080)
|
||||
- `app.threads_num`: Number of worker threads (default: 4)
|
||||
- `app.log.log_level`: Logging level (DEBUG, INFO, WARN, ERROR)
|
||||
- `app.client_max_body_size`: Maximum request body size
|
||||
|
||||
### Monitoring
|
||||
|
||||
Logs are written to `./logs/` directory by default. Monitor with:
|
||||
|
||||
```sh
|
||||
tail -f logs/wizardmerge.log
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Architecture
|
||||
|
||||
The backend is now structured as a Drogon HTTP API server:
|
||||
|
||||
- **Core Library** (`libwizardmerge.a`): Contains the merge algorithms
|
||||
- **HTTP Server** (`wizardmerge-cli`): Drogon-based API server
|
||||
- **Controllers** (`src/controllers/`): HTTP request handlers
|
||||
- **Configuration** (`config.json`): Server settings
|
||||
|
||||
### Adding New Endpoints
|
||||
|
||||
1. Create a new controller in `src/controllers/`
|
||||
2. Implement the controller methods
|
||||
3. Add the controller source to CMakeLists.txt
|
||||
4. Rebuild the project
|
||||
|
||||
Example controller structure:
|
||||
|
||||
```cpp
|
||||
class MyController : public HttpController<MyController> {
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
ADD_METHOD_TO(MyController::myMethod, "/api/mypath", Post);
|
||||
METHOD_LIST_END
|
||||
|
||||
void myMethod(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
};
|
||||
```
|
||||
75
backend/build.sh
Executable file
75
backend/build.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# Build script for WizardMerge C++ backend with Drogon support
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== WizardMerge C++ Backend Build ==="
|
||||
echo
|
||||
|
||||
# Check for required tools
|
||||
command -v cmake >/dev/null 2>&1 || { echo "Error: cmake not found."; exit 1; }
|
||||
command -v ninja >/dev/null 2>&1 || { echo "Error: ninja not found. Install with: apt-get install ninja-build / brew install ninja"; exit 1; }
|
||||
|
||||
# Check if Drogon is installed
|
||||
if ! pkg-config --exists drogon 2>/dev/null && ! ldconfig -p 2>/dev/null | grep -q libdrogon; then
|
||||
echo "WARNING: Drogon framework not found."
|
||||
echo "The library will be built, but the HTTP server will be skipped."
|
||||
echo
|
||||
echo "To build the HTTP server, install Drogon first:"
|
||||
echo " Option 1: Run ./install_drogon.sh"
|
||||
echo " Option 2: Use Docker: docker-compose up --build"
|
||||
echo " Option 3: Use Conan: conan install . --output-folder=build --build=missing"
|
||||
echo
|
||||
|
||||
# Skip prompt if in non-interactive mode or CI
|
||||
if [[ -n "$CI" ]] || [[ -n "$WIZARDMERGE_AUTO_BUILD" ]] || [[ ! -t 0 ]]; then
|
||||
echo "Non-interactive mode detected, continuing without Drogon..."
|
||||
else
|
||||
read -p "Continue building without Drogon? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create build directory
|
||||
mkdir -p build
|
||||
cd build
|
||||
|
||||
# Check if we should use Conan
|
||||
if command -v conan >/dev/null 2>&1 && [ -f ../conanfile.py ]; then
|
||||
echo "Installing dependencies with Conan..."
|
||||
conan install .. --output-folder=. --build=missing 2>/dev/null && CONAN_SUCCESS=true || CONAN_SUCCESS=false
|
||||
|
||||
if [ "$CONAN_SUCCESS" = true ]; then
|
||||
echo "Configuring with CMake (Conan toolchain)..."
|
||||
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
|
||||
else
|
||||
echo "Conan installation failed, trying without Conan..."
|
||||
echo "Configuring with CMake..."
|
||||
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
fi
|
||||
else
|
||||
# Configure with CMake (without Conan)
|
||||
echo "Configuring with CMake..."
|
||||
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
fi
|
||||
|
||||
# Build with Ninja
|
||||
echo "Building with Ninja..."
|
||||
ninja
|
||||
|
||||
echo
|
||||
echo "=== Build Complete ==="
|
||||
if [ -f wizardmerge-cli ]; then
|
||||
echo "HTTP Server: build/wizardmerge-cli"
|
||||
echo "Run with: cd build && ./wizardmerge-cli"
|
||||
else
|
||||
echo "Library: build/libwizardmerge.a"
|
||||
echo "HTTP server not built (Drogon not found)"
|
||||
fi
|
||||
if [ -f wizardmerge-tests ]; then
|
||||
echo "Tests: build/wizardmerge-tests"
|
||||
echo "Run with: cd build && ./wizardmerge-tests"
|
||||
fi
|
||||
47
backend/conanfile.py
Normal file
47
backend/conanfile.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Conan package configuration for WizardMerge backend."""
|
||||
from conan import ConanFile
|
||||
from conan.tools.cmake import CMake, cmake_layout
|
||||
|
||||
|
||||
class WizardMergeConan(ConanFile):
|
||||
"""WizardMerge C++ backend package."""
|
||||
|
||||
name = "wizardmerge"
|
||||
version = "0.1.0"
|
||||
|
||||
# Binary configuration
|
||||
settings = "os", "compiler", "build_type", "arch"
|
||||
options = {"shared": [True, False], "fPIC": [True, False]}
|
||||
default_options = {"shared": False, "fPIC": True}
|
||||
|
||||
# Sources
|
||||
exports_sources = "CMakeLists.txt", "src/*", "include/*"
|
||||
|
||||
# Dependencies
|
||||
requires = ["drogon/1.9.3", "libcurl/8.4.0"]
|
||||
|
||||
generators = "CMakeDeps", "CMakeToolchain"
|
||||
|
||||
def config_options(self):
|
||||
"""Configure platform-specific options."""
|
||||
if self.settings.os == "Windows":
|
||||
del self.options.fPIC
|
||||
|
||||
def layout(self):
|
||||
"""Define project layout."""
|
||||
cmake_layout(self)
|
||||
|
||||
def build(self):
|
||||
"""Build the project using CMake."""
|
||||
cmake = CMake(self)
|
||||
cmake.configure()
|
||||
cmake.build()
|
||||
|
||||
def package(self):
|
||||
"""Package the built artifacts."""
|
||||
cmake = CMake(self)
|
||||
cmake.install()
|
||||
|
||||
def package_info(self):
|
||||
"""Define package information for consumers."""
|
||||
self.cpp_info.libs = ["wizardmerge"]
|
||||
43
backend/config.json
Normal file
43
backend/config.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"app": {
|
||||
"threads_num": 4,
|
||||
"enable_session": false,
|
||||
"session_timeout": 0,
|
||||
"document_root": "",
|
||||
"upload_path": "./uploads",
|
||||
"file_types": [
|
||||
"json"
|
||||
],
|
||||
"max_connections": 100000,
|
||||
"max_connections_per_ip": 0,
|
||||
"load_libs": [],
|
||||
"log": {
|
||||
"log_path": "./logs",
|
||||
"logfile_base_name": "wizardmerge",
|
||||
"log_size_limit": 100000000,
|
||||
"log_level": "INFO"
|
||||
},
|
||||
"run_as_daemon": false,
|
||||
"relaunch_on_error": false,
|
||||
"use_sendfile": true,
|
||||
"use_gzip": true,
|
||||
"use_brotli": false,
|
||||
"static_files_cache_time": 0,
|
||||
"simple_controllers_map": [],
|
||||
"idle_connection_timeout": 60,
|
||||
"server_header_field": "WizardMerge/0.1.0",
|
||||
"enable_server_header": true,
|
||||
"keepalive_requests": 0,
|
||||
"pipelining_requests": 0,
|
||||
"client_max_body_size": "10M",
|
||||
"client_max_memory_body_size": "10M",
|
||||
"client_max_websocket_message_size": "128K"
|
||||
},
|
||||
"listeners": [
|
||||
{
|
||||
"address": "0.0.0.0",
|
||||
"port": 8080,
|
||||
"https": false
|
||||
}
|
||||
]
|
||||
}
|
||||
14
backend/docker-compose.yml
Normal file
14
backend/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
wizardmerge-backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./logs:/app/build/logs
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- LOG_LEVEL=INFO
|
||||
221
backend/examples/README.md
Normal file
221
backend/examples/README.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# WizardMerge API Examples
|
||||
|
||||
This directory contains example clients for the WizardMerge HTTP API.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure the WizardMerge HTTP server is running:
|
||||
|
||||
```sh
|
||||
cd ..
|
||||
./build.sh
|
||||
cd build
|
||||
./wizardmerge-cli
|
||||
```
|
||||
|
||||
The server should be running on http://localhost:8080
|
||||
|
||||
## Examples
|
||||
|
||||
### Python Client (`api_client.py`)
|
||||
|
||||
Demonstrates API usage with the requests library.
|
||||
|
||||
**Requirements:**
|
||||
```sh
|
||||
pip install requests
|
||||
```
|
||||
|
||||
**Run:**
|
||||
```sh
|
||||
./api_client.py
|
||||
```
|
||||
|
||||
### Curl Examples (`test_api.sh`)
|
||||
|
||||
Shell script with curl commands for testing the API.
|
||||
|
||||
**Requirements:**
|
||||
- curl
|
||||
- jq (optional, for pretty JSON output)
|
||||
|
||||
**Run:**
|
||||
```sh
|
||||
./test_api.sh
|
||||
```
|
||||
|
||||
## API Endpoint
|
||||
|
||||
### POST /api/merge
|
||||
|
||||
Performs a three-way merge operation.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"base": ["line1", "line2", "line3"],
|
||||
"ours": ["line1", "line2_modified", "line3"],
|
||||
"theirs": ["line1", "line2", "line3_modified"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Success):**
|
||||
```json
|
||||
{
|
||||
"merged": ["line1", "line2_modified", "line3_modified"],
|
||||
"conflicts": [],
|
||||
"has_conflicts": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response (With Conflicts):**
|
||||
```json
|
||||
{
|
||||
"merged": [
|
||||
"line1",
|
||||
"<<<<<<< OURS",
|
||||
"line2_ours",
|
||||
"=======",
|
||||
"line2_theirs",
|
||||
">>>>>>> THEIRS",
|
||||
"line3"
|
||||
],
|
||||
"conflicts": [
|
||||
{
|
||||
"start_line": 1,
|
||||
"end_line": 1,
|
||||
"base_lines": ["line2"],
|
||||
"our_lines": ["line2_ours"],
|
||||
"their_lines": ["line2_theirs"]
|
||||
}
|
||||
],
|
||||
"has_conflicts": true
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response (400 Bad Request):**
|
||||
```json
|
||||
{
|
||||
"error": "Missing required fields: base, ours, theirs"
|
||||
}
|
||||
```
|
||||
|
||||
## Manual Testing with curl
|
||||
|
||||
Basic example:
|
||||
```sh
|
||||
curl -X POST http://localhost:8080/api/merge \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"base": ["hello", "world"],
|
||||
"ours": ["hello", "beautiful world"],
|
||||
"theirs": ["goodbye", "world"]
|
||||
}'
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### JavaScript/Node.js
|
||||
|
||||
```javascript
|
||||
const axios = require('axios');
|
||||
|
||||
async function merge(base, ours, theirs) {
|
||||
const response = await axios.post('http://localhost:8080/api/merge', {
|
||||
base, ours, theirs
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Usage
|
||||
merge(
|
||||
['line1', 'line2'],
|
||||
['line1', 'line2_modified'],
|
||||
['line1', 'line2']
|
||||
).then(result => {
|
||||
console.log('Merged:', result.merged);
|
||||
console.log('Has conflicts:', result.has_conflicts);
|
||||
});
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
def merge(base, ours, theirs, server_url="http://localhost:8080"):
|
||||
response = requests.post(
|
||||
f"{server_url}/api/merge",
|
||||
json={"base": base, "ours": ours, "theirs": theirs}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
# Usage
|
||||
result = merge(
|
||||
base=["line1", "line2"],
|
||||
ours=["line1", "line2_modified"],
|
||||
theirs=["line1", "line2"]
|
||||
)
|
||||
print(f"Merged: {result['merged']}")
|
||||
print(f"Has conflicts: {result['has_conflicts']}")
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type MergeRequest struct {
|
||||
Base []string `json:"base"`
|
||||
Ours []string `json:"ours"`
|
||||
Theirs []string `json:"theirs"`
|
||||
}
|
||||
|
||||
type MergeResponse struct {
|
||||
Merged []string `json:"merged"`
|
||||
Conflicts []Conflict `json:"conflicts"`
|
||||
HasConflicts bool `json:"has_conflicts"`
|
||||
}
|
||||
|
||||
func merge(base, ours, theirs []string) (*MergeResponse, error) {
|
||||
req := MergeRequest{Base: base, Ours: ours, Theirs: theirs}
|
||||
jsonData, _ := json.Marshal(req)
|
||||
|
||||
resp, err := http.Post(
|
||||
"http://localhost:8080/api/merge",
|
||||
"application/json",
|
||||
bytes.NewBuffer(jsonData),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result MergeResponse
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
return &result, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Docker Usage
|
||||
|
||||
If running the server in Docker:
|
||||
|
||||
```sh
|
||||
# Start server
|
||||
docker-compose up -d
|
||||
|
||||
# Test API (from host)
|
||||
curl -X POST http://localhost:8080/api/merge \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"base":["a"],"ours":["b"],"theirs":["c"]}'
|
||||
|
||||
# Check logs
|
||||
docker-compose logs -f
|
||||
```
|
||||
98
backend/examples/api_client.py
Executable file
98
backend/examples/api_client.py
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example client for WizardMerge HTTP API
|
||||
Demonstrates how to use the POST /api/merge endpoint
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
|
||||
def test_merge(base_lines, ours_lines, theirs_lines, server_url="http://localhost:8080"):
|
||||
"""
|
||||
Test the merge API with given inputs
|
||||
"""
|
||||
endpoint = f"{server_url}/api/merge"
|
||||
|
||||
payload = {
|
||||
"base": base_lines,
|
||||
"ours": ours_lines,
|
||||
"theirs": theirs_lines
|
||||
}
|
||||
|
||||
print(f"Sending merge request to {endpoint}")
|
||||
print(f"Base: {base_lines}")
|
||||
print(f"Ours: {ours_lines}")
|
||||
print(f"Theirs: {theirs_lines}")
|
||||
print()
|
||||
|
||||
try:
|
||||
response = requests.post(endpoint, json=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
|
||||
print("=== Merge Result ===")
|
||||
print(f"Merged lines: {result['merged']}")
|
||||
print(f"Has conflicts: {result['has_conflicts']}")
|
||||
|
||||
if result['conflicts']:
|
||||
print(f"Number of conflicts: {len(result['conflicts'])}")
|
||||
for i, conflict in enumerate(result['conflicts']):
|
||||
print(f"\nConflict {i+1}:")
|
||||
print(f" Lines: {conflict['start_line']}-{conflict['end_line']}")
|
||||
print(f" Base: {conflict['base_lines']}")
|
||||
print(f" Ours: {conflict['our_lines']}")
|
||||
print(f" Theirs: {conflict['their_lines']}")
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"ERROR: Could not connect to server at {server_url}")
|
||||
print("Make sure the server is running with: ./wizardmerge-cli")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"ERROR: HTTP {e.response.status_code}")
|
||||
print(e.response.text)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
print("WizardMerge API Client - Example Usage")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
# Test 1: No conflicts
|
||||
print("Test 1: No conflicts (non-overlapping changes)")
|
||||
print("-" * 50)
|
||||
test_merge(
|
||||
base_lines=["line1", "line2", "line3"],
|
||||
ours_lines=["line1", "line2_modified", "line3"],
|
||||
theirs_lines=["line1", "line2", "line3_modified"]
|
||||
)
|
||||
print()
|
||||
|
||||
# Test 2: With conflicts
|
||||
print("\nTest 2: With conflicts (overlapping changes)")
|
||||
print("-" * 50)
|
||||
test_merge(
|
||||
base_lines=["line1", "line2", "line3"],
|
||||
ours_lines=["line1", "line2_ours", "line3"],
|
||||
theirs_lines=["line1", "line2_theirs", "line3"]
|
||||
)
|
||||
print()
|
||||
|
||||
# Test 3: Identical changes
|
||||
print("\nTest 3: Identical changes (auto-resolved)")
|
||||
print("-" * 50)
|
||||
test_merge(
|
||||
base_lines=["line1", "line2", "line3"],
|
||||
ours_lines=["line1", "line2_same", "line3"],
|
||||
theirs_lines=["line1", "line2_same", "line3"]
|
||||
)
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
193
backend/examples/pr_resolve_example.py
Executable file
193
backend/examples/pr_resolve_example.py
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Resolve GitHub Pull Request conflicts using WizardMerge API
|
||||
|
||||
This script demonstrates how to use the WizardMerge API to automatically
|
||||
resolve merge conflicts in a GitHub pull request.
|
||||
|
||||
Usage:
|
||||
python pr_resolve_example.py https://github.com/owner/repo/pull/123
|
||||
|
||||
Environment Variables:
|
||||
GITHUB_TOKEN: Optional GitHub API token for private repos
|
||||
WIZARDMERGE_BACKEND: Backend URL (default: http://localhost:8080)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def resolve_pr(
|
||||
pr_url: str,
|
||||
backend_url: str = "http://localhost:8080",
|
||||
github_token: Optional[str] = None,
|
||||
create_branch: bool = False,
|
||||
branch_name: Optional[str] = None
|
||||
) -> dict:
|
||||
"""
|
||||
Resolve conflicts in a GitHub pull request.
|
||||
|
||||
Args:
|
||||
pr_url: URL of the pull request (e.g., https://github.com/owner/repo/pull/123)
|
||||
backend_url: URL of WizardMerge backend server
|
||||
github_token: Optional GitHub API token
|
||||
create_branch: Whether to create a new branch with resolved conflicts
|
||||
branch_name: Name of the branch to create (optional)
|
||||
|
||||
Returns:
|
||||
dict: API response with resolution results
|
||||
"""
|
||||
endpoint = f"{backend_url}/api/pr/resolve"
|
||||
|
||||
payload = {
|
||||
"pr_url": pr_url,
|
||||
}
|
||||
|
||||
if github_token:
|
||||
payload["github_token"] = github_token
|
||||
|
||||
if create_branch:
|
||||
payload["create_branch"] = True
|
||||
if branch_name:
|
||||
payload["branch_name"] = branch_name
|
||||
|
||||
print(f"Resolving PR: {pr_url}")
|
||||
print(f"Backend: {endpoint}")
|
||||
print()
|
||||
|
||||
try:
|
||||
response = requests.post(endpoint, json=payload, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
return result
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"ERROR: Could not connect to backend at {backend_url}")
|
||||
print("Make sure the backend server is running:")
|
||||
print(" cd backend && ./wizardmerge-cli")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"ERROR: HTTP {e.response.status_code}")
|
||||
print(e.response.text)
|
||||
sys.exit(1)
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"ERROR: Request timed out after 60 seconds")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def print_results(result: dict):
|
||||
"""Pretty print the resolution results."""
|
||||
print("=" * 70)
|
||||
print("PULL REQUEST RESOLUTION RESULTS")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
if not result.get("success"):
|
||||
print("❌ Resolution failed")
|
||||
if "error" in result:
|
||||
print(f"Error: {result['error']}")
|
||||
return
|
||||
|
||||
# PR Info
|
||||
pr_info = result.get("pr_info", {})
|
||||
print(f"📋 PR #{pr_info.get('number')}: {pr_info.get('title')}")
|
||||
print(f" Base: {pr_info.get('base_ref')} ({pr_info.get('base_sha', '')[:7]})")
|
||||
print(f" Head: {pr_info.get('head_ref')} ({pr_info.get('head_sha', '')[:7]})")
|
||||
print(f" Mergeable: {pr_info.get('mergeable')}")
|
||||
print()
|
||||
|
||||
# Statistics
|
||||
total = result.get("total_files", 0)
|
||||
resolved = result.get("resolved_count", 0)
|
||||
failed = result.get("failed_count", 0)
|
||||
|
||||
print(f"📊 Statistics:")
|
||||
print(f" Total files: {total}")
|
||||
print(f" ✅ Resolved: {resolved}")
|
||||
print(f" ❌ Failed: {failed}")
|
||||
print(f" Success rate: {(resolved/total*100) if total > 0 else 0:.1f}%")
|
||||
print()
|
||||
|
||||
# File details
|
||||
print("📁 File Resolution Details:")
|
||||
print()
|
||||
|
||||
resolved_files = result.get("resolved_files", [])
|
||||
for file_info in resolved_files:
|
||||
filename = file_info.get("filename", "unknown")
|
||||
status = file_info.get("status", "unknown")
|
||||
|
||||
if file_info.get("skipped"):
|
||||
print(f" ⊘ {filename} (skipped: {file_info.get('reason', 'N/A')})")
|
||||
continue
|
||||
|
||||
if file_info.get("error"):
|
||||
print(f" ❌ {filename} - Error: {file_info.get('error')}")
|
||||
continue
|
||||
|
||||
had_conflicts = file_info.get("had_conflicts", False)
|
||||
auto_resolved = file_info.get("auto_resolved", False)
|
||||
|
||||
if auto_resolved:
|
||||
icon = "✅"
|
||||
msg = "auto-resolved"
|
||||
elif had_conflicts:
|
||||
icon = "⚠️"
|
||||
msg = "has unresolved conflicts"
|
||||
else:
|
||||
icon = "✓"
|
||||
msg = "no conflicts"
|
||||
|
||||
print(f" {icon} {filename} - {msg}")
|
||||
|
||||
print()
|
||||
|
||||
# Branch creation
|
||||
if result.get("branch_created"):
|
||||
branch = result.get("branch_name", "N/A")
|
||||
print(f"🌿 Created branch: {branch}")
|
||||
elif "branch_name" in result:
|
||||
print(f"📝 Note: {result.get('note', 'Branch creation pending')}")
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 2 or sys.argv[1] in ["-h", "--help"]:
|
||||
print(__doc__)
|
||||
sys.exit(0)
|
||||
|
||||
pr_url = sys.argv[1]
|
||||
|
||||
# Get configuration from environment
|
||||
backend_url = os.getenv("WIZARDMERGE_BACKEND", "http://localhost:8080")
|
||||
github_token = os.getenv("GITHUB_TOKEN")
|
||||
|
||||
# Resolve the PR
|
||||
result = resolve_pr(
|
||||
pr_url=pr_url,
|
||||
backend_url=backend_url,
|
||||
github_token=github_token
|
||||
)
|
||||
|
||||
# Print results
|
||||
print_results(result)
|
||||
|
||||
# Exit with appropriate code
|
||||
if result.get("success") and result.get("resolved_count", 0) > 0:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
70
backend/examples/test_api.sh
Executable file
70
backend/examples/test_api.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
# Example API calls using curl
|
||||
|
||||
SERVER_URL="http://localhost:8080"
|
||||
|
||||
echo "WizardMerge API - Example curl Commands"
|
||||
echo "========================================"
|
||||
echo
|
||||
|
||||
# Test 1: No conflicts
|
||||
echo "Test 1: No conflicts (non-overlapping changes)"
|
||||
echo "-----------------------------------------------"
|
||||
curl -X POST "${SERVER_URL}/api/merge" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"base": ["line1", "line2", "line3"],
|
||||
"ours": ["line1", "line2_modified", "line3"],
|
||||
"theirs": ["line1", "line2", "line3_modified"]
|
||||
}' | jq '.'
|
||||
echo
|
||||
echo
|
||||
|
||||
# Test 2: With conflicts
|
||||
echo "Test 2: With conflicts (overlapping changes)"
|
||||
echo "---------------------------------------------"
|
||||
curl -X POST "${SERVER_URL}/api/merge" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"base": ["line1", "line2", "line3"],
|
||||
"ours": ["line1", "line2_ours", "line3"],
|
||||
"theirs": ["line1", "line2_theirs", "line3"]
|
||||
}' | jq '.'
|
||||
echo
|
||||
echo
|
||||
|
||||
# Test 3: Identical changes
|
||||
echo "Test 3: Identical changes (auto-resolved)"
|
||||
echo "------------------------------------------"
|
||||
curl -X POST "${SERVER_URL}/api/merge" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"base": ["line1", "line2", "line3"],
|
||||
"ours": ["line1", "line2_same", "line3"],
|
||||
"theirs": ["line1", "line2_same", "line3"]
|
||||
}' | jq '.'
|
||||
echo
|
||||
echo
|
||||
|
||||
# Test 4: Error handling - Missing field
|
||||
echo "Test 4: Error handling - Missing required field"
|
||||
echo "------------------------------------------------"
|
||||
curl -X POST "${SERVER_URL}/api/merge" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"base": ["line1", "line2"],
|
||||
"ours": ["line1", "line2_modified"]
|
||||
}' | jq '.'
|
||||
echo
|
||||
echo
|
||||
|
||||
# Test 5: Error handling - Invalid JSON
|
||||
echo "Test 5: Error handling - Invalid JSON"
|
||||
echo "--------------------------------------"
|
||||
curl -X POST "${SERVER_URL}/api/merge" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d 'not json'
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "Done!"
|
||||
118
backend/include/wizardmerge/git/git_platform_client.h
Normal file
118
backend/include/wizardmerge/git/git_platform_client.h
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @file git_platform_client.h
|
||||
* @brief Git platform API client for fetching pull/merge request information
|
||||
*
|
||||
* Supports GitHub and GitLab platforms
|
||||
*/
|
||||
|
||||
#ifndef WIZARDMERGE_GIT_PLATFORM_CLIENT_H
|
||||
#define WIZARDMERGE_GIT_PLATFORM_CLIENT_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace git {
|
||||
|
||||
/**
|
||||
* @brief Supported git platforms
|
||||
*/
|
||||
enum class GitPlatform {
|
||||
GitHub,
|
||||
GitLab,
|
||||
Unknown
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Information about a file in a pull/merge request
|
||||
*/
|
||||
struct PRFile {
|
||||
std::string filename;
|
||||
std::string status; // "added", "modified", "removed", "renamed"
|
||||
int additions;
|
||||
int deletions;
|
||||
int changes;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Pull/merge request information from GitHub or GitLab
|
||||
*/
|
||||
struct PullRequest {
|
||||
GitPlatform platform;
|
||||
int number;
|
||||
std::string title;
|
||||
std::string state;
|
||||
std::string base_ref; // Base branch name
|
||||
std::string head_ref; // Head branch name
|
||||
std::string base_sha;
|
||||
std::string head_sha;
|
||||
std::string repo_owner;
|
||||
std::string repo_name;
|
||||
std::vector<PRFile> files;
|
||||
bool mergeable;
|
||||
std::string mergeable_state;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Parse pull/merge request URL
|
||||
*
|
||||
* Extracts platform, owner, repo, and PR/MR number from URLs like:
|
||||
* - https://github.com/owner/repo/pull/123
|
||||
* - https://gitlab.com/owner/repo/-/merge_requests/456
|
||||
* - github.com/owner/repo/pull/123
|
||||
* - gitlab.com/group/subgroup/project/-/merge_requests/789
|
||||
*
|
||||
* @param url The pull/merge request URL
|
||||
* @param platform Output git platform
|
||||
* @param owner Output repository owner/group
|
||||
* @param repo Output repository name/project
|
||||
* @param pr_number Output PR/MR number
|
||||
* @return true if successfully parsed, false otherwise
|
||||
*/
|
||||
bool parse_pr_url(const std::string& url, GitPlatform& platform,
|
||||
std::string& owner, std::string& repo, int& pr_number);
|
||||
|
||||
/**
|
||||
* @brief Fetch pull/merge request information from GitHub or GitLab API
|
||||
*
|
||||
* @param platform Git platform (GitHub or GitLab)
|
||||
* @param owner Repository owner/group
|
||||
* @param repo Repository name/project
|
||||
* @param pr_number Pull/merge request number
|
||||
* @param token Optional API token for authentication
|
||||
* @return Pull request information, or empty optional on error
|
||||
*/
|
||||
std::optional<PullRequest> fetch_pull_request(
|
||||
GitPlatform platform,
|
||||
const std::string& owner,
|
||||
const std::string& repo,
|
||||
int pr_number,
|
||||
const std::string& token = ""
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Fetch file content from GitHub or GitLab at a specific commit
|
||||
*
|
||||
* @param platform Git platform (GitHub or GitLab)
|
||||
* @param owner Repository owner/group
|
||||
* @param repo Repository name/project
|
||||
* @param sha Commit SHA
|
||||
* @param path File path
|
||||
* @param token Optional API token
|
||||
* @return File content as vector of lines, or empty optional on error
|
||||
*/
|
||||
std::optional<std::vector<std::string>> fetch_file_content(
|
||||
GitPlatform platform,
|
||||
const std::string& owner,
|
||||
const std::string& repo,
|
||||
const std::string& sha,
|
||||
const std::string& path,
|
||||
const std::string& token = ""
|
||||
);
|
||||
|
||||
} // namespace git
|
||||
} // namespace wizardmerge
|
||||
|
||||
#endif // WIZARDMERGE_GIT_PLATFORM_CLIENT_H
|
||||
82
backend/include/wizardmerge/merge/three_way_merge.h
Normal file
82
backend/include/wizardmerge/merge/three_way_merge.h
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @file three_way_merge.h
|
||||
* @brief Three-way merge algorithm for WizardMerge
|
||||
*
|
||||
* Implements the core three-way merge algorithm based on the paper from
|
||||
* The University of Hong Kong. This algorithm uses dependency analysis
|
||||
* at both text and LLVM-IR levels to provide intelligent merge suggestions.
|
||||
*/
|
||||
|
||||
#ifndef WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
|
||||
#define WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace merge {
|
||||
|
||||
/**
|
||||
* @brief Represents a single line in a file with its origin.
|
||||
*/
|
||||
struct Line {
|
||||
std::string content;
|
||||
enum Origin { BASE, OURS, THEIRS, MERGED } origin;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a conflict region in the merge result.
|
||||
*/
|
||||
struct Conflict {
|
||||
size_t start_line;
|
||||
size_t end_line;
|
||||
std::vector<Line> base_lines;
|
||||
std::vector<Line> our_lines;
|
||||
std::vector<Line> their_lines;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Result of a three-way merge operation.
|
||||
*/
|
||||
struct MergeResult {
|
||||
std::vector<Line> merged_lines;
|
||||
std::vector<Conflict> conflicts;
|
||||
bool has_conflicts() const { return !conflicts.empty(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Performs a three-way merge on three versions of content.
|
||||
*
|
||||
* This function implements the three-way merge algorithm that compares
|
||||
* the base version with two variants (ours and theirs) to produce a
|
||||
* merged result with conflict markers where automatic resolution is
|
||||
* not possible.
|
||||
*
|
||||
* @param base The common ancestor version
|
||||
* @param ours Our version (current branch)
|
||||
* @param theirs Their version (branch being merged)
|
||||
* @return MergeResult containing the merged content and any conflicts
|
||||
*/
|
||||
MergeResult three_way_merge(
|
||||
const std::vector<std::string>& base,
|
||||
const std::vector<std::string>& ours,
|
||||
const std::vector<std::string>& theirs
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Auto-resolves simple non-conflicting patterns.
|
||||
*
|
||||
* Handles common auto-resolvable patterns:
|
||||
* - Non-overlapping changes
|
||||
* - Identical changes from both sides
|
||||
* - Whitespace-only differences
|
||||
*
|
||||
* @param result The merge result to auto-resolve
|
||||
* @return Updated merge result with resolved conflicts
|
||||
*/
|
||||
MergeResult auto_resolve(const MergeResult& result);
|
||||
|
||||
} // namespace merge
|
||||
} // namespace wizardmerge
|
||||
|
||||
#endif // WIZARDMERGE_MERGE_THREE_WAY_MERGE_H
|
||||
51
backend/install_drogon.sh
Executable file
51
backend/install_drogon.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# Script to install Drogon framework from source
|
||||
# Run this script before building WizardMerge if Drogon is not installed
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Installing Drogon Framework from Source ==="
|
||||
echo
|
||||
|
||||
# Check for required tools
|
||||
command -v git >/dev/null 2>&1 || { echo "Error: git not found."; exit 1; }
|
||||
command -v cmake >/dev/null 2>&1 || { echo "Error: cmake not found."; exit 1; }
|
||||
command -v make >/dev/null 2>&1 || { echo "Error: make not found."; exit 1; }
|
||||
|
||||
# Install system dependencies (Ubuntu/Debian)
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
echo "Installing system dependencies..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libjsoncpp-dev \
|
||||
uuid-dev \
|
||||
zlib1g-dev \
|
||||
libssl-dev
|
||||
fi
|
||||
|
||||
# Clone Drogon
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
cd "$TEMP_DIR"
|
||||
|
||||
echo "Cloning Drogon from GitHub..."
|
||||
git clone https://github.com/drogonframework/drogon.git
|
||||
cd drogon
|
||||
git submodule update --init
|
||||
|
||||
# Build and install
|
||||
echo "Building Drogon..."
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
echo "Installing Drogon..."
|
||||
sudo make install
|
||||
|
||||
# Cleanup
|
||||
cd /
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo
|
||||
echo "=== Drogon Installation Complete ==="
|
||||
echo "You can now build WizardMerge with: ./build.sh"
|
||||
113
backend/src/controllers/MergeController.cc
Normal file
113
backend/src/controllers/MergeController.cc
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @file MergeController.cc
|
||||
* @brief Implementation of HTTP controller for merge operations
|
||||
*/
|
||||
|
||||
#include "MergeController.h"
|
||||
#include "wizardmerge/merge/three_way_merge.h"
|
||||
#include <json/json.h>
|
||||
|
||||
using namespace wizardmerge::controllers;
|
||||
using namespace wizardmerge::merge;
|
||||
|
||||
void MergeController::merge(
|
||||
const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
|
||||
// Parse request JSON
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON in request body";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &json = *jsonPtr;
|
||||
|
||||
// Validate required fields
|
||||
if (!json.isMember("base") || !json.isMember("ours") || !json.isMember("theirs")) {
|
||||
Json::Value error;
|
||||
error["error"] = "Missing required fields: base, ours, theirs";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert JSON arrays to std::vector<std::string>
|
||||
std::vector<std::string> base;
|
||||
std::vector<std::string> ours;
|
||||
std::vector<std::string> theirs;
|
||||
|
||||
try {
|
||||
for (const auto &line : json["base"]) {
|
||||
base.push_back(line.asString());
|
||||
}
|
||||
for (const auto &line : json["ours"]) {
|
||||
ours.push_back(line.asString());
|
||||
}
|
||||
for (const auto &line : json["theirs"]) {
|
||||
theirs.push_back(line.asString());
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid array format in request";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform merge
|
||||
auto result = three_way_merge(base, ours, theirs);
|
||||
|
||||
// Auto-resolve simple conflicts
|
||||
result = auto_resolve(result);
|
||||
|
||||
// Build response JSON
|
||||
Json::Value response;
|
||||
Json::Value mergedArray(Json::arrayValue);
|
||||
for (const auto &line : result.merged_lines) {
|
||||
mergedArray.append(line.content);
|
||||
}
|
||||
response["merged"] = mergedArray;
|
||||
|
||||
// Add conflicts information
|
||||
Json::Value conflictsArray(Json::arrayValue);
|
||||
for (const auto &conflict : result.conflicts) {
|
||||
Json::Value conflictObj;
|
||||
// Note: start_line and end_line are size_t (always non-negative)
|
||||
conflictObj["start_line"] = static_cast<Json::UInt64>(conflict.start_line);
|
||||
conflictObj["end_line"] = static_cast<Json::UInt64>(conflict.end_line);
|
||||
|
||||
Json::Value baseLines(Json::arrayValue);
|
||||
for (const auto &line : conflict.base_lines) {
|
||||
baseLines.append(line.content);
|
||||
}
|
||||
conflictObj["base_lines"] = baseLines;
|
||||
|
||||
Json::Value ourLines(Json::arrayValue);
|
||||
for (const auto &line : conflict.our_lines) {
|
||||
ourLines.append(line.content);
|
||||
}
|
||||
conflictObj["our_lines"] = ourLines;
|
||||
|
||||
Json::Value theirLines(Json::arrayValue);
|
||||
for (const auto &line : conflict.their_lines) {
|
||||
theirLines.append(line.content);
|
||||
}
|
||||
conflictObj["their_lines"] = theirLines;
|
||||
|
||||
conflictsArray.append(conflictObj);
|
||||
}
|
||||
response["conflicts"] = conflictsArray;
|
||||
response["has_conflicts"] = result.has_conflicts();
|
||||
|
||||
// Return successful response
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
resp->setStatusCode(k200OK);
|
||||
callback(resp);
|
||||
}
|
||||
49
backend/src/controllers/MergeController.h
Normal file
49
backend/src/controllers/MergeController.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @file MergeController.h
|
||||
* @brief HTTP controller for merge operations
|
||||
*/
|
||||
|
||||
#ifndef WIZARDMERGE_CONTROLLERS_MERGE_CONTROLLER_H
|
||||
#define WIZARDMERGE_CONTROLLERS_MERGE_CONTROLLER_H
|
||||
|
||||
#include <drogon/HttpController.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace controllers {
|
||||
|
||||
/**
|
||||
* @brief HTTP controller for three-way merge API
|
||||
*/
|
||||
class MergeController : public HttpController<MergeController> {
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
// POST /api/merge - Perform three-way merge
|
||||
ADD_METHOD_TO(MergeController::merge, "/api/merge", Post);
|
||||
METHOD_LIST_END
|
||||
|
||||
/**
|
||||
* @brief Perform three-way merge operation
|
||||
*
|
||||
* Request body should be JSON:
|
||||
* {
|
||||
* "base": ["line1", "line2", ...],
|
||||
* "ours": ["line1", "line2", ...],
|
||||
* "theirs": ["line1", "line2", ...]
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* "merged": ["line1", "line2", ...],
|
||||
* "conflicts": [...]
|
||||
* }
|
||||
*/
|
||||
void merge(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
};
|
||||
|
||||
} // namespace controllers
|
||||
} // namespace wizardmerge
|
||||
|
||||
#endif // WIZARDMERGE_CONTROLLERS_MERGE_CONTROLLER_H
|
||||
197
backend/src/controllers/PRController.cc
Normal file
197
backend/src/controllers/PRController.cc
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @file PRController.cc
|
||||
* @brief Implementation of HTTP controller for pull request operations
|
||||
*/
|
||||
|
||||
#include "PRController.h"
|
||||
#include "wizardmerge/git/git_platform_client.h"
|
||||
#include "wizardmerge/merge/three_way_merge.h"
|
||||
#include <json/json.h>
|
||||
#include <iostream>
|
||||
|
||||
using namespace wizardmerge::controllers;
|
||||
using namespace wizardmerge::git;
|
||||
using namespace wizardmerge::merge;
|
||||
|
||||
void PRController::resolvePR(
|
||||
const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
|
||||
// Parse request JSON
|
||||
auto jsonPtr = req->getJsonObject();
|
||||
if (!jsonPtr) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid JSON in request body";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &json = *jsonPtr;
|
||||
|
||||
// Validate required fields
|
||||
if (!json.isMember("pr_url")) {
|
||||
Json::Value error;
|
||||
error["error"] = "Missing required field: pr_url";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string pr_url = json["pr_url"].asString();
|
||||
std::string api_token = json.get("api_token", json.get("github_token", "").asString()).asString();
|
||||
bool create_branch = json.get("create_branch", false).asBool();
|
||||
std::string branch_name = json.get("branch_name", "").asString();
|
||||
|
||||
// Parse PR/MR URL
|
||||
GitPlatform platform;
|
||||
std::string owner, repo;
|
||||
int pr_number;
|
||||
|
||||
if (!parse_pr_url(pr_url, platform, owner, repo, pr_number)) {
|
||||
Json::Value error;
|
||||
error["error"] = "Invalid pull/merge request URL format";
|
||||
error["pr_url"] = pr_url;
|
||||
error["note"] = "Supported platforms: GitHub (pull requests) and GitLab (merge requests)";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch pull/merge request information
|
||||
auto pr_opt = fetch_pull_request(platform, owner, repo, pr_number, api_token);
|
||||
|
||||
if (!pr_opt) {
|
||||
Json::Value error;
|
||||
error["error"] = "Failed to fetch pull/merge request information";
|
||||
error["platform"] = (platform == GitPlatform::GitHub) ? "GitHub" : "GitLab";
|
||||
error["owner"] = owner;
|
||||
error["repo"] = repo;
|
||||
error["pr_number"] = pr_number;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(error);
|
||||
resp->setStatusCode(k502BadGateway);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
PullRequest pr = pr_opt.value();
|
||||
|
||||
// Process each file in the PR
|
||||
Json::Value resolved_files_array(Json::arrayValue);
|
||||
int total_files = 0;
|
||||
int resolved_files = 0;
|
||||
int failed_files = 0;
|
||||
|
||||
for (const auto& file : pr.files) {
|
||||
total_files++;
|
||||
|
||||
Json::Value file_result;
|
||||
file_result["filename"] = file.filename;
|
||||
file_result["status"] = file.status;
|
||||
|
||||
// Skip deleted files
|
||||
if (file.status == "removed") {
|
||||
file_result["skipped"] = true;
|
||||
file_result["reason"] = "File was deleted";
|
||||
resolved_files_array.append(file_result);
|
||||
continue;
|
||||
}
|
||||
|
||||
// For modified files, fetch base and head versions
|
||||
if (file.status == "modified" || file.status == "added") {
|
||||
// Fetch base version (empty for added files)
|
||||
std::vector<std::string> base_content;
|
||||
if (file.status == "modified") {
|
||||
auto base_opt = fetch_file_content(platform, owner, repo, pr.base_sha, file.filename, api_token);
|
||||
if (!base_opt) {
|
||||
file_result["error"] = "Failed to fetch base version";
|
||||
file_result["had_conflicts"] = false;
|
||||
failed_files++;
|
||||
resolved_files_array.append(file_result);
|
||||
continue;
|
||||
}
|
||||
base_content = base_opt.value();
|
||||
}
|
||||
|
||||
// Fetch head version
|
||||
auto head_opt = fetch_file_content(platform, owner, repo, pr.head_sha, file.filename, api_token);
|
||||
if (!head_opt) {
|
||||
file_result["error"] = "Failed to fetch head version";
|
||||
file_result["had_conflicts"] = false;
|
||||
failed_files++;
|
||||
resolved_files_array.append(file_result);
|
||||
continue;
|
||||
}
|
||||
std::vector<std::string> head_content = head_opt.value();
|
||||
|
||||
// For added files or when there might be a conflict with existing file
|
||||
// Note: This is a simplified merge for PR review purposes.
|
||||
// In a real merge scenario with conflicts, you'd need the merge-base commit.
|
||||
// Here we're showing what changes if we accept the head version:
|
||||
// - base: common ancestor (PR base)
|
||||
// - ours: current state (PR base)
|
||||
// - theirs: proposed changes (PR head)
|
||||
// This effectively shows all changes from the PR head.
|
||||
|
||||
// Perform three-way merge: base, ours (base), theirs (head)
|
||||
auto merge_result = three_way_merge(base_content, base_content, head_content);
|
||||
merge_result = auto_resolve(merge_result);
|
||||
|
||||
file_result["had_conflicts"] = merge_result.has_conflicts();
|
||||
file_result["auto_resolved"] = !merge_result.has_conflicts();
|
||||
|
||||
// Extract merged content
|
||||
Json::Value merged_content(Json::arrayValue);
|
||||
for (const auto& line : merge_result.merged_lines) {
|
||||
merged_content.append(line.content);
|
||||
}
|
||||
file_result["merged_content"] = merged_content;
|
||||
|
||||
if (!merge_result.has_conflicts()) {
|
||||
resolved_files++;
|
||||
}
|
||||
}
|
||||
|
||||
resolved_files_array.append(file_result);
|
||||
}
|
||||
|
||||
// Build response
|
||||
Json::Value response;
|
||||
response["success"] = true;
|
||||
|
||||
Json::Value pr_info;
|
||||
pr_info["platform"] = (pr.platform == GitPlatform::GitHub) ? "GitHub" : "GitLab";
|
||||
pr_info["number"] = pr.number;
|
||||
pr_info["title"] = pr.title;
|
||||
pr_info["state"] = pr.state;
|
||||
pr_info["base_ref"] = pr.base_ref;
|
||||
pr_info["head_ref"] = pr.head_ref;
|
||||
pr_info["base_sha"] = pr.base_sha;
|
||||
pr_info["head_sha"] = pr.head_sha;
|
||||
pr_info["mergeable"] = pr.mergeable;
|
||||
pr_info["mergeable_state"] = pr.mergeable_state;
|
||||
response["pr_info"] = pr_info;
|
||||
|
||||
response["resolved_files"] = resolved_files_array;
|
||||
response["total_files"] = total_files;
|
||||
response["resolved_count"] = resolved_files;
|
||||
response["failed_count"] = failed_files;
|
||||
|
||||
// Branch creation would require Git CLI access
|
||||
// For now, just report what would be done
|
||||
response["branch_created"] = false;
|
||||
if (create_branch) {
|
||||
if (branch_name.empty()) {
|
||||
branch_name = "wizardmerge-resolved-pr-" + std::to_string(pr_number);
|
||||
}
|
||||
response["branch_name"] = branch_name;
|
||||
response["note"] = "Branch creation requires Git CLI integration (not yet implemented)";
|
||||
}
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
resp->setStatusCode(k200OK);
|
||||
callback(resp);
|
||||
}
|
||||
65
backend/src/controllers/PRController.h
Normal file
65
backend/src/controllers/PRController.h
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @file PRController.h
|
||||
* @brief HTTP controller for pull request merge operations
|
||||
*/
|
||||
|
||||
#ifndef WIZARDMERGE_CONTROLLERS_PR_CONTROLLER_H
|
||||
#define WIZARDMERGE_CONTROLLERS_PR_CONTROLLER_H
|
||||
|
||||
#include <drogon/HttpController.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace controllers {
|
||||
|
||||
/**
|
||||
* @brief HTTP controller for pull request merge API
|
||||
*/
|
||||
class PRController : public HttpController<PRController> {
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
// POST /api/pr/resolve - Resolve conflicts in a pull request
|
||||
ADD_METHOD_TO(PRController::resolvePR, "/api/pr/resolve", Post);
|
||||
METHOD_LIST_END
|
||||
|
||||
/**
|
||||
* @brief Resolve merge conflicts in a pull request
|
||||
*
|
||||
* Request body should be JSON:
|
||||
* {
|
||||
* "pr_url": "https://github.com/owner/repo/pull/123",
|
||||
* "github_token": "optional_github_token",
|
||||
* "create_branch": true,
|
||||
* "branch_name": "wizardmerge-resolved-pr-123"
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* "success": true,
|
||||
* "pr_info": {
|
||||
* "number": 123,
|
||||
* "title": "...",
|
||||
* "base_ref": "main",
|
||||
* "head_ref": "feature-branch"
|
||||
* },
|
||||
* "resolved_files": [
|
||||
* {
|
||||
* "filename": "...",
|
||||
* "had_conflicts": true,
|
||||
* "auto_resolved": true,
|
||||
* "merged_content": ["line1", "line2", ...]
|
||||
* }
|
||||
* ],
|
||||
* "branch_created": true,
|
||||
* "branch_name": "wizardmerge-resolved-pr-123"
|
||||
* }
|
||||
*/
|
||||
void resolvePR(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
};
|
||||
|
||||
} // namespace controllers
|
||||
} // namespace wizardmerge
|
||||
|
||||
#endif // WIZARDMERGE_CONTROLLERS_PR_CONTROLLER_H
|
||||
417
backend/src/git/git_platform_client.cpp
Normal file
417
backend/src/git/git_platform_client.cpp
Normal file
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* @file git_platform_client.cpp
|
||||
* @brief Implementation of git platform API client for GitHub and GitLab
|
||||
*/
|
||||
|
||||
#include "wizardmerge/git/git_platform_client.h"
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <curl/curl.h>
|
||||
#include <json/json.h>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace git {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Simple base64 decoder
|
||||
*/
|
||||
std::string base64_decode(const std::string& encoded) {
|
||||
static const std::string base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
std::string decoded;
|
||||
std::vector<int> T(256, -1);
|
||||
for (int i = 0; i < 64; i++) T[base64_chars[i]] = i;
|
||||
|
||||
int val = 0, valb = -8;
|
||||
for (unsigned char c : encoded) {
|
||||
if (T[c] == -1) break;
|
||||
val = (val << 6) + T[c];
|
||||
valb += 6;
|
||||
if (valb >= 0) {
|
||||
decoded.push_back(char((val >> valb) & 0xFF));
|
||||
valb -= 8;
|
||||
}
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
// Callback for libcurl to write response data
|
||||
size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
((std::string*)userp)->append((char*)contents, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Perform HTTP GET request using libcurl
|
||||
*/
|
||||
bool http_get(const std::string& url, const std::string& token, std::string& response, GitPlatform platform = GitPlatform::GitHub) {
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
std::cerr << "Failed to initialize CURL" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
response.clear();
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "WizardMerge/1.0");
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
||||
|
||||
// Setup headers based on platform
|
||||
struct curl_slist* headers = nullptr;
|
||||
|
||||
if (platform == GitPlatform::GitHub) {
|
||||
headers = curl_slist_append(headers, "Accept: application/vnd.github.v3+json");
|
||||
if (!token.empty()) {
|
||||
std::string auth_header = "Authorization: token " + token;
|
||||
headers = curl_slist_append(headers, auth_header.c_str());
|
||||
}
|
||||
} else if (platform == GitPlatform::GitLab) {
|
||||
headers = curl_slist_append(headers, "Accept: application/json");
|
||||
if (!token.empty()) {
|
||||
std::string auth_header = "PRIVATE-TOKEN: " + token;
|
||||
headers = curl_slist_append(headers, auth_header.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
bool success = (res == CURLE_OK);
|
||||
if (!success) {
|
||||
std::cerr << "CURL error: " << curl_easy_strerror(res) << std::endl;
|
||||
}
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Split string by newlines
|
||||
*/
|
||||
std::vector<std::string> split_lines(const std::string& content) {
|
||||
std::vector<std::string> lines;
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
lines.push_back(line);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
bool parse_pr_url(const std::string& url, GitPlatform& platform,
|
||||
std::string& owner, std::string& repo, int& pr_number) {
|
||||
// Try GitHub pattern first:
|
||||
// https://github.com/owner/repo/pull/123
|
||||
// github.com/owner/repo/pull/123
|
||||
|
||||
std::regex github_regex(R"((?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+))");
|
||||
std::smatch matches;
|
||||
|
||||
if (std::regex_search(url, matches, github_regex)) {
|
||||
if (matches.size() == 4) {
|
||||
platform = GitPlatform::GitHub;
|
||||
owner = matches[1].str();
|
||||
repo = matches[2].str();
|
||||
pr_number = std::stoi(matches[3].str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Try GitLab pattern:
|
||||
// https://gitlab.com/owner/repo/-/merge_requests/456
|
||||
// gitlab.com/group/subgroup/project/-/merge_requests/789
|
||||
|
||||
std::regex gitlab_regex(R"((?:https?://)?(?:www\.)?gitlab\.com/([^/-]+(?:/[^/-]+)*?)/-/merge_requests/(\d+))");
|
||||
|
||||
if (std::regex_search(url, matches, gitlab_regex)) {
|
||||
if (matches.size() == 3) {
|
||||
platform = GitPlatform::GitLab;
|
||||
std::string full_path = matches[1].str();
|
||||
|
||||
// For GitLab, store the full project path
|
||||
// The path can be: owner/repo or group/subgroup/project
|
||||
// We split at the last slash to separate for potential use
|
||||
size_t last_slash = full_path.find_last_of('/');
|
||||
if (last_slash != std::string::npos) {
|
||||
owner = full_path.substr(0, last_slash);
|
||||
repo = full_path.substr(last_slash + 1);
|
||||
} else {
|
||||
// Single level project (rare but possible)
|
||||
// Store entire path as owner, repo empty
|
||||
// API calls will use full path by concatenating
|
||||
owner = full_path;
|
||||
repo = "";
|
||||
}
|
||||
|
||||
pr_number = std::stoi(matches[2].str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
platform = GitPlatform::Unknown;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<PullRequest> fetch_pull_request(
|
||||
GitPlatform platform,
|
||||
const std::string& owner,
|
||||
const std::string& repo,
|
||||
int pr_number,
|
||||
const std::string& token
|
||||
) {
|
||||
PullRequest pr;
|
||||
pr.platform = platform;
|
||||
pr.number = pr_number;
|
||||
pr.repo_owner = owner;
|
||||
pr.repo_name = repo;
|
||||
|
||||
std::string pr_url, files_url;
|
||||
|
||||
if (platform == GitPlatform::GitHub) {
|
||||
// GitHub API URLs
|
||||
pr_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number);
|
||||
files_url = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls/" + std::to_string(pr_number) + "/files";
|
||||
} else if (platform == GitPlatform::GitLab) {
|
||||
// GitLab API URLs - encode project path
|
||||
std::string project_path = owner;
|
||||
if (!repo.empty()) {
|
||||
project_path += "/" + repo;
|
||||
}
|
||||
// URL encode the project path
|
||||
CURL* curl = curl_easy_init();
|
||||
char* encoded = curl_easy_escape(curl, project_path.c_str(), project_path.length());
|
||||
std::string encoded_project = encoded;
|
||||
curl_free(encoded);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
pr_url = "https://gitlab.com/api/v4/projects/" + encoded_project + "/merge_requests/" + std::to_string(pr_number);
|
||||
files_url = "https://gitlab.com/api/v4/projects/" + encoded_project + "/merge_requests/" + std::to_string(pr_number) + "/changes";
|
||||
} else {
|
||||
std::cerr << "Unknown platform" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Fetch PR/MR info
|
||||
std::string response;
|
||||
if (!http_get(pr_url, token, response, platform)) {
|
||||
std::cerr << "Failed to fetch pull/merge request info" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
Json::Value root;
|
||||
Json::CharReaderBuilder reader;
|
||||
std::string errs;
|
||||
std::istringstream s(response);
|
||||
|
||||
if (!Json::parseFromStream(reader, s, &root, &errs)) {
|
||||
std::cerr << "Failed to parse PR/MR JSON: " << errs << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
pr.title = root.get("title", "").asString();
|
||||
pr.state = root.get("state", "").asString();
|
||||
|
||||
if (platform == GitPlatform::GitHub) {
|
||||
if (root.isMember("base") && root["base"].isObject()) {
|
||||
pr.base_ref = root["base"].get("ref", "").asString();
|
||||
pr.base_sha = root["base"].get("sha", "").asString();
|
||||
}
|
||||
|
||||
if (root.isMember("head") && root["head"].isObject()) {
|
||||
pr.head_ref = root["head"].get("ref", "").asString();
|
||||
pr.head_sha = root["head"].get("sha", "").asString();
|
||||
}
|
||||
|
||||
pr.mergeable = root.get("mergeable", false).asBool();
|
||||
pr.mergeable_state = root.get("mergeable_state", "unknown").asString();
|
||||
} else if (platform == GitPlatform::GitLab) {
|
||||
pr.base_ref = root.get("target_branch", "").asString();
|
||||
pr.head_ref = root.get("source_branch", "").asString();
|
||||
pr.base_sha = root.get("diff_refs", Json::Value::null).get("base_sha", "").asString();
|
||||
pr.head_sha = root.get("diff_refs", Json::Value::null).get("head_sha", "").asString();
|
||||
|
||||
// GitLab uses different merge status
|
||||
std::string merge_status = root.get("merge_status", "").asString();
|
||||
pr.mergeable = (merge_status == "can_be_merged");
|
||||
pr.mergeable_state = merge_status;
|
||||
}
|
||||
|
||||
// Fetch PR/MR files
|
||||
std::string files_response;
|
||||
|
||||
if (!http_get(files_url, token, files_response, platform)) {
|
||||
std::cerr << "Failed to fetch pull/merge request files" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Json::Value files_root;
|
||||
std::istringstream files_stream(files_response);
|
||||
|
||||
if (!Json::parseFromStream(reader, files_stream, &files_root, &errs)) {
|
||||
std::cerr << "Failed to parse files JSON: " << errs << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Process files based on platform
|
||||
if (platform == GitPlatform::GitHub && files_root.isArray()) {
|
||||
// GitHub format: array of file objects
|
||||
for (const auto& file : files_root) {
|
||||
PRFile pr_file;
|
||||
pr_file.filename = file.get("filename", "").asString();
|
||||
pr_file.status = file.get("status", "").asString();
|
||||
pr_file.additions = file.get("additions", 0).asInt();
|
||||
pr_file.deletions = file.get("deletions", 0).asInt();
|
||||
pr_file.changes = file.get("changes", 0).asInt();
|
||||
|
||||
pr.files.push_back(pr_file);
|
||||
}
|
||||
} else if (platform == GitPlatform::GitLab && files_root.isMember("changes")) {
|
||||
// GitLab format: object with "changes" array
|
||||
const Json::Value& changes = files_root["changes"];
|
||||
if (changes.isArray()) {
|
||||
for (const auto& file : changes) {
|
||||
PRFile pr_file;
|
||||
pr_file.filename = file.get("new_path", file.get("old_path", "").asString()).asString();
|
||||
|
||||
// Determine status from new_file, deleted_file, renamed_file flags
|
||||
bool new_file = file.get("new_file", false).asBool();
|
||||
bool deleted_file = file.get("deleted_file", false).asBool();
|
||||
bool renamed_file = file.get("renamed_file", false).asBool();
|
||||
|
||||
if (new_file) {
|
||||
pr_file.status = "added";
|
||||
} else if (deleted_file) {
|
||||
pr_file.status = "removed";
|
||||
} else if (renamed_file) {
|
||||
pr_file.status = "renamed";
|
||||
} else {
|
||||
pr_file.status = "modified";
|
||||
}
|
||||
|
||||
// GitLab doesn't provide addition/deletion counts in the changes endpoint
|
||||
pr_file.additions = 0;
|
||||
pr_file.deletions = 0;
|
||||
pr_file.changes = 0;
|
||||
|
||||
pr.files.push_back(pr_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pr;
|
||||
}
|
||||
|
||||
std::optional<std::vector<std::string>> fetch_file_content(
|
||||
GitPlatform platform,
|
||||
const std::string& owner,
|
||||
const std::string& repo,
|
||||
const std::string& sha,
|
||||
const std::string& path,
|
||||
const std::string& token
|
||||
) {
|
||||
std::string url;
|
||||
|
||||
if (platform == GitPlatform::GitHub) {
|
||||
// GitHub API URL
|
||||
url = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + path + "?ref=" + sha;
|
||||
} else if (platform == GitPlatform::GitLab) {
|
||||
// GitLab API URL - encode project path and file path
|
||||
std::string project_path = owner;
|
||||
if (!repo.empty()) {
|
||||
project_path += "/" + repo;
|
||||
}
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
char* encoded_project = curl_easy_escape(curl, project_path.c_str(), project_path.length());
|
||||
char* encoded_path = curl_easy_escape(curl, path.c_str(), path.length());
|
||||
|
||||
url = "https://gitlab.com/api/v4/projects/" + std::string(encoded_project) +
|
||||
"/repository/files/" + std::string(encoded_path) + "/raw?ref=" + sha;
|
||||
|
||||
curl_free(encoded_project);
|
||||
curl_free(encoded_path);
|
||||
curl_easy_cleanup(curl);
|
||||
} else {
|
||||
std::cerr << "Unknown platform" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string response;
|
||||
|
||||
if (!http_get(url, token, response, platform)) {
|
||||
std::cerr << "Failed to fetch file content for " << path << " at " << sha << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Handle response based on platform
|
||||
if (platform == GitPlatform::GitHub) {
|
||||
// GitHub returns JSON with base64-encoded content
|
||||
Json::Value root;
|
||||
Json::CharReaderBuilder reader;
|
||||
std::string errs;
|
||||
std::istringstream s(response);
|
||||
|
||||
if (!Json::parseFromStream(reader, s, &root, &errs)) {
|
||||
std::cerr << "Failed to parse content JSON: " << errs << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// GitHub API returns content as base64 encoded
|
||||
if (!root.isMember("content") || !root.isMember("encoding")) {
|
||||
std::cerr << "Invalid response format for file content" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string encoding = root["encoding"].asString();
|
||||
if (encoding != "base64") {
|
||||
std::cerr << "Unsupported encoding: " << encoding << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Decode base64 content
|
||||
std::string encoded_content = root["content"].asString();
|
||||
|
||||
// Remove newlines from base64 string
|
||||
encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\n'), encoded_content.end());
|
||||
encoded_content.erase(std::remove(encoded_content.begin(), encoded_content.end(), '\r'), encoded_content.end());
|
||||
|
||||
// Decode base64
|
||||
std::string decoded_content = base64_decode(encoded_content);
|
||||
|
||||
if (decoded_content.empty()) {
|
||||
std::cerr << "Failed to decode base64 content" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Split content into lines
|
||||
return split_lines(decoded_content);
|
||||
} else if (platform == GitPlatform::GitLab) {
|
||||
// GitLab returns raw file content directly
|
||||
return split_lines(response);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace git
|
||||
} // namespace wizardmerge
|
||||
54
backend/src/main.cpp
Normal file
54
backend/src/main.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @file main.cpp
|
||||
* @brief HTTP API server for WizardMerge using Drogon framework
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <drogon/drogon.h>
|
||||
#include "controllers/MergeController.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::cout << "WizardMerge - Intelligent Merge Conflict Resolution API\n";
|
||||
std::cout << "======================================================\n";
|
||||
std::cout << "Starting HTTP server...\n\n";
|
||||
|
||||
// Load configuration from file
|
||||
std::string config_file = "config.json";
|
||||
if (argc > 1) {
|
||||
config_file = argv[1];
|
||||
}
|
||||
|
||||
try {
|
||||
// Load configuration and start server
|
||||
app().loadConfigFile(config_file);
|
||||
|
||||
// Display listener information if available
|
||||
auto listeners = app().getListeners();
|
||||
if (!listeners.empty()) {
|
||||
try {
|
||||
std::cout << "Server will listen on port "
|
||||
<< listeners[0].toPort << "\n";
|
||||
} catch (...) {
|
||||
std::cout << "Server listener configured\n";
|
||||
}
|
||||
} else {
|
||||
std::cout << "Server configuration loaded\n";
|
||||
}
|
||||
|
||||
std::cout << "Available endpoints:\n";
|
||||
std::cout << " POST /api/merge - Three-way merge API\n";
|
||||
std::cout << "\nPress Ctrl+C to stop the server.\n\n";
|
||||
|
||||
// Run the application
|
||||
app().run();
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error: " << e.what() << '\n';
|
||||
std::cerr << "Failed to load config file: " << config_file << '\n';
|
||||
std::cerr << "Usage: " << argv[0] << " [config.json]\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
117
backend/src/merge/three_way_merge.cpp
Normal file
117
backend/src/merge/three_way_merge.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @file three_way_merge.cpp
|
||||
* @brief Implementation of three-way merge algorithm
|
||||
*/
|
||||
|
||||
#include "wizardmerge/merge/three_way_merge.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace wizardmerge {
|
||||
namespace merge {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Check if two lines are effectively equal (ignoring whitespace).
|
||||
*/
|
||||
bool lines_equal_ignore_whitespace(const std::string& a, const std::string& b) {
|
||||
auto trim = [](const std::string& s) {
|
||||
size_t start = s.find_first_not_of(" \t\n\r");
|
||||
size_t end = s.find_last_not_of(" \t\n\r");
|
||||
if (start == std::string::npos) return std::string();
|
||||
return s.substr(start, end - start + 1);
|
||||
};
|
||||
return trim(a) == trim(b);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MergeResult three_way_merge(
|
||||
const std::vector<std::string>& base,
|
||||
const std::vector<std::string>& ours,
|
||||
const std::vector<std::string>& theirs
|
||||
) {
|
||||
MergeResult result;
|
||||
|
||||
// Simple line-by-line comparison for initial implementation
|
||||
// This is a placeholder - full algorithm will use dependency analysis
|
||||
|
||||
size_t max_len = std::max({base.size(), ours.size(), theirs.size()});
|
||||
|
||||
for (size_t i = 0; i < max_len; ++i) {
|
||||
std::string base_line = (i < base.size()) ? base[i] : "";
|
||||
std::string our_line = (i < ours.size()) ? ours[i] : "";
|
||||
std::string their_line = (i < theirs.size()) ? theirs[i] : "";
|
||||
|
||||
// Case 1: All three are the same - use as-is
|
||||
if (base_line == our_line && base_line == their_line) {
|
||||
result.merged_lines.push_back({base_line, Line::BASE});
|
||||
}
|
||||
// Case 2: Base == Ours, but Theirs changed - use theirs
|
||||
else if (base_line == our_line && base_line != their_line) {
|
||||
result.merged_lines.push_back({their_line, Line::THEIRS});
|
||||
}
|
||||
// Case 3: Base == Theirs, but Ours changed - use ours
|
||||
else if (base_line == their_line && base_line != our_line) {
|
||||
result.merged_lines.push_back({our_line, Line::OURS});
|
||||
}
|
||||
// Case 4: Ours == Theirs, but different from Base - use the common change
|
||||
else if (our_line == their_line && our_line != base_line) {
|
||||
result.merged_lines.push_back({our_line, Line::MERGED});
|
||||
}
|
||||
// Case 5: All different - conflict
|
||||
else {
|
||||
Conflict conflict;
|
||||
conflict.start_line = result.merged_lines.size();
|
||||
conflict.base_lines.push_back({base_line, Line::BASE});
|
||||
conflict.our_lines.push_back({our_line, Line::OURS});
|
||||
conflict.their_lines.push_back({their_line, Line::THEIRS});
|
||||
conflict.end_line = result.merged_lines.size();
|
||||
|
||||
result.conflicts.push_back(conflict);
|
||||
|
||||
// Add conflict markers
|
||||
result.merged_lines.push_back({"<<<<<<< OURS", Line::MERGED});
|
||||
result.merged_lines.push_back({our_line, Line::OURS});
|
||||
result.merged_lines.push_back({"=======", Line::MERGED});
|
||||
result.merged_lines.push_back({their_line, Line::THEIRS});
|
||||
result.merged_lines.push_back({">>>>>>> THEIRS", Line::MERGED});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
MergeResult auto_resolve(const MergeResult& result) {
|
||||
MergeResult resolved = result;
|
||||
|
||||
// Auto-resolve whitespace-only differences
|
||||
std::vector<Conflict> remaining_conflicts;
|
||||
|
||||
for (const auto& conflict : result.conflicts) {
|
||||
bool can_resolve = false;
|
||||
|
||||
// Check if differences are whitespace-only
|
||||
if (conflict.our_lines.size() == conflict.their_lines.size()) {
|
||||
can_resolve = true;
|
||||
for (size_t i = 0; i < conflict.our_lines.size(); ++i) {
|
||||
if (!lines_equal_ignore_whitespace(
|
||||
conflict.our_lines[i].content,
|
||||
conflict.their_lines[i].content)) {
|
||||
can_resolve = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!can_resolve) {
|
||||
remaining_conflicts.push_back(conflict);
|
||||
}
|
||||
}
|
||||
|
||||
resolved.conflicts = remaining_conflicts;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace merge
|
||||
} // namespace wizardmerge
|
||||
116
backend/tests/test_git_platform_client.cpp
Normal file
116
backend/tests/test_git_platform_client.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @file test_git_platform_client.cpp
|
||||
* @brief Unit tests for git platform client functionality (GitHub and GitLab)
|
||||
*/
|
||||
|
||||
#include "wizardmerge/git/git_platform_client.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace wizardmerge::git;
|
||||
|
||||
/**
|
||||
* Test PR URL parsing with various GitHub formats
|
||||
*/
|
||||
TEST(GitPlatformClientTest, ParseGitHubPRUrl_ValidUrls) {
|
||||
GitPlatform platform;
|
||||
std::string owner, repo;
|
||||
int pr_number;
|
||||
|
||||
// Test full HTTPS URL
|
||||
ASSERT_TRUE(parse_pr_url("https://github.com/owner/repo/pull/123", platform, owner, repo, pr_number));
|
||||
EXPECT_EQ(platform, GitPlatform::GitHub);
|
||||
EXPECT_EQ(owner, "owner");
|
||||
EXPECT_EQ(repo, "repo");
|
||||
EXPECT_EQ(pr_number, 123);
|
||||
|
||||
// Test without https://
|
||||
ASSERT_TRUE(parse_pr_url("github.com/user/project/pull/456", platform, owner, repo, pr_number));
|
||||
EXPECT_EQ(platform, GitPlatform::GitHub);
|
||||
EXPECT_EQ(owner, "user");
|
||||
EXPECT_EQ(repo, "project");
|
||||
EXPECT_EQ(pr_number, 456);
|
||||
|
||||
// Test with www
|
||||
ASSERT_TRUE(parse_pr_url("https://www.github.com/testuser/testrepo/pull/789", platform, owner, repo, pr_number));
|
||||
EXPECT_EQ(platform, GitPlatform::GitHub);
|
||||
EXPECT_EQ(owner, "testuser");
|
||||
EXPECT_EQ(repo, "testrepo");
|
||||
EXPECT_EQ(pr_number, 789);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test GitLab MR URL parsing with various formats
|
||||
*/
|
||||
TEST(GitPlatformClientTest, ParseGitLabMRUrl_ValidUrls) {
|
||||
GitPlatform platform;
|
||||
std::string owner, repo;
|
||||
int pr_number;
|
||||
|
||||
// Test full HTTPS URL
|
||||
ASSERT_TRUE(parse_pr_url("https://gitlab.com/owner/repo/-/merge_requests/123", platform, owner, repo, pr_number));
|
||||
EXPECT_EQ(platform, GitPlatform::GitLab);
|
||||
EXPECT_EQ(owner, "owner");
|
||||
EXPECT_EQ(repo, "repo");
|
||||
EXPECT_EQ(pr_number, 123);
|
||||
|
||||
// Test with group/subgroup/project
|
||||
ASSERT_TRUE(parse_pr_url("https://gitlab.com/group/subgroup/project/-/merge_requests/456", platform, owner, repo, pr_number));
|
||||
EXPECT_EQ(platform, GitPlatform::GitLab);
|
||||
EXPECT_EQ(owner, "group/subgroup");
|
||||
EXPECT_EQ(repo, "project");
|
||||
EXPECT_EQ(pr_number, 456);
|
||||
|
||||
// Test without https://
|
||||
ASSERT_TRUE(parse_pr_url("gitlab.com/mygroup/myproject/-/merge_requests/789", platform, owner, repo, pr_number));
|
||||
EXPECT_EQ(platform, GitPlatform::GitLab);
|
||||
EXPECT_EQ(owner, "mygroup");
|
||||
EXPECT_EQ(repo, "myproject");
|
||||
EXPECT_EQ(pr_number, 789);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PR/MR URL parsing with invalid formats
|
||||
*/
|
||||
TEST(GitPlatformClientTest, ParsePRUrl_InvalidUrls) {
|
||||
GitPlatform platform;
|
||||
std::string owner, repo;
|
||||
int pr_number;
|
||||
|
||||
// Missing PR number
|
||||
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo/pull/", platform, owner, repo, pr_number));
|
||||
|
||||
// Invalid format
|
||||
EXPECT_FALSE(parse_pr_url("https://github.com/owner/repo", platform, owner, repo, pr_number));
|
||||
|
||||
// Not a GitHub or GitLab URL
|
||||
EXPECT_FALSE(parse_pr_url("https://bitbucket.org/owner/repo/pull-requests/123", platform, owner, repo, pr_number));
|
||||
|
||||
// Empty string
|
||||
EXPECT_FALSE(parse_pr_url("", platform, owner, repo, pr_number));
|
||||
|
||||
// Wrong path for GitLab
|
||||
EXPECT_FALSE(parse_pr_url("https://gitlab.com/owner/repo/pull/123", platform, owner, repo, pr_number));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PR/MR URL with special characters in owner/repo names
|
||||
*/
|
||||
TEST(GitPlatformClientTest, ParsePRUrl_SpecialCharacters) {
|
||||
GitPlatform platform;
|
||||
std::string owner, repo;
|
||||
int pr_number;
|
||||
|
||||
// GitHub: Underscores and hyphens
|
||||
ASSERT_TRUE(parse_pr_url("https://github.com/my-owner_123/my-repo_456/pull/999", platform, owner, repo, pr_number));
|
||||
EXPECT_EQ(platform, GitPlatform::GitHub);
|
||||
EXPECT_EQ(owner, "my-owner_123");
|
||||
EXPECT_EQ(repo, "my-repo_456");
|
||||
EXPECT_EQ(pr_number, 999);
|
||||
|
||||
// GitLab: Complex group paths
|
||||
ASSERT_TRUE(parse_pr_url("https://gitlab.com/org-name/team-1/my_project/-/merge_requests/100", platform, owner, repo, pr_number));
|
||||
EXPECT_EQ(platform, GitPlatform::GitLab);
|
||||
EXPECT_EQ(owner, "org-name/team-1");
|
||||
EXPECT_EQ(repo, "my_project");
|
||||
EXPECT_EQ(pr_number, 100);
|
||||
}
|
||||
125
backend/tests/test_three_way_merge.cpp
Normal file
125
backend/tests/test_three_way_merge.cpp
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @file test_three_way_merge.cpp
|
||||
* @brief Unit tests for three-way merge algorithm
|
||||
*/
|
||||
|
||||
#include "wizardmerge/merge/three_way_merge.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace wizardmerge::merge;
|
||||
|
||||
/**
|
||||
* Test basic three-way merge with no conflicts
|
||||
*/
|
||||
TEST(ThreeWayMergeTest, NoConflicts) {
|
||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||
std::vector<std::string> ours = {"line1", "line2_modified", "line3"};
|
||||
std::vector<std::string> theirs = {"line1", "line2", "line3_modified"};
|
||||
|
||||
auto result = three_way_merge(base, ours, theirs);
|
||||
|
||||
EXPECT_FALSE(result.has_conflicts());
|
||||
ASSERT_EQ(result.merged_lines.size(), 3);
|
||||
EXPECT_EQ(result.merged_lines[0].content, "line1");
|
||||
EXPECT_EQ(result.merged_lines[1].content, "line2_modified");
|
||||
EXPECT_EQ(result.merged_lines[2].content, "line3_modified");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test three-way merge with conflicts
|
||||
*/
|
||||
TEST(ThreeWayMergeTest, WithConflicts) {
|
||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||
std::vector<std::string> ours = {"line1", "line2_ours", "line3"};
|
||||
std::vector<std::string> theirs = {"line1", "line2_theirs", "line3"};
|
||||
|
||||
auto result = three_way_merge(base, ours, theirs);
|
||||
|
||||
EXPECT_TRUE(result.has_conflicts());
|
||||
EXPECT_EQ(result.conflicts.size(), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test identical changes from both sides
|
||||
*/
|
||||
TEST(ThreeWayMergeTest, IdenticalChanges) {
|
||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||
std::vector<std::string> ours = {"line1", "line2_same", "line3"};
|
||||
std::vector<std::string> theirs = {"line1", "line2_same", "line3"};
|
||||
|
||||
auto result = three_way_merge(base, ours, theirs);
|
||||
|
||||
EXPECT_FALSE(result.has_conflicts());
|
||||
EXPECT_EQ(result.merged_lines[1].content, "line2_same");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test base equals ours, theirs changed
|
||||
*/
|
||||
TEST(ThreeWayMergeTest, BaseEqualsOurs) {
|
||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||
std::vector<std::string> ours = {"line1", "line2", "line3"};
|
||||
std::vector<std::string> theirs = {"line1", "line2_changed", "line3"};
|
||||
|
||||
auto result = three_way_merge(base, ours, theirs);
|
||||
|
||||
EXPECT_FALSE(result.has_conflicts());
|
||||
EXPECT_EQ(result.merged_lines[1].content, "line2_changed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test base equals theirs, ours changed
|
||||
*/
|
||||
TEST(ThreeWayMergeTest, BaseEqualsTheirs) {
|
||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||
std::vector<std::string> ours = {"line1", "line2_changed", "line3"};
|
||||
std::vector<std::string> theirs = {"line1", "line2", "line3"};
|
||||
|
||||
auto result = three_way_merge(base, ours, theirs);
|
||||
|
||||
EXPECT_FALSE(result.has_conflicts());
|
||||
EXPECT_EQ(result.merged_lines[1].content, "line2_changed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test auto-resolve whitespace differences
|
||||
*/
|
||||
TEST(AutoResolveTest, WhitespaceOnly) {
|
||||
std::vector<std::string> base = {"line1", "line2", "line3"};
|
||||
std::vector<std::string> ours = {"line1", " line2_changed ", "line3"};
|
||||
std::vector<std::string> theirs = {"line1", "line2_changed", "line3"};
|
||||
|
||||
auto result = three_way_merge(base, ours, theirs);
|
||||
auto resolved = auto_resolve(result);
|
||||
|
||||
// Whitespace-only differences should be auto-resolved
|
||||
EXPECT_LT(resolved.conflicts.size(), result.conflicts.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test empty files
|
||||
*/
|
||||
TEST(ThreeWayMergeTest, EmptyFiles) {
|
||||
std::vector<std::string> base = {};
|
||||
std::vector<std::string> ours = {};
|
||||
std::vector<std::string> theirs = {};
|
||||
|
||||
auto result = three_way_merge(base, ours, theirs);
|
||||
|
||||
EXPECT_FALSE(result.has_conflicts());
|
||||
EXPECT_EQ(result.merged_lines.size(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test one side adds lines
|
||||
*/
|
||||
TEST(ThreeWayMergeTest, OneSideAddsLines) {
|
||||
std::vector<std::string> base = {"line1"};
|
||||
std::vector<std::string> ours = {"line1", "line2"};
|
||||
std::vector<std::string> theirs = {"line1"};
|
||||
|
||||
auto result = three_way_merge(base, ours, theirs);
|
||||
|
||||
EXPECT_FALSE(result.has_conflicts());
|
||||
ASSERT_EQ(result.merged_lines.size(), 2);
|
||||
}
|
||||
184
frontends/README.md
Normal file
184
frontends/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# WizardMerge Frontends
|
||||
|
||||
This directory contains multiple frontend implementations for WizardMerge, each designed for different use cases and workflows.
|
||||
|
||||
## Available Frontends
|
||||
|
||||
### 1. Qt6 Desktop Frontend (`qt6/`)
|
||||
|
||||
**Type**: Native desktop application
|
||||
**Language**: C++ with Qt6 and QML
|
||||
**Platforms**: Linux, Windows, macOS
|
||||
|
||||
A native desktop application providing the best performance and integration with desktop environments.
|
||||
|
||||
**Features**:
|
||||
- Native window management and desktop integration
|
||||
- Offline capability with embedded backend option
|
||||
- High-performance rendering
|
||||
- Three-panel diff viewer with QML-based UI
|
||||
- Keyboard shortcuts and native file dialogs
|
||||
|
||||
**Best for**: Desktop users who want a fast, native application with full offline support.
|
||||
|
||||
See [qt6/README.md](qt6/README.md) for build and usage instructions.
|
||||
|
||||
### 2. Next.js Web Frontend (`nextjs/`)
|
||||
|
||||
**Type**: Web application
|
||||
**Language**: TypeScript with React/Next.js
|
||||
**Runtime**: bun
|
||||
|
||||
A modern web-based interface accessible from any browser.
|
||||
|
||||
**Features**:
|
||||
- Cross-platform browser access
|
||||
- No installation required
|
||||
- Real-time collaboration (planned)
|
||||
- Responsive design
|
||||
- Easy deployment and updates
|
||||
|
||||
**Best for**: Teams needing shared access, cloud deployments, or users who prefer web interfaces.
|
||||
|
||||
See [nextjs/README.md](nextjs/README.md) for development and deployment instructions.
|
||||
|
||||
### 3. CLI Frontend (`cli/`)
|
||||
|
||||
**Type**: Command-line interface
|
||||
**Language**: C++
|
||||
**Platforms**: Linux, Windows, macOS
|
||||
|
||||
A command-line tool for automation and scripting.
|
||||
|
||||
**Features**:
|
||||
- Non-interactive batch processing
|
||||
- Scriptable and automatable
|
||||
- CI/CD pipeline integration
|
||||
- Git workflow integration
|
||||
- Minimal dependencies
|
||||
|
||||
**Best for**: Automation, scripting, CI/CD pipelines, and terminal-based workflows.
|
||||
|
||||
See [cli/README.md](cli/README.md) for usage and examples.
|
||||
|
||||
## Architecture
|
||||
|
||||
All frontends communicate with the WizardMerge C++ backend through a common HTTP API:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Frontends │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
|
||||
│ │ Qt6 Native │ │ Next.js │ │ CLI │ │
|
||||
│ │ (C++) │ │(TypeScript)│ │ (C++) │ │
|
||||
│ └─────┬──────┘ └──────┬─────┘ └────┬─────┘ │
|
||||
└────────┼─────────────────┼─────────────┼───────┘
|
||||
│ │ │
|
||||
└─────────────────┼─────────────┘
|
||||
│
|
||||
HTTP REST API
|
||||
│
|
||||
┌─────────────────▼──────────────────┐
|
||||
│ WizardMerge C++ Backend │
|
||||
│ (Drogon HTTP Server) │
|
||||
│ │
|
||||
│ - Three-way merge algorithm │
|
||||
│ - Conflict detection │
|
||||
│ - Auto-resolution │
|
||||
│ - Semantic analysis │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Backend API
|
||||
|
||||
The backend provides a REST API on port 8080 (configurable):
|
||||
|
||||
- `POST /api/merge` - Perform three-way merge
|
||||
|
||||
All frontends use this common API, ensuring consistent merge behavior regardless of the interface used.
|
||||
|
||||
## Choosing a Frontend
|
||||
|
||||
| Feature | Qt6 | Next.js | CLI |
|
||||
|---------|-----|---------|-----|
|
||||
| Native Performance | ✓ | - | ✓ |
|
||||
| Offline Support | ✓ | - | ✓ |
|
||||
| Web Browser Access | - | ✓ | - |
|
||||
| Collaboration | - | ✓ (planned) | - |
|
||||
| Automation/Scripting | - | - | ✓ |
|
||||
| Visual UI | ✓ | ✓ | - |
|
||||
| Installation Required | ✓ | - | ✓ |
|
||||
| Platform Support | All | All | All |
|
||||
|
||||
## Building All Frontends
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Install dependencies for all frontends you want to build:
|
||||
|
||||
```bash
|
||||
# Qt6 (for qt6 frontend)
|
||||
# Ubuntu/Debian:
|
||||
sudo apt-get install qt6-base-dev qt6-declarative-dev
|
||||
|
||||
# macOS:
|
||||
brew install qt@6
|
||||
|
||||
# Next.js (for nextjs frontend)
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# CLI (for cli frontend)
|
||||
# Ubuntu/Debian:
|
||||
sudo apt-get install libcurl4-openssl-dev
|
||||
|
||||
# macOS:
|
||||
brew install curl
|
||||
```
|
||||
|
||||
### Build All
|
||||
|
||||
```bash
|
||||
# Build Qt6 frontend
|
||||
cd qt6
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja
|
||||
ninja
|
||||
cd ../..
|
||||
|
||||
# Build/setup Next.js frontend
|
||||
cd nextjs
|
||||
bun install
|
||||
bun run build
|
||||
cd ..
|
||||
|
||||
# Build CLI frontend
|
||||
cd cli
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja
|
||||
ninja
|
||||
cd ../..
|
||||
```
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
When developing a frontend:
|
||||
|
||||
1. **Consistency**: Maintain consistent UX across all frontends where applicable
|
||||
2. **API Usage**: Use the common backend API for all merge operations
|
||||
3. **Error Handling**: Properly handle backend connection errors and API failures
|
||||
4. **Documentation**: Update frontend-specific README files
|
||||
5. **Testing**: Add tests for new features
|
||||
|
||||
### Adding a New Frontend
|
||||
|
||||
To add a new frontend implementation:
|
||||
|
||||
1. Create a new directory under `frontends/`
|
||||
2. Implement the UI using your chosen technology
|
||||
3. Use the backend HTTP API (`POST /api/merge`)
|
||||
4. Add a README.md with build and usage instructions
|
||||
5. Update this file to list the new frontend
|
||||
|
||||
## License
|
||||
|
||||
See [../LICENSE](../LICENSE) for details.
|
||||
61
frontends/cli/CMakeLists.txt
Normal file
61
frontends/cli/CMakeLists.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(wizardmerge-cli-frontend VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Find libcurl
|
||||
find_package(CURL QUIET)
|
||||
|
||||
if(NOT CURL_FOUND)
|
||||
message(WARNING "libcurl not found. Skipping CLI frontend build.")
|
||||
message(WARNING "Install libcurl to build the CLI frontend:")
|
||||
message(WARNING " - Ubuntu/Debian: sudo apt-get install libcurl4-openssl-dev")
|
||||
message(WARNING " - macOS: brew install curl")
|
||||
message(WARNING " - Windows: Install via vcpkg or use system curl")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Source files
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
src/http_client.cpp
|
||||
src/file_utils.cpp
|
||||
)
|
||||
|
||||
# Header files
|
||||
set(HEADERS
|
||||
include/http_client.h
|
||||
include/file_utils.h
|
||||
)
|
||||
|
||||
# Create executable
|
||||
add_executable(wizardmerge-cli-frontend
|
||||
${SOURCES}
|
||||
${HEADERS}
|
||||
)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(wizardmerge-cli-frontend PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
${CURL_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
# Link libraries
|
||||
target_link_libraries(wizardmerge-cli-frontend PRIVATE
|
||||
${CURL_LIBRARIES}
|
||||
)
|
||||
|
||||
# Compiler warnings
|
||||
if(MSVC)
|
||||
target_compile_options(wizardmerge-cli-frontend PRIVATE /W4)
|
||||
else()
|
||||
target_compile_options(wizardmerge-cli-frontend PRIVATE -Wall -Wextra -pedantic)
|
||||
endif()
|
||||
|
||||
# Install target
|
||||
install(TARGETS wizardmerge-cli-frontend
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
|
||||
message(STATUS "CLI frontend configured successfully")
|
||||
293
frontends/cli/README.md
Normal file
293
frontends/cli/README.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# WizardMerge CLI Frontend
|
||||
|
||||
Command-line interface for WizardMerge merge conflict resolution.
|
||||
|
||||
## Features
|
||||
|
||||
- Simple command-line interface
|
||||
- Communicates with WizardMerge backend via HTTP API
|
||||
- Suitable for automation and scripting
|
||||
- Cross-platform (Linux, Windows, macOS)
|
||||
- Non-interactive batch processing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- C++17 compiler (GCC 7+, Clang 6+, MSVC 2017+)
|
||||
- CMake 3.15+
|
||||
- libcurl (for HTTP client)
|
||||
|
||||
## Building
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt-get install libcurl4-openssl-dev
|
||||
```
|
||||
|
||||
**macOS (Homebrew):**
|
||||
```bash
|
||||
brew install curl
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
libcurl is typically included with MSVC or can be installed via vcpkg.
|
||||
|
||||
### Build the Application
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make
|
||||
```
|
||||
|
||||
Or using Ninja:
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
ninja
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
./wizardmerge-cli --help
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Three-Way Merge
|
||||
|
||||
```bash
|
||||
wizardmerge-cli merge --base base.txt --ours ours.txt --theirs theirs.txt -o result.txt
|
||||
```
|
||||
|
||||
### Merge with Backend Server
|
||||
|
||||
```bash
|
||||
# Use default backend (http://localhost:8080)
|
||||
wizardmerge-cli merge --base base.txt --ours ours.txt --theirs theirs.txt
|
||||
|
||||
# Specify custom backend URL
|
||||
wizardmerge-cli --backend http://remote-server:8080 merge --base base.txt --ours ours.txt --theirs theirs.txt
|
||||
```
|
||||
|
||||
### Git Integration
|
||||
|
||||
```bash
|
||||
# Resolve conflicts in a Git repository
|
||||
cd /path/to/git/repo
|
||||
wizardmerge-cli git-resolve
|
||||
|
||||
# Resolve a specific file
|
||||
wizardmerge-cli git-resolve path/to/conflicted/file.txt
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
|
||||
```bash
|
||||
# Process all conflicted files in current directory
|
||||
wizardmerge-cli batch-resolve .
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
|
||||
### Global Options
|
||||
|
||||
- `--backend <url>` - Backend server URL (default: http://localhost:8080)
|
||||
- `--verbose, -v` - Enable verbose output
|
||||
- `--quiet, -q` - Suppress non-error output
|
||||
- `--help, -h` - Show help message
|
||||
- `--version` - Show version information
|
||||
|
||||
### Commands
|
||||
|
||||
#### merge
|
||||
|
||||
Perform a three-way merge operation.
|
||||
|
||||
```bash
|
||||
wizardmerge-cli merge [OPTIONS]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--base <file>` - Path to base version (required)
|
||||
- `--ours <file>` - Path to our version (required)
|
||||
- `--theirs <file>` - Path to their version (required)
|
||||
- `-o, --output <file>` - Output file path (default: stdout)
|
||||
- `--format <format>` - Output format: text, json (default: text)
|
||||
|
||||
#### git-resolve
|
||||
|
||||
Resolve Git merge conflicts.
|
||||
|
||||
```bash
|
||||
wizardmerge-cli git-resolve [FILE]
|
||||
```
|
||||
|
||||
Arguments:
|
||||
- `FILE` - Specific file to resolve (optional, resolves all if omitted)
|
||||
|
||||
#### batch-resolve
|
||||
|
||||
Batch process multiple files.
|
||||
|
||||
```bash
|
||||
wizardmerge-cli batch-resolve [DIRECTORY]
|
||||
```
|
||||
|
||||
Arguments:
|
||||
- `DIRECTORY` - Directory to scan for conflicts (default: current directory)
|
||||
|
||||
Options:
|
||||
- `--recursive, -r` - Process directories recursively
|
||||
- `--pattern <pattern>` - File pattern to match (default: *)
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Merge
|
||||
|
||||
```bash
|
||||
# Create test files
|
||||
echo -e "line1\nline2\nline3" > base.txt
|
||||
echo -e "line1\nline2-ours\nline3" > ours.txt
|
||||
echo -e "line1\nline2-theirs\nline3" > theirs.txt
|
||||
|
||||
# Perform merge
|
||||
wizardmerge-cli merge --base base.txt --ours ours.txt --theirs theirs.txt
|
||||
```
|
||||
|
||||
### Example 2: JSON Output
|
||||
|
||||
```bash
|
||||
wizardmerge-cli merge --base base.txt --ours ours.txt --theirs theirs.txt --format json > result.json
|
||||
```
|
||||
|
||||
### Example 3: Git Workflow
|
||||
|
||||
```bash
|
||||
# In a Git repository with conflicts
|
||||
git merge feature-branch
|
||||
# Conflicts occur...
|
||||
|
||||
# Resolve using WizardMerge
|
||||
wizardmerge-cli git-resolve
|
||||
|
||||
# Review and commit
|
||||
git commit
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- `0` - Success (no conflicts or all conflicts resolved)
|
||||
- `1` - General error
|
||||
- `2` - Invalid arguments
|
||||
- `3` - Backend connection error
|
||||
- `4` - File I/O error
|
||||
- `5` - Merge conflicts detected (when running in strict mode)
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration can be provided via:
|
||||
|
||||
1. Command-line arguments (highest priority)
|
||||
2. Environment variables:
|
||||
- `WIZARDMERGE_BACKEND` - Backend server URL
|
||||
- `WIZARDMERGE_VERBOSE` - Enable verbose output (1/0)
|
||||
3. Configuration file `~/.wizardmergerc` (lowest priority)
|
||||
|
||||
### Configuration File Format
|
||||
|
||||
```ini
|
||||
[backend]
|
||||
url = http://localhost:8080
|
||||
|
||||
[cli]
|
||||
verbose = false
|
||||
format = text
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
cli/
|
||||
├── CMakeLists.txt # CMake build configuration
|
||||
├── README.md # This file
|
||||
├── src/ # C++ source files
|
||||
│ ├── main.cpp # Application entry point
|
||||
│ ├── http_client.cpp # HTTP client implementation
|
||||
│ └── file_utils.cpp # File handling utilities
|
||||
└── include/ # Header files
|
||||
├── http_client.h
|
||||
└── file_utils.h
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Architecture
|
||||
|
||||
The CLI frontend is a thin client that:
|
||||
1. Parses command-line arguments
|
||||
2. Reads input files
|
||||
3. Sends HTTP requests to backend
|
||||
4. Formats and displays results
|
||||
|
||||
### Current Limitations
|
||||
|
||||
**JSON Handling (Prototype Implementation)**:
|
||||
- The current implementation uses simple string-based JSON serialization/parsing
|
||||
- Does NOT escape special characters (quotes, backslashes, newlines, etc.)
|
||||
- Will fail on file content with complex characters
|
||||
- Suitable for simple text files and prototyping only
|
||||
|
||||
**Production Readiness**:
|
||||
For production use, the JSON handling should be replaced with a proper library:
|
||||
- Option 1: [nlohmann/json](https://github.com/nlohmann/json) - Header-only, modern C++
|
||||
- Option 2: [RapidJSON](https://github.com/Tencent/rapidjson) - Fast and lightweight
|
||||
- Option 3: [jsoncpp](https://github.com/open-source-parsers/jsoncpp) - Mature and stable
|
||||
|
||||
See `src/http_client.cpp` for TODO comments marking areas needing improvement.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Standard C++ library
|
||||
- libcurl (for HTTP client)
|
||||
- POSIX API (for file operations)
|
||||
|
||||
### Adding New Commands
|
||||
|
||||
1. Add command handler in `src/main.cpp`
|
||||
2. Implement command logic
|
||||
3. Update help text and README
|
||||
4. Add tests
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backend Connection Failed
|
||||
|
||||
```bash
|
||||
# Check backend is running
|
||||
curl http://localhost:8080/api/health
|
||||
|
||||
# Start backend if needed
|
||||
cd ../../backend
|
||||
./build/wizardmerge-cli
|
||||
```
|
||||
|
||||
### File Not Found
|
||||
|
||||
Ensure file paths are correct and files are readable:
|
||||
```bash
|
||||
ls -la base.txt ours.txt theirs.txt
|
||||
```
|
||||
|
||||
### Permission Denied
|
||||
|
||||
Check file permissions:
|
||||
```bash
|
||||
chmod +r base.txt ours.txt theirs.txt
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](../../LICENSE) for details.
|
||||
43
frontends/cli/include/file_utils.h
Normal file
43
frontends/cli/include/file_utils.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef FILE_UTILS_H
|
||||
#define FILE_UTILS_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @brief File utility functions
|
||||
*/
|
||||
class FileUtils {
|
||||
public:
|
||||
/**
|
||||
* @brief Read a file and split into lines
|
||||
* @param filePath Path to the file
|
||||
* @param lines Output vector of lines
|
||||
* @return true if successful, false on error
|
||||
*/
|
||||
static bool readLines(const std::string& filePath, std::vector<std::string>& lines);
|
||||
|
||||
/**
|
||||
* @brief Write lines to a file
|
||||
* @param filePath Path to the file
|
||||
* @param lines Vector of lines to write
|
||||
* @return true if successful, false on error
|
||||
*/
|
||||
static bool writeLines(const std::string& filePath, const std::vector<std::string>& lines);
|
||||
|
||||
/**
|
||||
* @brief Check if a file exists
|
||||
* @param filePath Path to the file
|
||||
* @return true if file exists, false otherwise
|
||||
*/
|
||||
static bool fileExists(const std::string& filePath);
|
||||
|
||||
/**
|
||||
* @brief Get file size in bytes
|
||||
* @param filePath Path to the file
|
||||
* @return File size, or -1 on error
|
||||
*/
|
||||
static long getFileSize(const std::string& filePath);
|
||||
};
|
||||
|
||||
#endif // FILE_UTILS_H
|
||||
62
frontends/cli/include/http_client.h
Normal file
62
frontends/cli/include/http_client.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#ifndef HTTP_CLIENT_H
|
||||
#define HTTP_CLIENT_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
/**
|
||||
* @brief HTTP client for communicating with WizardMerge backend
|
||||
*/
|
||||
class HttpClient {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct HTTP client with backend URL
|
||||
* @param backendUrl URL of the backend server (e.g., "http://localhost:8080")
|
||||
*/
|
||||
explicit HttpClient(const std::string& backendUrl);
|
||||
|
||||
/**
|
||||
* @brief Perform a three-way merge via backend API
|
||||
* @param base Base version lines
|
||||
* @param ours Our version lines
|
||||
* @param theirs Their version lines
|
||||
* @param merged Output merged lines
|
||||
* @param hasConflicts Output whether conflicts were detected
|
||||
* @return true if successful, false on error
|
||||
*/
|
||||
bool performMerge(
|
||||
const std::vector<std::string>& base,
|
||||
const std::vector<std::string>& ours,
|
||||
const std::vector<std::string>& theirs,
|
||||
std::vector<std::string>& merged,
|
||||
bool& hasConflicts
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Check if backend is reachable
|
||||
* @return true if backend responds, false otherwise
|
||||
*/
|
||||
bool checkBackend();
|
||||
|
||||
/**
|
||||
* @brief Get last error message
|
||||
* @return Error message string
|
||||
*/
|
||||
std::string getLastError() const { return lastError_; }
|
||||
|
||||
private:
|
||||
std::string backendUrl_;
|
||||
std::string lastError_;
|
||||
|
||||
/**
|
||||
* @brief Perform HTTP POST request
|
||||
* @param endpoint API endpoint (e.g., "/api/merge")
|
||||
* @param jsonBody JSON request body
|
||||
* @param response Output response string
|
||||
* @return true if successful, false on error
|
||||
*/
|
||||
bool post(const std::string& endpoint, const std::string& jsonBody, std::string& response);
|
||||
};
|
||||
|
||||
#endif // HTTP_CLIENT_H
|
||||
47
frontends/cli/src/file_utils.cpp
Normal file
47
frontends/cli/src/file_utils.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "file_utils.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <sys/stat.h>
|
||||
|
||||
bool FileUtils::readLines(const std::string& filePath, std::vector<std::string>& lines) {
|
||||
std::ifstream file(filePath);
|
||||
if (!file.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lines.clear();
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
lines.push_back(line);
|
||||
}
|
||||
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FileUtils::writeLines(const std::string& filePath, const std::vector<std::string>& lines) {
|
||||
std::ofstream file(filePath);
|
||||
if (!file.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& line : lines) {
|
||||
file << line << "\n";
|
||||
}
|
||||
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FileUtils::fileExists(const std::string& filePath) {
|
||||
struct stat buffer;
|
||||
return (stat(filePath.c_str(), &buffer) == 0);
|
||||
}
|
||||
|
||||
long FileUtils::getFileSize(const std::string& filePath) {
|
||||
struct stat buffer;
|
||||
if (stat(filePath.c_str(), &buffer) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return buffer.st_size;
|
||||
}
|
||||
142
frontends/cli/src/http_client.cpp
Normal file
142
frontends/cli/src/http_client.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "http_client.h"
|
||||
#include <curl/curl.h>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
|
||||
// Callback for libcurl to write response data
|
||||
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
((std::string*)userp)->append((char*)contents, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
HttpClient::HttpClient(const std::string& backendUrl)
|
||||
: backendUrl_(backendUrl), lastError_("") {
|
||||
}
|
||||
|
||||
bool HttpClient::post(const std::string& endpoint, const std::string& jsonBody, std::string& response) {
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
lastError_ = "Failed to initialize CURL";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string url = backendUrl_ + endpoint;
|
||||
response.clear();
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonBody.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
struct curl_slist* headers = nullptr;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
bool success = (res == CURLE_OK);
|
||||
if (!success) {
|
||||
lastError_ = std::string("CURL error: ") + curl_easy_strerror(res);
|
||||
}
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool HttpClient::performMerge(
|
||||
const std::vector<std::string>& base,
|
||||
const std::vector<std::string>& ours,
|
||||
const std::vector<std::string>& theirs,
|
||||
std::vector<std::string>& merged,
|
||||
bool& hasConflicts
|
||||
) {
|
||||
// Build JSON request
|
||||
// NOTE: This is a simplified JSON builder for prototype purposes.
|
||||
// LIMITATION: Does not escape special characters in strings (quotes, backslashes, etc.)
|
||||
// TODO: For production, use a proper JSON library like nlohmann/json or rapidjson
|
||||
// This implementation works for simple test cases but will fail with complex content.
|
||||
std::ostringstream json;
|
||||
json << "{";
|
||||
json << "\"base\":[";
|
||||
for (size_t i = 0; i < base.size(); ++i) {
|
||||
json << "\"" << base[i] << "\""; // WARNING: No escaping!
|
||||
if (i < base.size() - 1) json << ",";
|
||||
}
|
||||
json << "],";
|
||||
json << "\"ours\":[";
|
||||
for (size_t i = 0; i < ours.size(); ++i) {
|
||||
json << "\"" << ours[i] << "\""; // WARNING: No escaping!
|
||||
if (i < ours.size() - 1) json << ",";
|
||||
}
|
||||
json << "],";
|
||||
json << "\"theirs\":[";
|
||||
for (size_t i = 0; i < theirs.size(); ++i) {
|
||||
json << "\"" << theirs[i] << "\""; // WARNING: No escaping!
|
||||
if (i < theirs.size() - 1) json << ",";
|
||||
}
|
||||
json << "]";
|
||||
json << "}";
|
||||
|
||||
std::string response;
|
||||
if (!post("/api/merge", json.str(), response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse JSON response (simple parsing for now)
|
||||
// NOTE: This is a fragile string-based parser for prototype purposes.
|
||||
// LIMITATION: Will break on complex JSON or unexpected formatting.
|
||||
// TODO: For production, use a proper JSON library like nlohmann/json or rapidjson
|
||||
merged.clear();
|
||||
hasConflicts = (response.find("\"has_conflicts\":true") != std::string::npos);
|
||||
|
||||
// Extract merged lines from response
|
||||
// This is a simplified parser - production code MUST use a JSON library
|
||||
size_t mergedPos = response.find("\"merged\":");
|
||||
if (mergedPos != std::string::npos) {
|
||||
size_t startBracket = response.find("[", mergedPos);
|
||||
size_t endBracket = response.find("]", startBracket);
|
||||
if (startBracket != std::string::npos && endBracket != std::string::npos) {
|
||||
std::string mergedArray = response.substr(startBracket + 1, endBracket - startBracket - 1);
|
||||
|
||||
// Parse lines (simplified)
|
||||
size_t pos = 0;
|
||||
while (pos < mergedArray.size()) {
|
||||
size_t quoteStart = mergedArray.find("\"", pos);
|
||||
if (quoteStart == std::string::npos) break;
|
||||
size_t quoteEnd = mergedArray.find("\"", quoteStart + 1);
|
||||
if (quoteEnd == std::string::npos) break;
|
||||
|
||||
std::string line = mergedArray.substr(quoteStart + 1, quoteEnd - quoteStart - 1);
|
||||
merged.push_back(line);
|
||||
pos = quoteEnd + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HttpClient::checkBackend() {
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
lastError_ = "Failed to initialize CURL";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string url = backendUrl_ + "/";
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
bool success = (res == CURLE_OK);
|
||||
|
||||
if (!success) {
|
||||
lastError_ = std::string("Cannot reach backend: ") + curl_easy_strerror(res);
|
||||
}
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
return success;
|
||||
}
|
||||
395
frontends/cli/src/main.cpp
Normal file
395
frontends/cli/src/main.cpp
Normal file
@@ -0,0 +1,395 @@
|
||||
#include "http_client.h"
|
||||
#include "file_utils.h"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <curl/curl.h>
|
||||
|
||||
/**
|
||||
* @brief Print usage information
|
||||
*/
|
||||
void printUsage(const char* programName) {
|
||||
std::cout << "WizardMerge CLI Frontend - Intelligent Merge Conflict Resolution\n\n";
|
||||
std::cout << "Usage:\n";
|
||||
std::cout << " " << programName << " [OPTIONS] merge --base <file> --ours <file> --theirs <file>\n";
|
||||
std::cout << " " << programName << " [OPTIONS] pr-resolve --url <pr_url> [--token <token>]\n";
|
||||
std::cout << " " << programName << " [OPTIONS] git-resolve [FILE]\n";
|
||||
std::cout << " " << programName << " --help\n";
|
||||
std::cout << " " << programName << " --version\n\n";
|
||||
std::cout << "Global Options:\n";
|
||||
std::cout << " --backend <url> Backend server URL (default: http://localhost:8080)\n";
|
||||
std::cout << " -v, --verbose Enable verbose output\n";
|
||||
std::cout << " -q, --quiet Suppress non-error output\n";
|
||||
std::cout << " -h, --help Show this help message\n";
|
||||
std::cout << " --version Show version information\n\n";
|
||||
std::cout << "Commands:\n";
|
||||
std::cout << " merge Perform three-way merge\n";
|
||||
std::cout << " --base <file> Base version file (required)\n";
|
||||
std::cout << " --ours <file> Our version file (required)\n";
|
||||
std::cout << " --theirs <file> Their version file (required)\n";
|
||||
std::cout << " -o, --output <file> Output file (default: stdout)\n";
|
||||
std::cout << " --format <format> Output format: text, json (default: text)\n\n";
|
||||
std::cout << " pr-resolve Resolve pull request conflicts\n";
|
||||
std::cout << " --url <url> Pull request URL (required)\n";
|
||||
std::cout << " --token <token> GitHub API token (optional, can use GITHUB_TOKEN env)\n";
|
||||
std::cout << " --branch <name> Create branch with resolved conflicts (optional)\n";
|
||||
std::cout << " -o, --output <dir> Output directory for resolved files (default: stdout)\n\n";
|
||||
std::cout << " git-resolve Resolve Git merge conflicts (not yet implemented)\n";
|
||||
std::cout << " [FILE] Specific file to resolve (optional)\n\n";
|
||||
std::cout << "Examples:\n";
|
||||
std::cout << " " << programName << " merge --base base.txt --ours ours.txt --theirs theirs.txt\n";
|
||||
std::cout << " " << programName << " merge --base base.txt --ours ours.txt --theirs theirs.txt -o result.txt\n";
|
||||
std::cout << " " << programName << " pr-resolve --url https://github.com/owner/repo/pull/123\n";
|
||||
std::cout << " " << programName << " pr-resolve --url https://github.com/owner/repo/pull/123 --token ghp_xxx\n";
|
||||
std::cout << " " << programName << " --backend http://remote:8080 merge --base b.txt --ours o.txt --theirs t.txt\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print version information
|
||||
*/
|
||||
void printVersion() {
|
||||
std::cout << "WizardMerge CLI Frontend v1.0.0\n";
|
||||
std::cout << "Part of the WizardMerge Intelligent Merge Conflict Resolution system\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse command-line arguments and execute merge
|
||||
*/
|
||||
int main(int argc, char* argv[]) {
|
||||
// Default configuration
|
||||
std::string backendUrl = "http://localhost:8080";
|
||||
bool verbose = false;
|
||||
bool quiet = false;
|
||||
std::string command;
|
||||
std::string baseFile, oursFile, theirsFile, outputFile;
|
||||
std::string format = "text";
|
||||
std::string prUrl, githubToken, branchName;
|
||||
|
||||
// Check environment variable
|
||||
const char* envBackend = std::getenv("WIZARDMERGE_BACKEND");
|
||||
if (envBackend) {
|
||||
backendUrl = envBackend;
|
||||
}
|
||||
|
||||
// Check for GitHub token in environment
|
||||
const char* envToken = std::getenv("GITHUB_TOKEN");
|
||||
if (envToken) {
|
||||
githubToken = envToken;
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
printUsage(argv[0]);
|
||||
return 0;
|
||||
} else if (arg == "--version") {
|
||||
printVersion();
|
||||
return 0;
|
||||
} else if (arg == "--backend") {
|
||||
if (i + 1 < argc) {
|
||||
backendUrl = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --backend requires an argument\n";
|
||||
return 2;
|
||||
}
|
||||
} else if (arg == "--verbose" || arg == "-v") {
|
||||
verbose = true;
|
||||
} else if (arg == "--quiet" || arg == "-q") {
|
||||
quiet = true;
|
||||
} else if (arg == "merge") {
|
||||
command = "merge";
|
||||
} else if (arg == "pr-resolve") {
|
||||
command = "pr-resolve";
|
||||
} else if (arg == "git-resolve") {
|
||||
command = "git-resolve";
|
||||
} else if (arg == "--url") {
|
||||
if (i + 1 < argc) {
|
||||
prUrl = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --url requires an argument\n";
|
||||
return 2;
|
||||
}
|
||||
} else if (arg == "--token") {
|
||||
if (i + 1 < argc) {
|
||||
githubToken = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --token requires an argument\n";
|
||||
return 2;
|
||||
}
|
||||
} else if (arg == "--branch") {
|
||||
if (i + 1 < argc) {
|
||||
branchName = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --branch requires an argument\n";
|
||||
return 2;
|
||||
}
|
||||
} else if (arg == "--base") {
|
||||
if (i + 1 < argc) {
|
||||
baseFile = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --base requires an argument\n";
|
||||
return 2;
|
||||
}
|
||||
} else if (arg == "--ours") {
|
||||
if (i + 1 < argc) {
|
||||
oursFile = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --ours requires an argument\n";
|
||||
return 2;
|
||||
}
|
||||
} else if (arg == "--theirs") {
|
||||
if (i + 1 < argc) {
|
||||
theirsFile = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --theirs requires an argument\n";
|
||||
return 2;
|
||||
}
|
||||
} else if (arg == "--output" || arg == "-o") {
|
||||
if (i + 1 < argc) {
|
||||
outputFile = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --output requires an argument\n";
|
||||
return 2;
|
||||
}
|
||||
} else if (arg == "--format") {
|
||||
if (i + 1 < argc) {
|
||||
format = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --format requires an argument\n";
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if command was provided
|
||||
if (command.empty()) {
|
||||
std::cerr << "Error: No command specified\n\n";
|
||||
printUsage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Execute command
|
||||
if (command == "merge") {
|
||||
// Validate required arguments
|
||||
if (baseFile.empty() || oursFile.empty() || theirsFile.empty()) {
|
||||
std::cerr << "Error: merge command requires --base, --ours, and --theirs arguments\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Check files exist
|
||||
if (!FileUtils::fileExists(baseFile)) {
|
||||
std::cerr << "Error: Base file not found: " << baseFile << "\n";
|
||||
return 4;
|
||||
}
|
||||
if (!FileUtils::fileExists(oursFile)) {
|
||||
std::cerr << "Error: Ours file not found: " << oursFile << "\n";
|
||||
return 4;
|
||||
}
|
||||
if (!FileUtils::fileExists(theirsFile)) {
|
||||
std::cerr << "Error: Theirs file not found: " << theirsFile << "\n";
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
std::cout << "Backend URL: " << backendUrl << "\n";
|
||||
std::cout << "Base file: " << baseFile << "\n";
|
||||
std::cout << "Ours file: " << oursFile << "\n";
|
||||
std::cout << "Theirs file: " << theirsFile << "\n";
|
||||
}
|
||||
|
||||
// Read input files
|
||||
std::vector<std::string> baseLines, oursLines, theirsLines;
|
||||
if (!FileUtils::readLines(baseFile, baseLines)) {
|
||||
std::cerr << "Error: Failed to read base file\n";
|
||||
return 4;
|
||||
}
|
||||
if (!FileUtils::readLines(oursFile, oursLines)) {
|
||||
std::cerr << "Error: Failed to read ours file\n";
|
||||
return 4;
|
||||
}
|
||||
if (!FileUtils::readLines(theirsFile, theirsLines)) {
|
||||
std::cerr << "Error: Failed to read theirs file\n";
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
std::cout << "Read " << baseLines.size() << " lines from base\n";
|
||||
std::cout << "Read " << oursLines.size() << " lines from ours\n";
|
||||
std::cout << "Read " << theirsLines.size() << " lines from theirs\n";
|
||||
}
|
||||
|
||||
// Connect to backend and perform merge
|
||||
HttpClient client(backendUrl);
|
||||
|
||||
if (!quiet) {
|
||||
std::cout << "Connecting to backend: " << backendUrl << "\n";
|
||||
}
|
||||
|
||||
if (!client.checkBackend()) {
|
||||
std::cerr << "Error: Cannot connect to backend: " << client.getLastError() << "\n";
|
||||
std::cerr << "Make sure the backend server is running on " << backendUrl << "\n";
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
std::cout << "Performing three-way merge...\n";
|
||||
}
|
||||
|
||||
std::vector<std::string> mergedLines;
|
||||
bool hasConflicts = false;
|
||||
|
||||
if (!client.performMerge(baseLines, oursLines, theirsLines, mergedLines, hasConflicts)) {
|
||||
std::cerr << "Error: Merge failed: " << client.getLastError() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Output results
|
||||
if (!quiet) {
|
||||
std::cout << "Merge completed. Has conflicts: " << (hasConflicts ? "Yes" : "No") << "\n";
|
||||
std::cout << "Result has " << mergedLines.size() << " lines\n";
|
||||
}
|
||||
|
||||
// Write output
|
||||
if (outputFile.empty()) {
|
||||
// Write to stdout
|
||||
for (const auto& line : mergedLines) {
|
||||
std::cout << line << "\n";
|
||||
}
|
||||
} else {
|
||||
if (!FileUtils::writeLines(outputFile, mergedLines)) {
|
||||
std::cerr << "Error: Failed to write output file\n";
|
||||
return 4;
|
||||
}
|
||||
if (!quiet) {
|
||||
std::cout << "Output written to: " << outputFile << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return hasConflicts ? 5 : 0;
|
||||
|
||||
} else if (command == "pr-resolve") {
|
||||
// Validate required arguments
|
||||
if (prUrl.empty()) {
|
||||
std::cerr << "Error: pr-resolve command requires --url argument\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
std::cout << "Backend URL: " << backendUrl << "\n";
|
||||
std::cout << "Pull Request URL: " << prUrl << "\n";
|
||||
if (!githubToken.empty()) {
|
||||
std::cout << "Using GitHub token: " << githubToken.substr(0, 4) << "...\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to backend
|
||||
HttpClient client(backendUrl);
|
||||
|
||||
if (!quiet) {
|
||||
std::cout << "Connecting to backend: " << backendUrl << "\n";
|
||||
}
|
||||
|
||||
if (!client.checkBackend()) {
|
||||
std::cerr << "Error: Cannot connect to backend: " << client.getLastError() << "\n";
|
||||
std::cerr << "Make sure the backend server is running on " << backendUrl << "\n";
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
std::cout << "Resolving pull request conflicts...\n";
|
||||
}
|
||||
|
||||
// Build JSON request for PR resolution
|
||||
std::ostringstream json;
|
||||
json << "{";
|
||||
json << "\"pr_url\":\"" << prUrl << "\"";
|
||||
if (!githubToken.empty()) {
|
||||
json << ",\"github_token\":\"" << githubToken << "\"";
|
||||
}
|
||||
if (!branchName.empty()) {
|
||||
json << ",\"create_branch\":true";
|
||||
json << ",\"branch_name\":\"" << branchName << "\"";
|
||||
}
|
||||
json << "}";
|
||||
|
||||
// Perform HTTP POST to /api/pr/resolve
|
||||
std::string response;
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
std::cerr << "Error: Failed to initialize CURL\n";
|
||||
return 3;
|
||||
}
|
||||
|
||||
std::string url = backendUrl + "/api/pr/resolve";
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.str().c_str());
|
||||
|
||||
auto WriteCallback = [](void* contents, size_t size, size_t nmemb, void* userp) -> size_t {
|
||||
((std::string*)userp)->append((char*)contents, size * nmemb);
|
||||
return size * nmemb;
|
||||
};
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
struct curl_slist* headers = nullptr;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
std::cerr << "Error: Request failed: " << curl_easy_strerror(res) << "\n";
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
return 3;
|
||||
}
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
// Output response
|
||||
if (outputFile.empty()) {
|
||||
std::cout << "\n=== Pull Request Resolution Result ===\n";
|
||||
std::cout << response << "\n";
|
||||
} else {
|
||||
std::ofstream out(outputFile);
|
||||
if (!out) {
|
||||
std::cerr << "Error: Failed to write output file\n";
|
||||
return 4;
|
||||
}
|
||||
out << response;
|
||||
out.close();
|
||||
if (!quiet) {
|
||||
std::cout << "Result written to: " << outputFile << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Check if resolution was successful (simple check)
|
||||
if (response.find("\"success\":true") != std::string::npos) {
|
||||
if (!quiet) {
|
||||
std::cout << "\nPull request conflicts resolved successfully!\n";
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
if (!quiet) {
|
||||
std::cerr << "\nFailed to resolve some conflicts. See output for details.\n";
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
} else if (command == "git-resolve") {
|
||||
std::cerr << "Error: git-resolve command not yet implemented\n";
|
||||
return 1;
|
||||
} else {
|
||||
std::cerr << "Error: Unknown command: " << command << "\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
66
frontends/nextjs/README.md
Normal file
66
frontends/nextjs/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# WizardMerge Frontend
|
||||
|
||||
Next.js-based web frontend for WizardMerge.
|
||||
|
||||
## Runtime
|
||||
|
||||
- **Package Manager**: bun
|
||||
- **Framework**: Next.js 14
|
||||
- **Language**: TypeScript
|
||||
- **Styling**: Plain CSS (Tailwind CSS planned for future)
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```sh
|
||||
# Install bun
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```sh
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Run development server
|
||||
bun run dev
|
||||
|
||||
# Build for production
|
||||
bun run build
|
||||
|
||||
# Start production server
|
||||
bun run start
|
||||
```
|
||||
|
||||
The application will be available at http://localhost:3000
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── app/ # Next.js app directory
|
||||
│ ├── layout.tsx # Root layout
|
||||
│ ├── page.tsx # Home page
|
||||
│ └── globals.css # Global styles
|
||||
├── public/ # Static assets
|
||||
├── package.json # Dependencies
|
||||
├── tsconfig.json # TypeScript config
|
||||
└── next.config.js # Next.js config
|
||||
```
|
||||
|
||||
## Features (Planned)
|
||||
|
||||
- Three-panel diff viewer
|
||||
- Conflict resolution interface
|
||||
- Real-time collaboration
|
||||
- Syntax highlighting
|
||||
- Integration with C++ backend via REST API
|
||||
|
||||
## Scripts
|
||||
|
||||
- `bun run dev` - Start development server
|
||||
- `bun run build` - Build for production
|
||||
- `bun run start` - Start production server
|
||||
- `bun run lint` - Run ESLint
|
||||
165
frontends/nextjs/app/globals.css
Normal file
165
frontends/nextjs/app/globals.css
Normal file
@@ -0,0 +1,165 @@
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 255, 255, 255;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.bg-blue-50 {
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
|
||||
ul.list-disc {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
ul.list-inside {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.space-y-2 > * + * {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.max-w-6xl {
|
||||
max-width: 72rem;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
19
frontends/nextjs/app/layout.tsx
Normal file
19
frontends/nextjs/app/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'WizardMerge - Intelligent Merge Conflict Resolution',
|
||||
description: 'Resolve merge conflicts with intelligent dependency-aware algorithms',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
73
frontends/nextjs/app/page.tsx
Normal file
73
frontends/nextjs/app/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="min-h-screen p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h1 className="text-4xl font-bold mb-4">WizardMerge</h1>
|
||||
<p className="text-xl mb-8 text-gray-600">
|
||||
Intelligent Merge Conflict Resolution
|
||||
</p>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="border rounded-lg p-6">
|
||||
<h2 className="text-2xl font-semibold mb-3">Three-Way Merge</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Advanced merge algorithm with dependency analysis at text and LLVM-IR levels
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-sm">
|
||||
<li>28.85% reduction in conflict resolution time</li>
|
||||
<li>Merge suggestions for 70%+ of conflict blocks</li>
|
||||
<li>Smart auto-resolution patterns</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-6">
|
||||
<h2 className="text-2xl font-semibold mb-3">Visual Interface</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Clean, intuitive UI for reviewing and resolving conflicts
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-sm">
|
||||
<li>Three-panel diff view</li>
|
||||
<li>Syntax highlighting</li>
|
||||
<li>Keyboard shortcuts</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-6">
|
||||
<h2 className="text-2xl font-semibold mb-3">Git Integration</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Seamless integration with Git workflows
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-sm">
|
||||
<li>Detect and list conflicted files</li>
|
||||
<li>Mark files as resolved</li>
|
||||
<li>Command-line interface</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-6">
|
||||
<h2 className="text-2xl font-semibold mb-3">Smart Analysis</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Context-aware code understanding
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-sm">
|
||||
<li>Semantic merge for JSON, YAML, XML</li>
|
||||
<li>Language-aware merging (AST-based)</li>
|
||||
<li>Auto-resolution suggestions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-6 bg-blue-50 rounded-lg">
|
||||
<h3 className="text-xl font-semibold mb-2">Getting Started</h3>
|
||||
<p className="text-gray-700">
|
||||
WizardMerge is currently in active development. See the{' '}
|
||||
<a href="https://github.com/johndoe6345789/WizardMerge" className="text-blue-600 underline">
|
||||
GitHub repository
|
||||
</a>{' '}
|
||||
for roadmap and progress.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
6
frontends/nextjs/next.config.js
Normal file
6
frontends/nextjs/next.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
22
frontends/nextjs/package.json
Normal file
22
frontends/nextjs/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "wizardmerge-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^16.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
27
frontends/nextjs/tsconfig.json
Normal file
27
frontends/nextjs/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
78
frontends/qt6/CMakeLists.txt
Normal file
78
frontends/qt6/CMakeLists.txt
Normal file
@@ -0,0 +1,78 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(wizardmerge-qt6 VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Qt6 configuration
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# Find Qt6 packages
|
||||
find_package(Qt6 COMPONENTS Core Widgets Quick Network QUIET)
|
||||
|
||||
if(NOT Qt6_FOUND)
|
||||
message(WARNING "Qt6 not found. Skipping Qt6 frontend build.")
|
||||
message(WARNING "Install Qt6 to build the Qt6 frontend:")
|
||||
message(WARNING " - Ubuntu/Debian: sudo apt-get install qt6-base-dev qt6-declarative-dev")
|
||||
message(WARNING " - macOS: brew install qt@6")
|
||||
message(WARNING " - Windows: Download from https://www.qt.io/download")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Source files
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
# QML files
|
||||
set(QML_FILES
|
||||
qml/main.qml
|
||||
)
|
||||
|
||||
# Create executable
|
||||
qt_add_executable(wizardmerge-qt6
|
||||
${SOURCES}
|
||||
)
|
||||
|
||||
# Add QML module
|
||||
qt_add_qml_module(wizardmerge-qt6
|
||||
URI WizardMerge
|
||||
VERSION 1.0
|
||||
QML_FILES ${QML_FILES}
|
||||
)
|
||||
|
||||
# Link Qt libraries
|
||||
target_link_libraries(wizardmerge-qt6 PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Widgets
|
||||
Qt6::Quick
|
||||
Qt6::Network
|
||||
)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(wizardmerge-qt6 PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
# Install target
|
||||
install(TARGETS wizardmerge-qt6
|
||||
BUNDLE DESTINATION .
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
|
||||
# Platform-specific settings
|
||||
if(WIN32)
|
||||
set_target_properties(wizardmerge-qt6 PROPERTIES
|
||||
WIN32_EXECUTABLE TRUE
|
||||
)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(wizardmerge-qt6 PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
)
|
||||
endif()
|
||||
|
||||
message(STATUS "Qt6 frontend configured successfully")
|
||||
104
frontends/qt6/README.md
Normal file
104
frontends/qt6/README.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# WizardMerge Qt6 Frontend
|
||||
|
||||
Native desktop frontend for WizardMerge built with Qt6 and C++.
|
||||
|
||||
## Features
|
||||
|
||||
- Native desktop application for Linux, Windows, and macOS
|
||||
- Qt6 Widgets/QML-based UI
|
||||
- Direct integration with C++ backend
|
||||
- Offline capability
|
||||
- High performance
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Qt6 (6.2+)
|
||||
- CMake 3.16+
|
||||
- C++17 compiler (GCC 7+, Clang 6+, MSVC 2017+)
|
||||
- Ninja (recommended)
|
||||
|
||||
## Building
|
||||
|
||||
### Install Qt6
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt-get install qt6-base-dev qt6-declarative-dev
|
||||
```
|
||||
|
||||
**macOS (Homebrew):**
|
||||
```bash
|
||||
brew install qt@6
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
Download and install Qt6 from https://www.qt.io/download
|
||||
|
||||
### Build the Application
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
ninja
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
./wizardmerge-qt6
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
qt6/
|
||||
├── CMakeLists.txt # CMake build configuration
|
||||
├── README.md # This file
|
||||
├── src/ # C++ source files
|
||||
│ └── main.cpp # Application entry point
|
||||
├── qml/ # QML UI files
|
||||
│ └── main.qml # Main window UI
|
||||
└── include/ # Header files
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Architecture
|
||||
|
||||
The Qt6 frontend communicates with the WizardMerge C++ backend via:
|
||||
- Direct library linking (for standalone mode)
|
||||
- HTTP API calls (for client-server mode)
|
||||
|
||||
### UI Components
|
||||
|
||||
The UI is built using QML for declarative UI design:
|
||||
- Three-panel diff viewer
|
||||
- Conflict resolution controls
|
||||
- Syntax highlighting
|
||||
- File navigation
|
||||
|
||||
## Configuration
|
||||
|
||||
The application can be configured via command-line arguments:
|
||||
|
||||
```bash
|
||||
# Open a specific file
|
||||
./wizardmerge-qt6 /path/to/conflicted/file
|
||||
|
||||
# Connect to remote backend
|
||||
./wizardmerge-qt6 --backend-url http://localhost:8080
|
||||
|
||||
# Use standalone mode (embedded backend)
|
||||
./wizardmerge-qt6 --standalone
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Qt6 Core
|
||||
- Qt6 Widgets
|
||||
- Qt6 Quick (QML)
|
||||
- Qt6 Network (for HTTP client)
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](../../LICENSE) for details.
|
||||
227
frontends/qt6/qml/main.qml
Normal file
227
frontends/qt6/qml/main.qml
Normal file
@@ -0,0 +1,227 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
visible: true
|
||||
width: 1200
|
||||
height: 800
|
||||
title: "WizardMerge - Intelligent Merge Conflict Resolution"
|
||||
|
||||
// Properties exposed from C++
|
||||
property string backendUrl: ""
|
||||
property bool standalone: false
|
||||
property string initialFile: ""
|
||||
|
||||
header: ToolBar {
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Label {
|
||||
text: "WizardMerge"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Label {
|
||||
text: standalone ? "Standalone Mode" : "Client Mode"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
text: "Open File"
|
||||
onClicked: fileDialog.open()
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
text: "Settings"
|
||||
onClicked: settingsDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main content area
|
||||
SplitView {
|
||||
anchors.fill: parent
|
||||
orientation: Qt.Horizontal
|
||||
|
||||
// Left panel - Base version
|
||||
Rectangle {
|
||||
SplitView.preferredWidth: parent.width / 3
|
||||
color: "#f5f5f5"
|
||||
border.color: "#cccccc"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 5
|
||||
|
||||
Label {
|
||||
text: "Base Version"
|
||||
font.bold: true
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
TextArea {
|
||||
id: baseText
|
||||
readOnly: true
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
wrapMode: TextEdit.NoWrap
|
||||
placeholderText: "Base version will appear here..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Middle panel - Ours version
|
||||
Rectangle {
|
||||
SplitView.preferredWidth: parent.width / 3
|
||||
color: "#e8f5e9"
|
||||
border.color: "#4caf50"
|
||||
border.width: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 5
|
||||
|
||||
Label {
|
||||
text: "Ours (Current Branch)"
|
||||
font.bold: true
|
||||
font.pixelSize: 14
|
||||
color: "#2e7d32"
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
TextArea {
|
||||
id: oursText
|
||||
readOnly: true
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
wrapMode: TextEdit.NoWrap
|
||||
placeholderText: "Our version will appear here..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right panel - Theirs version
|
||||
Rectangle {
|
||||
SplitView.preferredWidth: parent.width / 3
|
||||
color: "#e3f2fd"
|
||||
border.color: "#2196f3"
|
||||
border.width: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 5
|
||||
|
||||
Label {
|
||||
text: "Theirs (Incoming Branch)"
|
||||
font.bold: true
|
||||
font.pixelSize: 14
|
||||
color: "#1565c0"
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
TextArea {
|
||||
id: theirsText
|
||||
readOnly: true
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
wrapMode: TextEdit.NoWrap
|
||||
placeholderText: "Their version will appear here..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status bar
|
||||
footer: ToolBar {
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Label {
|
||||
text: "Backend: " + backendUrl
|
||||
font.pixelSize: 10
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Label {
|
||||
text: initialFile !== "" ? "File: " + initialFile : "No file loaded"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Ready"
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File dialog (placeholder)
|
||||
Dialog {
|
||||
id: fileDialog
|
||||
title: "Open File"
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
Label {
|
||||
text: "File selection not yet implemented.\nUse command line: wizardmerge-qt6 <file>"
|
||||
}
|
||||
}
|
||||
|
||||
// Settings dialog (placeholder)
|
||||
Dialog {
|
||||
id: settingsDialog
|
||||
title: "Settings"
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 10
|
||||
|
||||
Label {
|
||||
text: "Backend URL:"
|
||||
}
|
||||
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
text: backendUrl
|
||||
placeholderText: "http://localhost:8080"
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Standalone Mode"
|
||||
checked: standalone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Component initialization
|
||||
Component.onCompleted: {
|
||||
console.log("WizardMerge Qt6 UI initialized")
|
||||
console.log("Backend URL:", backendUrl)
|
||||
console.log("Standalone:", standalone)
|
||||
console.log("Initial File:", initialFile)
|
||||
}
|
||||
}
|
||||
88
frontends/qt6/src/main.cpp
Normal file
88
frontends/qt6/src/main.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QCommandLineParser>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QUrl>
|
||||
#include <iostream>
|
||||
|
||||
/**
|
||||
* @brief Main entry point for WizardMerge Qt6 frontend
|
||||
*
|
||||
* This application provides a native desktop interface for WizardMerge,
|
||||
* supporting both standalone mode (with embedded backend) and client mode
|
||||
* (connecting to a remote backend server).
|
||||
*/
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QGuiApplication app(argc, argv);
|
||||
app.setApplicationName("WizardMerge");
|
||||
app.setApplicationVersion("1.0.0");
|
||||
app.setOrganizationName("WizardMerge");
|
||||
app.setOrganizationDomain("wizardmerge.dev");
|
||||
|
||||
// Command line parser
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription("WizardMerge - Intelligent Merge Conflict Resolution");
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
|
||||
QCommandLineOption backendUrlOption(
|
||||
QStringList() << "b" << "backend-url",
|
||||
"Backend server URL (default: http://localhost:8080)",
|
||||
"url",
|
||||
"http://localhost:8080"
|
||||
);
|
||||
parser.addOption(backendUrlOption);
|
||||
|
||||
QCommandLineOption standaloneOption(
|
||||
QStringList() << "s" << "standalone",
|
||||
"Run in standalone mode with embedded backend"
|
||||
);
|
||||
parser.addOption(standaloneOption);
|
||||
|
||||
parser.addPositionalArgument("file", "File to open (optional)");
|
||||
|
||||
parser.process(app);
|
||||
|
||||
// Get command line arguments
|
||||
QString backendUrl = parser.value(backendUrlOption);
|
||||
bool standalone = parser.isSet(standaloneOption);
|
||||
QStringList positionalArgs = parser.positionalArguments();
|
||||
QString filePath = positionalArgs.isEmpty() ? QString() : positionalArgs.first();
|
||||
|
||||
// Create QML engine
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
// Expose application settings to QML
|
||||
QQmlContext* rootContext = engine.rootContext();
|
||||
rootContext->setContextProperty("backendUrl", backendUrl);
|
||||
rootContext->setContextProperty("standalone", standalone);
|
||||
rootContext->setContextProperty("initialFile", filePath);
|
||||
|
||||
// Load main QML file
|
||||
const QUrl url(u"qrc:/qt/qml/WizardMerge/main.qml"_qs);
|
||||
|
||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
|
||||
&app, []() {
|
||||
std::cerr << "Error: Failed to load QML" << std::endl;
|
||||
QCoreApplication::exit(-1);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
engine.load(url);
|
||||
|
||||
if (engine.rootObjects().isEmpty()) {
|
||||
std::cerr << "Error: No root objects loaded from QML" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::cout << "WizardMerge Qt6 Frontend Started" << std::endl;
|
||||
std::cout << "Backend URL: " << backendUrl.toStdString() << std::endl;
|
||||
std::cout << "Standalone Mode: " << (standalone ? "Yes" : "No") << std::endl;
|
||||
if (!filePath.isEmpty()) {
|
||||
std::cout << "Opening file: " << filePath.toStdString() << std::endl;
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
6
renovate.json
Normal file
6
renovate.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
PyQt6>=6.6
|
||||
|
||||
# Optional: OCR dependencies for extracting text from documents
|
||||
# Uncomment if you need to run scripts/ocr_pages.py
|
||||
# pillow>=10.0
|
||||
# pytesseract>=0.3.10
|
||||
# System requirement: tesseract-ocr (install via: sudo apt-get install tesseract-ocr)
|
||||
@@ -1,303 +0,0 @@
|
||||
"""Extract image XObjects from wizardmerge.pdf and emit a JSON manifest.
|
||||
|
||||
The script avoids external dependencies so it can run in constrained environments.
|
||||
Flate-encoded images are converted into PNG byte streams, while DCT-encoded
|
||||
images are treated as JPEG. A companion ``images.json`` file captures every
|
||||
image's metadata, a lightweight content analysis, and a base64 payload without
|
||||
writing raw binaries to disk. Semantic file names are generated from the
|
||||
analysis (color, contrast, orientation) so the manifest is easier to navigate.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import struct
|
||||
import zlib
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
PDF_PATH = pathlib.Path("wizardmerge.pdf")
|
||||
OUTPUT_DIR = pathlib.Path("extracted_graphics")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageObject:
|
||||
"""Metadata and raw bytes for a single PDF image object."""
|
||||
|
||||
object_number: int
|
||||
width: int
|
||||
height: int
|
||||
color_space: str
|
||||
bits_per_component: int
|
||||
filter: str
|
||||
stream: bytes
|
||||
|
||||
@property
|
||||
def channels(self) -> int:
|
||||
if "/DeviceRGB" in self.color_space:
|
||||
return 3
|
||||
if "/DeviceGray" in self.color_space:
|
||||
return 1
|
||||
raise ValueError(f"Unsupported colorspace {self.color_space!r}")
|
||||
|
||||
|
||||
OBJECT_PATTERN = re.compile(rb"(\d+)\s+\d+\s+obj(.*?)endobj", re.DOTALL)
|
||||
|
||||
|
||||
def _extract_stream(obj_bytes: bytes) -> bytes:
|
||||
"""Return the raw stream bytes for a PDF object."""
|
||||
|
||||
stream_match = re.search(rb"stream\r?\n", obj_bytes)
|
||||
if not stream_match:
|
||||
raise ValueError("No stream found in object")
|
||||
|
||||
start = stream_match.end()
|
||||
length_match = re.search(rb"/Length\s+(\d+)", obj_bytes)
|
||||
if length_match:
|
||||
length = int(length_match.group(1))
|
||||
return obj_bytes[start : start + length]
|
||||
|
||||
end = obj_bytes.find(b"endstream", start)
|
||||
return obj_bytes[start:end].rstrip(b"\r\n")
|
||||
|
||||
|
||||
def iter_image_objects(pdf_bytes: bytes) -> Iterable[ImageObject]:
|
||||
"""Yield image objects discovered in the PDF payload."""
|
||||
|
||||
for match in OBJECT_PATTERN.finditer(pdf_bytes):
|
||||
obj_bytes = match.group(0)
|
||||
if b"/Subtype /Image" not in obj_bytes:
|
||||
continue
|
||||
|
||||
object_number = int(match.group(1))
|
||||
|
||||
def _lookup(name: bytes) -> Optional[str]:
|
||||
pattern = re.search(rb"/" + name + rb"\s+(/[^\s]+)", obj_bytes)
|
||||
return pattern.group(1).decode("ascii") if pattern else None
|
||||
|
||||
width_match = re.search(rb"/Width\s+(\d+)", obj_bytes)
|
||||
height_match = re.search(rb"/Height\s+(\d+)", obj_bytes)
|
||||
bits_match = re.search(rb"/BitsPerComponent\s+(\d+)", obj_bytes)
|
||||
|
||||
if not (width_match and height_match and bits_match):
|
||||
raise ValueError(f"Image {object_number} missing dimension metadata")
|
||||
|
||||
image = ImageObject(
|
||||
object_number=object_number,
|
||||
width=int(width_match.group(1)),
|
||||
height=int(height_match.group(1)),
|
||||
color_space=_lookup(b"ColorSpace") or "/DeviceRGB",
|
||||
bits_per_component=int(bits_match.group(1)),
|
||||
filter=_lookup(b"Filter") or "",
|
||||
stream=_extract_stream(obj_bytes),
|
||||
)
|
||||
yield image
|
||||
|
||||
|
||||
def _png_chunk(tag: bytes, payload: bytes) -> bytes:
|
||||
length = struct.pack(">I", len(payload))
|
||||
crc = struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF)
|
||||
return length + tag + payload + crc
|
||||
|
||||
|
||||
def _dominant_color_label(means: Tuple[float, ...]) -> str:
|
||||
"""Return a coarse color label from per-channel means."""
|
||||
|
||||
if len(means) == 1:
|
||||
gray = means[0]
|
||||
if gray < 16:
|
||||
return "black"
|
||||
if gray < 64:
|
||||
return "dark-gray"
|
||||
if gray < 160:
|
||||
return "mid-gray"
|
||||
if gray < 224:
|
||||
return "light-gray"
|
||||
return "white"
|
||||
|
||||
red, green, blue = means
|
||||
brightness = (red + green + blue) / 3
|
||||
if max(red, green, blue) - min(red, green, blue) < 12:
|
||||
# Essentially grayscale
|
||||
return _dominant_color_label((brightness,))
|
||||
|
||||
dominant_channel = max(range(3), key=lambda idx: (red, green, blue)[idx])
|
||||
channel_names = {0: "red", 1: "green", 2: "blue"}
|
||||
brightness_label = _dominant_color_label((brightness,))
|
||||
return f"{brightness_label}-{channel_names[dominant_channel]}"
|
||||
|
||||
|
||||
def _orientation_tag(width: int, height: int) -> str:
|
||||
if width == height:
|
||||
return "square"
|
||||
if width > height:
|
||||
return "landscape"
|
||||
return "portrait"
|
||||
|
||||
|
||||
def analyse_flate_image(image: ImageObject) -> Dict[str, object]:
|
||||
"""Compute basic color statistics for a Flate-decoded image."""
|
||||
|
||||
raw = zlib.decompress(image.stream)
|
||||
row_stride = image.width * image.channels
|
||||
expected_size = row_stride * image.height
|
||||
if len(raw) != expected_size:
|
||||
raise ValueError(
|
||||
f"Unexpected data length for image {image.object_number}: "
|
||||
f"got {len(raw)}, expected {expected_size}"
|
||||
)
|
||||
|
||||
channel_stats = [
|
||||
{"count": 0, "mean": 0.0, "m2": 0.0, "min": 255, "max": 0}
|
||||
for _ in range(image.channels)
|
||||
]
|
||||
palette: set[Tuple[int, ...]] = set()
|
||||
palette_limit = 1024
|
||||
|
||||
for idx in range(0, len(raw), image.channels):
|
||||
for channel in range(image.channels):
|
||||
value = raw[idx + channel]
|
||||
stats = channel_stats[channel]
|
||||
stats["count"] += 1
|
||||
delta = value - stats["mean"]
|
||||
stats["mean"] += delta / stats["count"]
|
||||
stats["m2"] += delta * (value - stats["mean"])
|
||||
stats["min"] = min(stats["min"], value)
|
||||
stats["max"] = max(stats["max"], value)
|
||||
|
||||
if len(palette) < palette_limit:
|
||||
if image.channels == 1:
|
||||
palette.add((raw[idx],))
|
||||
else:
|
||||
palette.add(tuple(raw[idx : idx + image.channels]))
|
||||
|
||||
means = tuple(stat["mean"] for stat in channel_stats)
|
||||
variances = tuple(stat["m2"] / max(stat["count"], 1) for stat in channel_stats)
|
||||
palette_size = len(palette) if len(palette) < palette_limit else None
|
||||
primary_color = _dominant_color_label(means)
|
||||
|
||||
return {
|
||||
"means": means,
|
||||
"variances": variances,
|
||||
"min": tuple(stat["min"] for stat in channel_stats),
|
||||
"max": tuple(stat["max"] for stat in channel_stats),
|
||||
"palette_size": palette_size,
|
||||
"primary_color": primary_color,
|
||||
"orientation": _orientation_tag(image.width, image.height),
|
||||
}
|
||||
|
||||
|
||||
def semantic_name(image: ImageObject, mime: str, analysis: Optional[Dict[str, object]]) -> str:
|
||||
"""Generate a more meaningful file name based on image analysis."""
|
||||
|
||||
extension = "png" if mime == "image/png" else "jpg"
|
||||
base_parts = []
|
||||
|
||||
if analysis:
|
||||
palette_size = analysis.get("palette_size")
|
||||
variances: Tuple[float, ...] = analysis.get("variances", ()) # type: ignore[assignment]
|
||||
variance_score = sum(variances) / max(len(variances), 1)
|
||||
primary_color = analysis.get("primary_color") or "unknown"
|
||||
base_parts.append(primary_color)
|
||||
|
||||
if palette_size == 1:
|
||||
base_parts.append("solid")
|
||||
elif palette_size and palette_size <= 4:
|
||||
base_parts.append("two-tone")
|
||||
elif variance_score < 400:
|
||||
base_parts.append("low-contrast")
|
||||
else:
|
||||
base_parts.append("detailed")
|
||||
|
||||
base_parts.append(str(analysis.get("orientation", "unknown")))
|
||||
else:
|
||||
base_parts.extend(["jpeg", _orientation_tag(image.width, image.height)])
|
||||
|
||||
base_parts.append(f"{image.width}x{image.height}")
|
||||
base_parts.append(f"obj{image.object_number}")
|
||||
return "-".join(base_parts) + f".{extension}"
|
||||
|
||||
|
||||
def raw_to_png(image: ImageObject) -> tuple[bytes, Dict[str, object]]:
|
||||
"""Convert a Flate-encoded image stream to PNG bytes and analysis."""
|
||||
|
||||
if image.bits_per_component != 8:
|
||||
raise ValueError(f"Unsupported bit depth: {image.bits_per_component}")
|
||||
|
||||
analysis = analyse_flate_image(image)
|
||||
|
||||
raw = zlib.decompress(image.stream)
|
||||
row_stride = image.width * image.channels
|
||||
filtered = b"".join(
|
||||
b"\x00" + raw[i : i + row_stride] for i in range(0, len(raw), row_stride)
|
||||
)
|
||||
|
||||
color_type = 2 if image.channels == 3 else 0
|
||||
ihdr = struct.pack(
|
||||
">IIBBBBB", image.width, image.height, 8, color_type, 0, 0, 0
|
||||
)
|
||||
png = b"\x89PNG\r\n\x1a\n"
|
||||
png += _png_chunk(b"IHDR", ihdr)
|
||||
png += _png_chunk(b"IDAT", zlib.compress(filtered))
|
||||
png += _png_chunk(b"IEND", b"")
|
||||
return png, analysis
|
||||
|
||||
|
||||
def save_images(images: List[ImageObject]) -> None:
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
manifest: List[dict[str, object]] = []
|
||||
errors: List[str] = []
|
||||
|
||||
for index, image in enumerate(sorted(images, key=lambda im: im.object_number), start=1):
|
||||
analysis: Optional[Dict[str, object]] = None
|
||||
|
||||
try:
|
||||
if image.filter == "/FlateDecode":
|
||||
raw_bytes, analysis = raw_to_png(image)
|
||||
mime = "image/png"
|
||||
elif image.filter == "/DCTDecode":
|
||||
raw_bytes = image.stream
|
||||
mime = "image/jpeg"
|
||||
else:
|
||||
raise ValueError(f"Unsupported filter {image.filter}")
|
||||
except Exception as exc: # noqa: BLE001 - surface helpful error context
|
||||
placeholder = f"obj{image.object_number}"
|
||||
errors.append(f"{placeholder}: {exc}")
|
||||
print(f"Skipping {placeholder}: {exc}")
|
||||
continue
|
||||
|
||||
name = semantic_name(image, mime, analysis)
|
||||
encoded = base64.b64encode(raw_bytes).decode("ascii")
|
||||
manifest.append(
|
||||
{
|
||||
"name": name,
|
||||
"object_number": image.object_number,
|
||||
"width": image.width,
|
||||
"height": image.height,
|
||||
"color_space": image.color_space,
|
||||
"bits_per_component": image.bits_per_component,
|
||||
"mime": mime,
|
||||
"base64": encoded,
|
||||
"analysis": analysis,
|
||||
}
|
||||
)
|
||||
print(f"Captured {name} ({image.width}x{image.height}, {mime})")
|
||||
|
||||
images_path = OUTPUT_DIR / "images.json"
|
||||
images_path.write_text(json.dumps(manifest, indent=2))
|
||||
if errors:
|
||||
(OUTPUT_DIR / "errors.txt").write_text("\n".join(errors))
|
||||
print(f"Encountered errors for {len(errors)} image(s); see errors.txt")
|
||||
print(f"Wrote JSON manifest to {images_path}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
pdf_bytes = PDF_PATH.read_bytes()
|
||||
images = list(iter_image_objects(pdf_bytes))
|
||||
save_images(images)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Install Python dependencies system-wide or in the active environment.
|
||||
set -eu
|
||||
|
||||
PYTHON_BIN=${PYTHON_BIN:-python3}
|
||||
|
||||
"$PYTHON_BIN" -m pip install --upgrade pip
|
||||
"$PYTHON_BIN" -m pip install -r requirements.txt
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract text from page images using OCR and save as a markdown document.
|
||||
|
||||
Dependencies:
|
||||
pip install pillow pytesseract
|
||||
|
||||
System requirements:
|
||||
tesseract-ocr (install via: sudo apt-get install tesseract-ocr)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import pytesseract
|
||||
from PIL import Image
|
||||
|
||||
def ocr_pages(pages_dir: Path, output_file: Path) -> None:
|
||||
"""Perform OCR on all page images and create a single document."""
|
||||
|
||||
pages_dir = pages_dir.resolve()
|
||||
if not pages_dir.exists():
|
||||
raise FileNotFoundError(f"Pages directory not found: {pages_dir}")
|
||||
|
||||
# Get all PNG files sorted by number
|
||||
def get_page_number(path: Path) -> int:
|
||||
"""Extract page number from filename, defaulting to 0 if not found."""
|
||||
try:
|
||||
return int(path.stem.split("_")[-1])
|
||||
except (ValueError, IndexError):
|
||||
return 0
|
||||
|
||||
image_files = sorted(pages_dir.glob("*.png"), key=get_page_number)
|
||||
|
||||
if not image_files:
|
||||
raise ValueError(f"No PNG files found in {pages_dir}")
|
||||
|
||||
print(f"Found {len(image_files)} page images to process...")
|
||||
|
||||
full_text = []
|
||||
full_text.append("# WizardMerge Research Paper\n")
|
||||
full_text.append("*Extracted via OCR from paper pages*\n\n")
|
||||
full_text.append("---\n\n")
|
||||
|
||||
for idx, image_file in enumerate(image_files, start=1):
|
||||
print(f"Processing page {idx}/{len(image_files)}: {image_file.name}")
|
||||
|
||||
try:
|
||||
# Open image and perform OCR
|
||||
img = Image.open(image_file)
|
||||
text = pytesseract.image_to_string(img)
|
||||
|
||||
# Add page separator and text
|
||||
full_text.append(f"## Page {idx}\n\n")
|
||||
full_text.append(text.strip())
|
||||
full_text.append("\n\n---\n\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f" Error processing {image_file.name}: {e}")
|
||||
full_text.append(f"## Page {idx}\n\n")
|
||||
full_text.append(f"*[OCR Error: {e}]*\n\n")
|
||||
full_text.append("---\n\n")
|
||||
|
||||
# Write output
|
||||
output_file.write_text("".join(full_text))
|
||||
print(f"\nOCR complete! Output written to: {output_file}")
|
||||
print(f"Total pages processed: {len(image_files)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pages_dir = Path(__file__).parent.parent / "docs" / "pages"
|
||||
output_file = Path(__file__).parent.parent / "docs" / "PAPER.md"
|
||||
|
||||
ocr_pages(pages_dir, output_file)
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Launch the WizardMerge GUI using the local virtual environment when present.
|
||||
set -eu
|
||||
|
||||
VENV_DIR=${VENV_DIR:-.venv}
|
||||
PYTHON_BIN=${PYTHON_BIN:-python3}
|
||||
|
||||
if [ -d "$VENV_DIR" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$VENV_DIR/bin/activate"
|
||||
fi
|
||||
|
||||
exec "$PYTHON_BIN" -m wizardmerge.app "$@"
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Prepare a local virtual environment and install dependencies.
|
||||
set -eu
|
||||
|
||||
VENV_DIR=${VENV_DIR:-.venv}
|
||||
PYTHON_BIN=${PYTHON_BIN:-python3}
|
||||
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
. "$VENV_DIR/bin/activate"
|
||||
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo "Environment ready. Activate with: . $VENV_DIR/bin/activate"
|
||||
@@ -1,286 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unified TLC helper: replaces bootstrap.{sh,ps1} and run-tlc.{sh,ps1}.
|
||||
|
||||
Subcommands:
|
||||
- bootstrap : download tla2tools.jar into tools/ (or custom dir)
|
||||
- run : ensure jar exists, then run TLC and tee output to log
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List
|
||||
from urllib.request import urlopen
|
||||
|
||||
|
||||
DEFAULT_VERSION = "1.8.0"
|
||||
DEFAULT_TOOLS_DIR = "tools"
|
||||
DEFAULT_RESULTS_DIR = "ci-results"
|
||||
DEFAULT_MODULE = "STLRepairAlgo"
|
||||
DEFAULT_CONFIG = "models/STLRepairAlgo.cfg"
|
||||
DEFAULT_SPEC_DIR = "spec"
|
||||
DEFAULT_JAVA = "java"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="TLA+ TLC helper (bootstrap + run) in one script."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--tools-dir",
|
||||
default=DEFAULT_TOOLS_DIR,
|
||||
help=f"Directory for tla2tools.jar (default: {DEFAULT_TOOLS_DIR!r}).",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
default=DEFAULT_VERSION,
|
||||
help=f"TLA+ tools version tag (default: {DEFAULT_VERSION!r}).",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--url-template",
|
||||
default=(
|
||||
"https://github.com/tlaplus/tlaplus/releases/"
|
||||
"download/v{version}/tla2tools.jar"
|
||||
),
|
||||
help="Template URL for tla2tools.jar; {version} will be substituted.",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
dest="command",
|
||||
required=True,
|
||||
help="Subcommands.",
|
||||
)
|
||||
|
||||
bootstrap = subparsers.add_parser(
|
||||
"bootstrap",
|
||||
help="Download tla2tools.jar into tools-dir if missing.",
|
||||
)
|
||||
|
||||
bootstrap.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Re-download even if tla2tools.jar already exists.",
|
||||
)
|
||||
|
||||
run_p = subparsers.add_parser(
|
||||
"run",
|
||||
help="Run TLC on a TLA+ module, teeing output to a log file.",
|
||||
)
|
||||
|
||||
run_p.add_argument(
|
||||
"module",
|
||||
nargs="?",
|
||||
default=DEFAULT_MODULE,
|
||||
help=f"TLA+ module name without .tla (default: {DEFAULT_MODULE!r}).",
|
||||
)
|
||||
|
||||
run_p.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
default=DEFAULT_CONFIG,
|
||||
help=f"Path to TLC config file (default: {DEFAULT_CONFIG!r}).",
|
||||
)
|
||||
|
||||
run_p.add_argument(
|
||||
"--spec-dir",
|
||||
default=DEFAULT_SPEC_DIR,
|
||||
help=f"Directory containing .tla specs (default: {DEFAULT_SPEC_DIR!r}).",
|
||||
)
|
||||
|
||||
run_p.add_argument(
|
||||
"--results-dir",
|
||||
default=DEFAULT_RESULTS_DIR,
|
||||
help=f"Directory for TLC log files (default: {DEFAULT_RESULTS_DIR!r}).",
|
||||
)
|
||||
|
||||
run_p.add_argument(
|
||||
"--java",
|
||||
default=DEFAULT_JAVA,
|
||||
help=f"Java executable (default: {DEFAULT_JAVA!r}).",
|
||||
)
|
||||
|
||||
run_p.add_argument(
|
||||
"--extra-java-arg",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="ARG",
|
||||
help="Extra argument to pass to Java (can be repeated).",
|
||||
)
|
||||
|
||||
run_p.add_argument(
|
||||
"--no-bootstrap",
|
||||
action="store_true",
|
||||
help="Skip automatic bootstrap before running TLC.",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def ensure_dir(path: Path) -> None:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def build_jar_url(version: str, url_template: str) -> str:
|
||||
return url_template.format(version=version)
|
||||
|
||||
|
||||
def download_tla_tools(url: str, target: Path, overwrite: bool = False) -> None:
|
||||
if target.exists() and not overwrite:
|
||||
print(f"tla2tools.jar already present at {target}.")
|
||||
return
|
||||
|
||||
ensure_dir(target.parent)
|
||||
tmp = target.with_suffix(target.suffix + ".tmp")
|
||||
|
||||
print(f"Downloading tla2tools.jar from {url} ...")
|
||||
try:
|
||||
with urlopen(url) as resp, tmp.open("wb") as f:
|
||||
chunk = resp.read(8192)
|
||||
while chunk:
|
||||
f.write(chunk)
|
||||
chunk = resp.read(8192)
|
||||
except Exception as exc:
|
||||
if tmp.exists():
|
||||
tmp.unlink()
|
||||
raise SystemExit(f"Failed to download tla2tools.jar: {exc}") from exc
|
||||
|
||||
tmp.replace(target)
|
||||
target.chmod(0o644)
|
||||
print(f"Saved tla2tools.jar to {target}.")
|
||||
|
||||
|
||||
def ensure_java_available(java_exe: str) -> None:
|
||||
if shutil.which(java_exe) is None:
|
||||
raise SystemExit(
|
||||
f"Java executable {java_exe!r} not found in PATH. "
|
||||
"Install Java or pass --java with a full path."
|
||||
)
|
||||
|
||||
|
||||
def tee_process_output(
|
||||
proc: subprocess.Popen,
|
||||
log_path: Path,
|
||||
) -> int:
|
||||
ensure_dir(log_path.parent)
|
||||
with log_path.open("w", encoding="utf-8", errors="replace") as log:
|
||||
assert proc.stdout is not None
|
||||
for line in proc.stdout:
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
log.write(line)
|
||||
log.flush()
|
||||
return proc.wait()
|
||||
|
||||
|
||||
def build_tlc_command(
|
||||
java_exe: str,
|
||||
extra_java_args: Iterable[str],
|
||||
jar_path: Path,
|
||||
cfg_path: Path,
|
||||
module_path: Path,
|
||||
) -> List[str]:
|
||||
cmd: List[str] = [java_exe]
|
||||
cmd.extend(extra_java_args)
|
||||
cmd.extend(
|
||||
[
|
||||
"-cp",
|
||||
str(jar_path),
|
||||
"tlc2.TLC",
|
||||
"-config",
|
||||
str(cfg_path),
|
||||
str(module_path),
|
||||
]
|
||||
)
|
||||
return cmd
|
||||
|
||||
|
||||
def run_tlc(
|
||||
java_exe: str,
|
||||
extra_java_args: Iterable[str],
|
||||
tools_dir: Path,
|
||||
spec_dir: Path,
|
||||
module: str,
|
||||
cfg: Path,
|
||||
results_dir: Path,
|
||||
) -> int:
|
||||
ensure_java_available(java_exe)
|
||||
|
||||
jar_path = tools_dir / "tla2tools.jar"
|
||||
if not jar_path.exists():
|
||||
raise SystemExit(
|
||||
f"{jar_path} does not exist. Run with 'bootstrap' first "
|
||||
"or omit --no-bootstrap on the 'run' command."
|
||||
)
|
||||
|
||||
module_path = spec_dir / f"{module}.tla"
|
||||
if not module_path.exists():
|
||||
raise SystemExit(f"Spec file not found: {module_path}")
|
||||
|
||||
cfg_path = cfg
|
||||
if not cfg_path.exists():
|
||||
raise SystemExit(f"Config file not found: {cfg_path}")
|
||||
|
||||
ensure_dir(results_dir)
|
||||
log_path = results_dir / f"{module}.tlc.log"
|
||||
|
||||
cmd = build_tlc_command(
|
||||
java_exe=java_exe,
|
||||
extra_java_args=list(extra_java_args),
|
||||
jar_path=jar_path,
|
||||
cfg_path=cfg_path,
|
||||
module_path=module_path,
|
||||
)
|
||||
|
||||
print("Running TLC with command:")
|
||||
print(" " + " ".join(str(c) for c in cmd))
|
||||
print(f"Logging output to {log_path}")
|
||||
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True,
|
||||
)
|
||||
return tee_process_output(proc, log_path)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
tools_dir = Path(args.tools_dir)
|
||||
url = build_jar_url(args.version, args.url_template)
|
||||
jar_target = tools_dir / "tla2tools.jar"
|
||||
|
||||
if args.command == "bootstrap":
|
||||
download_tla_tools(url, jar_target, overwrite=args.force)
|
||||
return
|
||||
|
||||
if args.command == "run":
|
||||
if not args.no_bootstrap:
|
||||
download_tla_tools(url, jar_target, overwrite=False)
|
||||
|
||||
exit_code = run_tlc(
|
||||
java_exe=args.java,
|
||||
extra_java_args=args.extra_java_arg,
|
||||
tools_dir=tools_dir,
|
||||
spec_dir=Path(args.spec_dir),
|
||||
module=args.module,
|
||||
cfg=Path(args.config),
|
||||
results_dir=Path(args.results_dir),
|
||||
)
|
||||
raise SystemExit(exit_code)
|
||||
|
||||
raise SystemExit("No command given; use --help for usage.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +1,61 @@
|
||||
------------------------------- MODULE WizardMergeSpec -------------------------------
|
||||
EXTENDS Naturals, FiniteSets
|
||||
|
||||
(*
|
||||
Implementation Status (as of December 2024):
|
||||
|
||||
This formal specification describes the dependency-aware merge algorithm that
|
||||
WizardMerge aims to implement. The current implementation status is:
|
||||
|
||||
IMPLEMENTED (Phase 1.1):
|
||||
- Basic three-way merge algorithm (C++ backend)
|
||||
- Line-level conflict detection
|
||||
- Auto-resolution for common patterns:
|
||||
* Non-overlapping changes
|
||||
* Identical changes from both sides
|
||||
* Whitespace-only differences
|
||||
- Command-line interface (wizardmerge-cli)
|
||||
- Pull request/merge request URL processing and conflict resolution:
|
||||
* Parse GitHub PR URLs and GitLab MR URLs
|
||||
* Fetch PR/MR data via GitHub and GitLab APIs
|
||||
* Apply merge algorithm to PR/MR files
|
||||
* HTTP API endpoint for PR/MR resolution
|
||||
* Support for multiple git platforms (GitHub and GitLab)
|
||||
|
||||
NOT YET IMPLEMENTED (Future phases):
|
||||
- Dependency graph construction (SDG analysis)
|
||||
- LLVM-IR level analysis
|
||||
- Edge classification (safe vs. violated)
|
||||
- Fine-grained DCB (Definition-Code Block) tracking
|
||||
- Mirror mapping and matching
|
||||
- Git branch creation for resolved PRs/MRs
|
||||
- Support for additional platforms (Bitbucket, etc.)
|
||||
|
||||
The current implementation in backend/src/merge/three_way_merge.cpp provides
|
||||
a foundation for the full dependency-aware algorithm specified here. Future
|
||||
phases will enhance it with the SDG analysis, edge classification, and
|
||||
dependency-aware conflict resolution described in this specification.
|
||||
|
||||
PR/MR Resolution Workflow (Phase 1.2):
|
||||
The PR/MR resolution feature extends the core merge algorithm to work with
|
||||
both GitHub pull requests and GitLab merge requests. The workflow is:
|
||||
1. Accept PR/MR URL: Parse URL to detect platform and extract owner, repo, and number
|
||||
2. Fetch PR/MR metadata: Use platform-specific API to retrieve information
|
||||
3. Fetch file versions: Retrieve base and head versions of modified files
|
||||
4. Apply merge algorithm: For each file, perform three-way merge
|
||||
5. Auto-resolve conflicts: Apply heuristic resolution where possible
|
||||
6. Return results: Provide merged content and conflict status
|
||||
|
||||
Platform Support:
|
||||
- GitHub: Uses GitHub API v3 with "Authorization: token" header
|
||||
- GitLab: Uses GitLab API v4 with "PRIVATE-TOKEN" header
|
||||
- Both platforms support public and private repositories with proper authentication
|
||||
|
||||
This workflow enables batch processing of PR/MR conflicts using the same
|
||||
dependency-aware merge principles, with future integration planned for
|
||||
automatic branch creation and PR/MR updates.
|
||||
*)
|
||||
|
||||
(*
|
||||
High-level intent
|
||||
|
||||
@@ -288,4 +343,112 @@ Inv ==
|
||||
|
||||
THEOREM Spec => []Inv
|
||||
|
||||
(***************************************************************************)
|
||||
(* Pull Request/Merge Request Resolution Specification (Phase 1.2) *)
|
||||
(***************************************************************************)
|
||||
|
||||
(*
|
||||
This section extends the core merge specification to model the PR/MR resolution
|
||||
workflow. It describes how WizardMerge processes GitHub pull requests and
|
||||
GitLab merge requests to identify and resolve conflicts across multiple files.
|
||||
|
||||
Supported Platforms:
|
||||
- GitHub: Uses "pull request" terminology with "/pull/" URL path
|
||||
- GitLab: Uses "merge request" terminology with "/-/merge_requests/" URL path
|
||||
*)
|
||||
|
||||
CONSTANTS
|
||||
(*
|
||||
GitPlatform: the platform type - GitHub or GitLab
|
||||
*)
|
||||
GitPlatform,
|
||||
|
||||
(*
|
||||
PR_FILES: the set of all files in the pull/merge request
|
||||
*)
|
||||
PR_FILES,
|
||||
|
||||
(*
|
||||
FileStatus: maps each file to its modification status in the PR/MR
|
||||
Possible values: "modified", "added", "removed", "renamed"
|
||||
*)
|
||||
FileStatus,
|
||||
|
||||
(*
|
||||
BaseSHA, HeadSHA: commit identifiers for base and head of the PR/MR
|
||||
*)
|
||||
BaseSHA, HeadSHA
|
||||
|
||||
(*
|
||||
Platform types - GitHub uses pull requests, GitLab uses merge requests
|
||||
*)
|
||||
ASSUME GitPlatform \in {"GitHub", "GitLab"}
|
||||
|
||||
(*
|
||||
A file is resolvable if it was modified (not removed) and we can fetch
|
||||
both its base and head versions.
|
||||
*)
|
||||
Resolvable(f) ==
|
||||
FileStatus[f] \in {"modified", "added"}
|
||||
|
||||
(*
|
||||
PR_MergeResult: for each file f in PR_FILES, we compute a merge result
|
||||
using the three-way merge algorithm. This is a function from PR_FILES
|
||||
to merge outcomes.
|
||||
|
||||
Possible outcomes:
|
||||
- "success": file merged without conflicts
|
||||
- "conflict": file has unresolved conflicts
|
||||
- "error": failed to fetch or process file
|
||||
- "skipped": file was removed or not applicable
|
||||
*)
|
||||
|
||||
VARIABLE PR_MergeResults
|
||||
|
||||
PR_Init ==
|
||||
PR_MergeResults = [f \in PR_FILES |-> "pending"]
|
||||
|
||||
(*
|
||||
Process a single file by applying the three-way merge algorithm.
|
||||
This abstracts the actual merge computation but captures the key decision:
|
||||
whether the file can be auto-resolved or requires manual intervention.
|
||||
*)
|
||||
ProcessFile(f) ==
|
||||
/\ PR_MergeResults[f] = "pending"
|
||||
/\ IF ~Resolvable(f)
|
||||
THEN PR_MergeResults' = [PR_MergeResults EXCEPT ![f] = "skipped"]
|
||||
ELSE \/ PR_MergeResults' = [PR_MergeResults EXCEPT ![f] = "success"]
|
||||
\/ PR_MergeResults' = [PR_MergeResults EXCEPT ![f] = "conflict"]
|
||||
\/ PR_MergeResults' = [PR_MergeResults EXCEPT ![f] = "error"]
|
||||
|
||||
(*
|
||||
PR completion: all files have been processed
|
||||
*)
|
||||
PR_Complete ==
|
||||
\A f \in PR_FILES : PR_MergeResults[f] # "pending"
|
||||
|
||||
(*
|
||||
PR success metric: percentage of files successfully merged
|
||||
*)
|
||||
PR_SuccessRate ==
|
||||
LET successful == {f \in PR_FILES : PR_MergeResults[f] = "success"}
|
||||
IN Cardinality(successful) * 100 \div Cardinality(PR_FILES)
|
||||
|
||||
(*
|
||||
PR resolution quality property: a "good" PR resolution is one where
|
||||
all resolvable files are either successfully merged or marked as conflicts
|
||||
(no errors in fetching or processing).
|
||||
*)
|
||||
GoodPRResolution ==
|
||||
\A f \in PR_FILES :
|
||||
Resolvable(f) => PR_MergeResults[f] \in {"success", "conflict"}
|
||||
|
||||
PR_Spec ==
|
||||
PR_Init /\ [][(\E f \in PR_FILES : ProcessFile(f))]_<<PR_MergeResults>>
|
||||
|
||||
PR_Invariant ==
|
||||
PR_Complete => GoodPRResolution
|
||||
|
||||
THEOREM PR_Spec => []PR_Invariant
|
||||
|
||||
=============================================================================
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""WizardMerge package entry point and metadata."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Algorithmic utilities for WizardMerge."""
|
||||
|
||||
from .merge import MergeResult, merge_pairs
|
||||
|
||||
__all__ = ["MergeResult", "merge_pairs"]
|
||||
@@ -1,41 +0,0 @@
|
||||
"""Toy merge utilities to accompany the GUI."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, List, Sequence
|
||||
|
||||
|
||||
@dataclass
|
||||
class MergeResult:
|
||||
"""Hold the combined payload and an audit trail of sources."""
|
||||
|
||||
merged: str
|
||||
sources: List[str]
|
||||
|
||||
|
||||
def merge_pairs(lines_a: Sequence[str], lines_b: Sequence[str]) -> MergeResult:
|
||||
"""Return interleaved lines and capture their origin.
|
||||
|
||||
This function is intentionally simple, providing a deterministic merge
|
||||
strategy useful for demonstration in the GUI layer.
|
||||
"""
|
||||
|
||||
merged_lines: List[str] = []
|
||||
sources: List[str] = []
|
||||
|
||||
for index, (line_a, line_b) in enumerate(zip(lines_a, lines_b)):
|
||||
merged_lines.append(line_a)
|
||||
merged_lines.append(line_b)
|
||||
sources.append(f"A{index}")
|
||||
sources.append(f"B{index}")
|
||||
|
||||
if len(lines_a) > len(lines_b):
|
||||
for tail_index, line in enumerate(lines_a[len(lines_b) :], start=len(lines_b)):
|
||||
merged_lines.append(line)
|
||||
sources.append(f"A{tail_index}")
|
||||
elif len(lines_b) > len(lines_a):
|
||||
for tail_index, line in enumerate(lines_b[len(lines_a) :], start=len(lines_a)):
|
||||
merged_lines.append(line)
|
||||
sources.append(f"B{tail_index}")
|
||||
|
||||
return MergeResult(merged="\n".join(merged_lines), sources=sources)
|
||||
@@ -1,54 +0,0 @@
|
||||
"""Application bootstrap for the WizardMerge PyQt6 + QML UI."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PyQt6.QtCore import QUrl
|
||||
from PyQt6.QtGui import QGuiApplication
|
||||
from PyQt6.QtQml import QQmlApplicationEngine
|
||||
|
||||
from wizardmerge.themes.loader import ThemeManager
|
||||
|
||||
|
||||
def _resolve_qml_path() -> Path:
|
||||
"""Return the absolute path to the main QML entry file."""
|
||||
qml_path = Path(__file__).parent / "qml" / "main.qml"
|
||||
if not qml_path.exists():
|
||||
raise FileNotFoundError("Unable to locate main.qml; ensure resources are installed.")
|
||||
return qml_path
|
||||
|
||||
|
||||
def run(preferred_theme: Optional[str] = None) -> int:
|
||||
"""Run the WizardMerge UI.
|
||||
|
||||
Args:
|
||||
preferred_theme: Optional theme name to prioritize when loading themes.
|
||||
|
||||
Returns:
|
||||
Exit code to propagate to the caller.
|
||||
"""
|
||||
app = QGuiApplication(sys.argv)
|
||||
theme_manager = ThemeManager()
|
||||
theme = theme_manager.select_theme(preferred_theme)
|
||||
|
||||
engine = QQmlApplicationEngine()
|
||||
engine.rootContext().setContextProperty("theme", theme.as_dict())
|
||||
|
||||
qml_path = _resolve_qml_path()
|
||||
engine.load(QUrl.fromLocalFile(qml_path.as_posix()))
|
||||
|
||||
if not engine.rootObjects():
|
||||
return 1
|
||||
|
||||
return app.exec()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Entry-point wrapper for console scripts."""
|
||||
sys.exit(run())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,116 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
ApplicationWindow {
|
||||
width: 720
|
||||
height: 480
|
||||
visible: true
|
||||
title: "WizardMerge"
|
||||
color: theme.background
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
padding: 16
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 64
|
||||
color: theme.surface
|
||||
radius: 8
|
||||
border.color: theme.border
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
color: theme.accent
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 4
|
||||
Text {
|
||||
text: "WizardMerge"
|
||||
font.bold: true
|
||||
color: theme.text
|
||||
font.pointSize: 18
|
||||
}
|
||||
Text {
|
||||
text: "PyQt6 + QML theming demo"
|
||||
color: theme.text
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 1
|
||||
height: 40
|
||||
color: theme.border
|
||||
}
|
||||
|
||||
Text {
|
||||
text: `Current theme: ${theme.name}`
|
||||
color: theme.text
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 320
|
||||
radius: 8
|
||||
color: theme.surface
|
||||
border.color: theme.border
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Algorithm preview"
|
||||
font.bold: true
|
||||
color: theme.text
|
||||
font.pointSize: 14
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
width: parent.width
|
||||
color: theme.border
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Drop your merge data here. The algorithm preview uses a simple interleaving strategy from wizardmerge.algo.merge.merge_pairs."
|
||||
wrapMode: Text.Wrap
|
||||
color: theme.text
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 180
|
||||
color: theme.background
|
||||
radius: 6
|
||||
border.color: theme.border
|
||||
border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Future input widgets will live here."
|
||||
color: theme.text
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Theme helpers for WizardMerge."""
|
||||
|
||||
from .base import Theme
|
||||
from .loader import ThemeManager
|
||||
|
||||
__all__ = ["Theme", "ThemeManager"]
|
||||
@@ -1,17 +0,0 @@
|
||||
"""Core theme definitions."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class Theme:
|
||||
"""Simple theme container for color palette values."""
|
||||
|
||||
name: str
|
||||
palette: Dict[str, str]
|
||||
|
||||
def as_dict(self) -> Dict[str, str]:
|
||||
"""Return a dictionary representation usable by QML contexts."""
|
||||
return {"name": self.name, **self.palette}
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Built-in dark theme."""
|
||||
from wizardmerge.themes.base import Theme
|
||||
|
||||
|
||||
palette = {
|
||||
"background": "#0d1117",
|
||||
"surface": "#161b22",
|
||||
"text": "#e6edf3",
|
||||
"accent": "#7c9aff",
|
||||
"border": "#30363d",
|
||||
}
|
||||
|
||||
theme = Theme(name="Dark", palette=palette)
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Built-in light theme."""
|
||||
from wizardmerge.themes.base import Theme
|
||||
|
||||
|
||||
palette = {
|
||||
"background": "#f5f5f5",
|
||||
"surface": "#ffffff",
|
||||
"text": "#1f2933",
|
||||
"accent": "#0f7ada",
|
||||
"border": "#d8d8d8",
|
||||
}
|
||||
|
||||
theme = Theme(name="Light", palette=palette)
|
||||
@@ -1,76 +0,0 @@
|
||||
"""Theme loading and plugin discovery helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from pkgutil import iter_modules
|
||||
from typing import Iterable, List, Sequence
|
||||
|
||||
from wizardmerge.themes.base import Theme
|
||||
|
||||
|
||||
class ThemeManager:
|
||||
"""Manage built-in and plugin-based themes."""
|
||||
|
||||
def __init__(self, extra_plugin_paths: Sequence[Path] | None = None) -> None:
|
||||
self._builtin_modules = self._discover_builtin_modules()
|
||||
self._plugin_modules = self._discover_plugin_modules(extra_plugin_paths)
|
||||
|
||||
@staticmethod
|
||||
def _discover_builtin_modules() -> List[str]:
|
||||
"""Return module names for bundled themes."""
|
||||
module_path = Path(__file__).parent
|
||||
modules = []
|
||||
for module in iter_modules([str(module_path)]):
|
||||
if module.name.endswith("_theme"):
|
||||
modules.append(f"{__package__}.{module.name}")
|
||||
return modules
|
||||
|
||||
@staticmethod
|
||||
def _discover_plugin_modules(extra_paths: Sequence[Path] | None) -> List[str]:
|
||||
"""Return module names for shipped plugin examples and user-defined themes."""
|
||||
modules: List[str] = []
|
||||
plugin_package = f"{__package__}.plugins"
|
||||
plugin_path = Path(__file__).parent / "plugins"
|
||||
modules.extend(
|
||||
f"{plugin_package}.{module.name}" for module in iter_modules([str(plugin_path)]) if module.ispkg is False
|
||||
)
|
||||
|
||||
if extra_paths:
|
||||
for path in extra_paths:
|
||||
if not path.exists():
|
||||
continue
|
||||
sys.path.append(str(path))
|
||||
modules.extend(module.name for module in iter_modules([str(path)]))
|
||||
|
||||
return modules
|
||||
|
||||
def _load_theme_from_module(self, module_name: str) -> Theme | None:
|
||||
module = importlib.import_module(module_name)
|
||||
theme = getattr(module, "theme", None) or getattr(module, "warm_theme", None)
|
||||
if isinstance(theme, Theme):
|
||||
return theme
|
||||
return None
|
||||
|
||||
def available_themes(self) -> List[Theme]:
|
||||
"""Return a list of all themes that could be loaded."""
|
||||
themes: List[Theme] = []
|
||||
for module in [*self._builtin_modules, *self._plugin_modules]:
|
||||
theme = self._load_theme_from_module(module)
|
||||
if theme:
|
||||
themes.append(theme)
|
||||
return themes
|
||||
|
||||
def select_theme(self, preferred_name: str | None = None) -> Theme:
|
||||
"""Return the preferred theme or fall back to the first available one."""
|
||||
themes = self.available_themes()
|
||||
if not themes:
|
||||
raise RuntimeError("No themes could be loaded.")
|
||||
|
||||
if preferred_name:
|
||||
for theme in themes:
|
||||
if theme.name.lower() == preferred_name.lower():
|
||||
return theme
|
||||
|
||||
return themes[0]
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Example plugin themes shipped with the project."""
|
||||
from wizardmerge.themes.base import Theme
|
||||
|
||||
from .warm_theme import warm_theme
|
||||
|
||||
__all__ = ["warm_theme", "Theme"]
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Sample theme plugin distributed separately from the built-ins."""
|
||||
from wizardmerge.themes.base import Theme
|
||||
|
||||
|
||||
palette = {
|
||||
"background": "#fdf1e5",
|
||||
"surface": "#fde7d3",
|
||||
"text": "#3b2f2f",
|
||||
"accent": "#d67b4d",
|
||||
"border": "#f6c4a3",
|
||||
}
|
||||
|
||||
warm_theme = Theme(name="Warm", palette=palette)
|
||||
Reference in New Issue
Block a user